mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-05 06:59:42 +02:00
chore: bump version to 0.1431
This commit is contained in:
+25
-51
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user