mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-04 06:29:42 +02:00
ui: improve respawn controller layout and filter action log noise
- Stack timers row vertically (timers on top, action log below) - Reduce action log height to 60px since it's now full width - Filter action log to show only important entries: - Commands sent to console - Plan-check with action taken - Step completions - Skip timer starts/cancels, detection updates, ai-check status Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -165,7 +165,7 @@ const CYCLE_PATTERN = /cycle\s*#?(\d+)|respawn cycle #(\d+)/i;
|
||||
* Examples: "Iteration 5/50", "[5/50]", "iteration #5", "iter. 3 of 10"
|
||||
* Capture groups: (1,2) for "Iteration X/Y" format, (3,4) for "[X/Y]" format
|
||||
*/
|
||||
const ITERATION_PATTERN = /(?:iteration|iter\.?)\s*#?(\d+)(?:\s*[\/of]\s*(\d+))?|\[(\d+)\/(\d+)\]/i;
|
||||
const ITERATION_PATTERN = /(?:iteration|iter\.?)\s*#?(\d+)(?:\s*(?:\/|of)\s*(\d+))?|\[(\d+)\/(\d+)\]/i;
|
||||
|
||||
/**
|
||||
* Matches Ralph loop start command or announcement
|
||||
|
||||
@@ -667,6 +667,10 @@ export class RespawnController extends EventEmitter {
|
||||
Object.entries(config).filter(([, v]) => v !== undefined)
|
||||
) as Partial<RespawnConfig>;
|
||||
this.config = { ...DEFAULT_CONFIG, ...filteredConfig };
|
||||
|
||||
// Validate configuration values
|
||||
this.validateConfig();
|
||||
|
||||
this.aiChecker = new AiIdleChecker(session.id, {
|
||||
enabled: this.config.aiIdleCheckEnabled,
|
||||
model: this.config.aiIdleCheckModel,
|
||||
@@ -685,6 +689,36 @@ export class RespawnController extends EventEmitter {
|
||||
this.setupPlanCheckerListeners();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate configuration values and reset invalid ones to defaults.
|
||||
* Ensures timeouts are positive and logically consistent.
|
||||
*/
|
||||
private validateConfig(): void {
|
||||
const c = this.config;
|
||||
|
||||
// Ensure timeouts are positive
|
||||
if (c.idleTimeoutMs <= 0) c.idleTimeoutMs = DEFAULT_CONFIG.idleTimeoutMs;
|
||||
if (c.completionConfirmMs <= 0) c.completionConfirmMs = DEFAULT_CONFIG.completionConfirmMs;
|
||||
if (c.noOutputTimeoutMs <= 0) c.noOutputTimeoutMs = DEFAULT_CONFIG.noOutputTimeoutMs;
|
||||
if (c.autoAcceptDelayMs < 0) c.autoAcceptDelayMs = DEFAULT_CONFIG.autoAcceptDelayMs;
|
||||
if (c.interStepDelayMs <= 0) c.interStepDelayMs = DEFAULT_CONFIG.interStepDelayMs;
|
||||
|
||||
// Ensure completion confirm doesn't exceed no-output timeout
|
||||
if (c.completionConfirmMs > c.noOutputTimeoutMs) {
|
||||
c.completionConfirmMs = c.noOutputTimeoutMs;
|
||||
}
|
||||
|
||||
// Ensure AI check timeouts are positive
|
||||
if (c.aiIdleCheckTimeoutMs <= 0) c.aiIdleCheckTimeoutMs = DEFAULT_CONFIG.aiIdleCheckTimeoutMs;
|
||||
if (c.aiIdleCheckCooldownMs < 0) c.aiIdleCheckCooldownMs = DEFAULT_CONFIG.aiIdleCheckCooldownMs;
|
||||
if (c.aiIdleCheckMaxContext <= 0) c.aiIdleCheckMaxContext = DEFAULT_CONFIG.aiIdleCheckMaxContext;
|
||||
|
||||
// Ensure plan check timeouts are positive
|
||||
if (c.aiPlanCheckTimeoutMs <= 0) c.aiPlanCheckTimeoutMs = DEFAULT_CONFIG.aiPlanCheckTimeoutMs;
|
||||
if (c.aiPlanCheckCooldownMs < 0) c.aiPlanCheckCooldownMs = DEFAULT_CONFIG.aiPlanCheckCooldownMs;
|
||||
if (c.aiPlanCheckMaxContext <= 0) c.aiPlanCheckMaxContext = DEFAULT_CONFIG.aiPlanCheckMaxContext;
|
||||
}
|
||||
|
||||
/** Wire up AI checker events to controller events */
|
||||
private setupAiCheckerListeners(): void {
|
||||
this.aiChecker.on('log', (message: string) => {
|
||||
|
||||
+6
-1
@@ -61,7 +61,12 @@ const LINE_BUFFER_FLUSH_INTERVAL = 100;
|
||||
const FOCUS_ESCAPE_FILTER = /\x1b\[\?1004[hl]|\x1b\[[IO]/g;
|
||||
|
||||
// Pre-compiled regex patterns for performance (avoid re-compilation on each call)
|
||||
const ANSI_ESCAPE_PATTERN = /\x1b\[[0-9;]*m/g;
|
||||
// Comprehensive ANSI escape pattern:
|
||||
// - SGR (colors/styles): ESC [ params m
|
||||
// - CSI sequences (cursor, scroll, etc.): ESC [ params letter
|
||||
// - OSC sequences (title, etc.): ESC ] ... BEL or ESC ] ... ST
|
||||
// - Single-char escapes: ESC = or ESC >
|
||||
const ANSI_ESCAPE_PATTERN = /\x1b(?:\[[0-9;?]*[A-Za-z]|\][^\x07\x1b]*(?:\x07|\x1b\\)|[=>])/g;
|
||||
const TOKEN_PATTERN = /(\d+(?:\.\d+)?)\s*([kKmM])?\s*tokens/;
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@@ -227,6 +227,7 @@ export class SpawnOrchestrator extends EventEmitter {
|
||||
|
||||
/**
|
||||
* Cancel an agent by ID.
|
||||
* Cascades cancellation to all child agents before cleaning up the parent.
|
||||
*/
|
||||
async cancelAgent(agentId: string, reason: string = 'Cancelled by parent'): Promise<void> {
|
||||
const agent = this._agents.get(agentId);
|
||||
@@ -241,6 +242,28 @@ export class SpawnOrchestrator extends EventEmitter {
|
||||
return;
|
||||
}
|
||||
|
||||
// Cancel all child agents first (cascade)
|
||||
// Child agents have their parentSessionId set to this agent's sessionId
|
||||
if (agent.sessionId) {
|
||||
for (const [childId, childAgent] of this._agents) {
|
||||
if (childAgent.parentSessionId === agent.sessionId && childAgent.status !== 'cancelled') {
|
||||
await this.cancelAgent(childId, `Parent ${agentId} cancelled`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also remove any queued tasks that depend on this agent's session
|
||||
if (agent.sessionId) {
|
||||
const queuedChildren = this._queue.filter(t => t.parentSessionId === agent.sessionId);
|
||||
for (const task of queuedChildren) {
|
||||
const idx = this._queue.indexOf(task);
|
||||
if (idx >= 0) {
|
||||
this._queue.splice(idx, 1);
|
||||
this.emit('cancelled', { agentId: task.spec.agentId, reason: `Parent ${agentId} cancelled` });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
agent.status = 'cancelled';
|
||||
this.emit('cancelled', { agentId, reason });
|
||||
|
||||
|
||||
+52
-1
@@ -54,9 +54,16 @@ export class TaskQueue extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
/** Adds a new task to the queue. */
|
||||
/**
|
||||
* Adds a new task to the queue.
|
||||
* @throws Error if the task's dependencies would create a circular dependency
|
||||
*/
|
||||
addTask(options: CreateTaskOptions): Task {
|
||||
const task = new Task(options);
|
||||
// Validate dependencies before adding to prevent circular dependency deadlocks
|
||||
if (options.dependencies && options.dependencies.length > 0) {
|
||||
this.validateDependencies(task.id, options.dependencies);
|
||||
}
|
||||
this.tasks.set(task.id, task);
|
||||
this.store.setTask(task.id, task.toState());
|
||||
this.emit('taskAdded', task);
|
||||
@@ -151,6 +158,50 @@ export class TaskQueue extends EventEmitter {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect if adding a dependency would create a cycle.
|
||||
* Uses DFS to check if there's a path from depId back to taskId.
|
||||
*
|
||||
* @param taskId - The task that would have the new dependency
|
||||
* @param depId - The dependency being added
|
||||
* @param visited - Set of already visited nodes (for DFS)
|
||||
* @returns true if adding this dependency would create a cycle
|
||||
*/
|
||||
private wouldCreateCycle(taskId: string, depId: string, visited: Set<string> = new Set()): boolean {
|
||||
// Direct self-reference
|
||||
if (depId === taskId) return true;
|
||||
// Already visited this node in current path
|
||||
if (visited.has(depId)) return false;
|
||||
|
||||
visited.add(depId);
|
||||
const depTask = this.tasks.get(depId);
|
||||
if (!depTask) return false;
|
||||
|
||||
// Recursively check all dependencies of the dependency
|
||||
for (const nextDep of depTask.dependencies) {
|
||||
if (this.wouldCreateCycle(taskId, nextDep, visited)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that a set of dependencies won't create cycles for a given task.
|
||||
* Throws an error if a circular dependency is detected.
|
||||
*
|
||||
* @param taskId - The task ID that will have these dependencies
|
||||
* @param dependencies - Array of dependency task IDs to validate
|
||||
* @throws Error if a circular dependency would be created
|
||||
*/
|
||||
private validateDependencies(taskId: string, dependencies: string[]): void {
|
||||
for (const depId of dependencies) {
|
||||
if (this.wouldCreateCycle(taskId, depId)) {
|
||||
throw new Error(`Circular dependency detected: adding dependency ${depId} to task ${taskId} would create a cycle`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Gets all tasks assigned to a specific session. */
|
||||
getTasksBySession(sessionId: string): Task[] {
|
||||
return this.getAllTasks().filter((t) => t.assignedSessionId === sessionId);
|
||||
|
||||
@@ -2247,6 +2247,15 @@ class ClaudemanApp {
|
||||
// ========== Countdown Timer Display Methods ==========
|
||||
|
||||
addActionLogEntry(sessionId, action) {
|
||||
// Filter to important actions only
|
||||
// Skip: timer starts/cancels, detection updates, ai-check status
|
||||
// Keep: command (sent to console), plan-check (with action), step (completions)
|
||||
if (action.type === 'timer' || action.type === 'timer-cancel') return;
|
||||
if (action.type === 'detection') return;
|
||||
if (action.type === 'ai-check') return;
|
||||
if (action.type === 'plan-check' && !action.detail.includes('sending')) return;
|
||||
if (action.type === 'step' && !action.detail.includes('completed')) return;
|
||||
|
||||
if (!this.respawnActionLogs[sessionId]) {
|
||||
this.respawnActionLogs[sessionId] = [];
|
||||
}
|
||||
|
||||
@@ -470,8 +470,8 @@ body {
|
||||
|
||||
.respawn-timers-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
padding-top: 0.35rem;
|
||||
border-top: 1px solid rgba(34, 197, 94, 0.15);
|
||||
margin-top: 0.35rem;
|
||||
@@ -479,7 +479,7 @@ body {
|
||||
|
||||
.respawn-countdown-timers {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
gap: 0.4rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
@@ -520,8 +520,8 @@ body {
|
||||
}
|
||||
|
||||
.respawn-action-log {
|
||||
flex: 1;
|
||||
max-height: 100px;
|
||||
width: 100%;
|
||||
max-height: 60px;
|
||||
overflow-y: auto;
|
||||
font-size: 0.7rem;
|
||||
opacity: 0.9;
|
||||
|
||||
Reference in New Issue
Block a user