chore: bump version to 0.1431

This commit is contained in:
arkon
2026-01-29 13:22:02 +01:00
parent 8ed1c218f4
commit 7b866c1998
14 changed files with 29 additions and 3512 deletions
+25 -51
View File
@@ -466,7 +466,7 @@ class ClaudemanApp {
this._subagentHideTimeout = null; // Timeout for hover-based dropdown hide
this.ralphStatePanelCollapsed = true; // Default to collapsed
// Plan subagent windows (visible Opus agents during plan generation)
// Plan subagent windows (visible agents during plan generation)
this.planSubagents = new Map(); // Map<agentId, { type, model, status, startTime, element, relativePos }>
this.planSubagentWindowZIndex = 1100;
this.planGenerationStopped = false; // Flag to ignore SSE events after Stop
@@ -641,7 +641,7 @@ class ClaudemanApp {
fontFamily: '"Fira Code", "Cascadia Code", "JetBrains Mono", "SF Mono", Monaco, monospace',
fontSize: 14,
lineHeight: 1.2,
cursorBlink: true,
cursorBlink: false,
cursorStyle: 'block',
scrollback: scrollback,
allowTransparency: true,
@@ -1146,6 +1146,10 @@ class ClaudemanApp {
// This connects subagents that were waiting for the session to identify itself
if (claudeSessionIdJustSet) {
this.recheckOrphanSubagents();
// Update connection lines after DOM settles (ensure tabs are rendered)
requestAnimationFrame(() => {
this.updateConnectionLines();
});
}
});
@@ -1690,55 +1694,6 @@ class ClaudemanApp {
this.handleBashToolsUpdate(data.sessionId, data.tools);
});
// Spawn agent notification events
addListener('spawn:failed', (e) => {
const data = JSON.parse(e.data);
this.notificationManager?.notify({
urgency: 'critical',
category: 'spawn-failed',
sessionId: data.sessionId,
sessionName: data.agentId || 'agent',
title: 'Agent Failed',
message: `Agent "${data.agentId}" failed: ${data.reason || 'unknown'}`,
});
});
addListener('spawn:timeout', (e) => {
const data = JSON.parse(e.data);
this.notificationManager?.notify({
urgency: 'critical',
category: 'spawn-timeout',
sessionId: data.sessionId,
sessionName: data.agentId || 'agent',
title: 'Agent Timeout',
message: `Agent "${data.agentId}" exceeded time limit`,
});
});
addListener('spawn:budgetWarning', (e) => {
const data = JSON.parse(e.data);
this.notificationManager?.notify({
urgency: 'warning',
category: 'spawn-budget',
sessionId: data.sessionId,
sessionName: data.agentId || 'agent',
title: 'Budget Warning',
message: `Agent "${data.agentId}" at ${data.percent || 80}% budget`,
});
});
addListener('spawn:completed', (e) => {
const data = JSON.parse(e.data);
this.notificationManager?.notify({
urgency: 'info',
category: 'spawn-completed',
sessionId: data.sessionId,
sessionName: data.agentId || 'agent',
title: 'Agent Complete',
message: `Agent "${data.agentId}" finished successfully`,
});
});
// Hook events (from Claude Code hooks system)
// Use pendingHooks state machine to track hook events and derive tab alerts.
// This ensures alerts persist even when session:working events fire.
@@ -1832,6 +1787,11 @@ class ClaudemanApp {
if (data.status === 'active') {
this.openSubagentWindow(data.agentId);
}
// Ensure connection lines are updated after window is created and DOM settles
requestAnimationFrame(() => {
this.updateConnectionLines();
});
});
addListener('subagent:updated', (e) => {
@@ -7658,6 +7618,11 @@ class ClaudemanApp {
this.renderSessionTabs(); // Update tab badges
this.saveSubagentWindowStates(); // Persist corrected mappings
// Update connection lines after all windows are restored (use rAF to ensure DOM is ready)
requestAnimationFrame(() => {
this.updateConnectionLines();
});
}
// ========== Help Modal ==========
@@ -9425,11 +9390,20 @@ class ClaudemanApp {
* Called when session:updated fires, in case claudeSessionId was just set.
*/
recheckOrphanSubagents() {
let anyFound = false;
for (const [agentId, agent] of this.subagents) {
if (!agent.parentSessionId && agent.sessionId) {
const hadParent = agent.parentSessionId;
this.findParentSessionForSubagent(agentId);
if (!hadParent && this.subagents.get(agentId)?.parentSessionId) {
anyFound = true;
}
}
}
// Ensure connection lines are updated after all orphans are processed
if (anyFound) {
this.updateConnectionLines();
}
}
/**
-207
View File
@@ -22,8 +22,6 @@ import { EventEmitter } from 'node:events';
import { Session, ClaudeMessage, type BackgroundTask, type RalphTrackerState, type RalphTodoItem, type ActiveBashTool } from '../session.js';
import { fileStreamManager } from '../file-stream-manager.js';
import { RespawnController, RespawnConfig, RespawnState } from '../respawn-controller.js';
import { SpawnOrchestrator, type SessionCreator } from '../spawn-orchestrator.js';
import type { SpawnOrchestratorConfig } from '../spawn-types.js';
import { ScreenManager } from '../screen-manager.js';
import { getStore } from '../state-store.js';
import { generateClaudeMd } from '../templates/claude-md.js';
@@ -307,8 +305,6 @@ export class WebServer extends EventEmitter {
private sseHealthCheckTimer: NodeJS.Timeout | null = null;
// Flag to prevent new timers during shutdown
private _isStopping: boolean = false;
// Spawn1337 agent orchestrator
private spawnOrchestrator: SpawnOrchestrator;
// Token recording for daily stats (track what's been recorded to avoid double-counting)
private lastRecordedTokens: Map<string, { input: number; output: number }> = new Map();
private tokenRecordingTimer: NodeJS.Timeout | null = null;
@@ -350,10 +346,6 @@ export class WebServer extends EventEmitter {
this.broadcast('screen:statsUpdated', screens);
});
// Initialize spawn orchestrator
this.spawnOrchestrator = new SpawnOrchestrator();
this.setupSpawnOrchestratorListeners();
// Initialize execution bridge with model config from settings
this.executionBridge = getExecutionBridge(this.loadModelConfig());
this.setupExecutionBridgeListeners();
@@ -510,7 +502,6 @@ export class WebServer extends EventEmitter {
// Returns comprehensive memory metrics for debugging memory leaks
this.app.get('/api/debug/memory', async () => {
const mem = process.memoryUsage();
const spawnState = this.spawnOrchestrator.getState();
const subagentStats = subagentWatcher.getStats();
// Calculate total Map entries for memory estimation
@@ -569,13 +560,6 @@ export class WebServer extends EventEmitter {
subagentIdleTimers: subagentStats.idleTimerCount,
total: this.respawnTimers.size + this.pendingRespawnStarts.size + subagentStats.idleTimerCount,
},
spawn: {
activeAgents: spawnState.activeCount,
queuedAgents: spawnState.queuedCount,
totalSpawned: spawnState.totalSpawned,
totalCompleted: spawnState.totalCompleted,
totalFailed: spawnState.totalFailed,
},
uptime: {
seconds: Math.round(process.uptime()),
formatted: formatUptime(process.uptime()),
@@ -1987,9 +1971,6 @@ export class WebServer extends EventEmitter {
const claudeMd = generateClaudeMd(name, description || '', templatePath);
writeFileSync(join(casePath, 'CLAUDE.md'), claudeMd);
// Write .mcp.json for Claude Code to discover spawn tools
this.writeMcpConfig(casePath);
// Write .claude/settings.local.json with hooks for desktop notifications
writeHooksConfig(casePath);
@@ -2236,9 +2217,6 @@ export class WebServer extends EventEmitter {
const claudeMd = generateClaudeMd(caseName, '', templatePath);
writeFileSync(join(casePath, 'CLAUDE.md'), claudeMd);
// Write .mcp.json for Claude Code to discover spawn tools
this.writeMcpConfig(casePath);
// Write .claude/settings.local.json with hooks for desktop notifications
writeHooksConfig(casePath);
@@ -3019,93 +2997,6 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
return this.getSystemStats();
});
// ========== Spawn1337 Agent Protocol Endpoints ==========
this.app.get('/api/spawn/agents', async () => {
return { success: true, data: this.spawnOrchestrator.getAllAgentStatuses() };
});
this.app.get('/api/spawn/agents/:agentId', async (req) => {
const { agentId } = req.params as { agentId: string };
const status = this.spawnOrchestrator.getAgentStatus(agentId);
if (!status) {
return createErrorResponse(ApiErrorCode.NOT_FOUND, `Agent ${agentId} not found`);
}
return { success: true, data: status };
});
this.app.get('/api/spawn/agents/:agentId/result', async (req) => {
const { agentId } = req.params as { agentId: string };
const result = this.spawnOrchestrator.readAgentResult(agentId);
if (!result) {
return createErrorResponse(ApiErrorCode.NOT_FOUND, `No result found for agent ${agentId}`);
}
return { success: true, data: result };
});
this.app.get('/api/spawn/agents/:agentId/progress', async (req) => {
const { agentId } = req.params as { agentId: string };
const progress = this.spawnOrchestrator.readAgentProgress(agentId);
return { success: true, data: progress };
});
this.app.get('/api/spawn/agents/:agentId/messages', async (req) => {
const { agentId } = req.params as { agentId: string };
const messages = this.spawnOrchestrator.readAgentMessages(agentId);
return { success: true, data: messages };
});
this.app.post('/api/spawn/agents/:agentId/message', async (req) => {
const { agentId } = req.params as { agentId: string };
const { content } = req.body as { content: string };
if (!content) {
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'Message content is required');
}
await this.spawnOrchestrator.sendMessageToAgent(agentId, content);
return { success: true };
});
this.app.post('/api/spawn/agents/:agentId/cancel', async (req) => {
const { agentId } = req.params as { agentId: string };
const { reason } = (req.body as { reason?: string }) || {};
await this.spawnOrchestrator.cancelAgent(agentId, reason || 'Cancelled via API');
return { success: true };
});
this.app.delete('/api/spawn/agents/:agentId', async (req) => {
const { agentId } = req.params as { agentId: string };
await this.spawnOrchestrator.cancelAgent(agentId, 'Force killed via API');
return { success: true };
});
this.app.get('/api/spawn/status', async () => {
return { success: true, data: this.spawnOrchestrator.getState() };
});
this.app.put('/api/spawn/config', async (req) => {
const config = req.body as Partial<SpawnOrchestratorConfig>;
this.spawnOrchestrator.updateConfig(config);
return { success: true, data: this.spawnOrchestrator.config };
});
this.app.post('/api/spawn/trigger', async (req) => {
const { taskContent, parentSessionId, parentWorkingDir } = req.body as {
taskContent: string;
parentSessionId: string;
parentWorkingDir?: string;
};
if (!taskContent || !parentSessionId) {
return createErrorResponse(ApiErrorCode.INVALID_INPUT, 'taskContent and parentSessionId are required');
}
const session = this.sessions.get(parentSessionId);
const workingDir = parentWorkingDir || session?.workingDir || process.cwd();
const agentId = await this.spawnOrchestrator.triggerSpawn(taskContent, parentSessionId, workingDir);
if (!agentId) {
return createErrorResponse(ApiErrorCode.OPERATION_FAILED, 'Failed to parse task spec');
}
return { success: true, data: { agentId } };
});
// ========== Execution Bridge Endpoints ==========
// Get execution status
@@ -3971,83 +3862,6 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
});
}
private setupSpawnOrchestratorListeners(): void {
const sessionCreator: SessionCreator = {
createAgentSession: async (workingDir: string, name: string) => {
const globalNice = this.getGlobalNiceConfig();
const session = new Session({
workingDir,
screenManager: this.screenManager,
useScreen: true,
mode: 'claude',
name: `spawn:${name}`,
niceConfig: globalNice,
});
this.sessions.set(session.id, session);
this.store.incrementSessionsCreated();
this.setupSessionListeners(session);
session.parentAgentId = name;
await session.startInteractive();
this.broadcast('session:created', session.toDetailedState());
this.broadcast('session:interactive', { id: session.id });
this.persistSessionState(session);
// Configure ralph tracker for completion detection
session.ralphTracker.enable();
return { sessionId: session.id };
},
writeToSession: (sessionId: string, data: string) => {
const session = this.sessions.get(sessionId);
if (session) {
session.writeViaScreen(data);
}
},
getSessionTokens: (sessionId: string) => {
const session = this.sessions.get(sessionId);
return session ? session.totalTokens : 0;
},
getSessionCost: (sessionId: string) => {
const session = this.sessions.get(sessionId);
return session ? session.totalCost : 0;
},
stopSession: async (sessionId: string) => {
// Use cleanupSession to properly clean up all resources (respawn controllers,
// run summary trackers, file streams, Ralph state, etc.)
await this.cleanupSession(sessionId);
},
onSessionCompletion: (sessionId: string, handler: (phrase: string) => void) => {
const session = this.sessions.get(sessionId);
if (session) {
session.on('ralphCompletionDetected', handler);
}
},
removeSessionCompletionHandler: (sessionId: string, handler: (phrase: string) => void) => {
const session = this.sessions.get(sessionId);
if (session) {
session.off('ralphCompletionDetected', handler);
}
},
};
this.spawnOrchestrator.setSessionCreator(sessionCreator);
// Forward orchestrator events as SSE broadcasts
this.spawnOrchestrator.on('queued', (data) => this.broadcast('spawn:queued', data));
this.spawnOrchestrator.on('initializing', (data) => this.broadcast('spawn:initializing', data));
this.spawnOrchestrator.on('started', (data) => this.broadcast('spawn:started', data));
this.spawnOrchestrator.on('progress', (data) => this.broadcast('spawn:progress', data));
this.spawnOrchestrator.on('message', (data) => this.broadcast('spawn:message', data));
this.spawnOrchestrator.on('completed', (data) => this.broadcast('spawn:completed', data));
this.spawnOrchestrator.on('failed', (data) => this.broadcast('spawn:failed', data));
this.spawnOrchestrator.on('timeout', (data) => this.broadcast('spawn:timeout', data));
this.spawnOrchestrator.on('cancelled', (data) => this.broadcast('spawn:cancelled', data));
this.spawnOrchestrator.on('budgetWarning', (data) => this.broadcast('spawn:budgetWarning', data));
this.spawnOrchestrator.on('stateUpdate', (data) => this.broadcast('spawn:stateUpdate', data));
}
/**
* Load model configuration from settings file.
*/
@@ -4373,23 +4187,6 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
return undefined;
}
/**
* Write .mcp.json to a case directory for Claude Code to discover spawn MCP tools.
*/
private writeMcpConfig(casePath: string): void {
const projectRoot = join(__dirname, '..', '..');
const mcpServerPath = join(projectRoot, 'dist', 'mcp-server.js');
const mcpConfig = {
mcpServers: {
'claudeman-spawn': {
command: 'node',
args: [mcpServerPath],
},
},
};
writeFileSync(join(casePath, '.mcp.json'), JSON.stringify(mcpConfig, null, 2) + '\n');
}
private async startScheduledRun(prompt: string, workingDir: string, durationMinutes: number): Promise<ScheduledRun> {
const id = uuidv4();
const now = Date.now();
@@ -5147,10 +4944,6 @@ NOW: Generate the implementation plan for the task above. Think step by step.`;
}
this.respawnControllers.clear();
// Stop spawn orchestrator and all agents
await this.spawnOrchestrator.stopAll();
this.spawnOrchestrator.removeAllListeners();
// Stop all scheduled runs first (they have their own session cleanup)
for (const [id] of this.scheduledRuns) {
await this.stopScheduledRun(id);