feat: add per-agent logging UI and fix stuck agent issues

Changes:
- Add per-agent log viewer with copy-to-clipboard functionality
  - New AgentLogEntry type for structured log entries
  - Logs stored per-agent in WebSocket state (up to 500 entries)
  - Log modal rendered via React Portal to avoid overflow issues
  - Click log icon on agent card to view full activity history

- Fix agents getting stuck in "failed" state
  - Wrap client context manager in try/except (agent.py)
  - Remove failed agents from UI on error state (useWebSocket.ts)
  - Handle permanently failed features in get_all_complete()

- Add friendlier agent state labels
  - "Hit an issue" → "Trying plan B..."
  - "Retrying..." → "Being persistent..."
  - Softer colors (yellow/orange instead of red)

- Add scheduling scores for smarter feature ordering
  - compute_scheduling_scores() in dependency_resolver.py
  - Features that unblock others get higher priority

- Update CLAUDE.md with parallel mode documentation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Auto
2026-01-17 14:11:24 +02:00
parent 85f6940a54
commit bf3a6b0b73
10 changed files with 387 additions and 49 deletions

View File

@@ -9,6 +9,7 @@ import type {
DevServerStatus,
ActiveAgent,
AgentMascot,
AgentLogEntry,
} from '../lib/types'
// Activity item for the feed
@@ -42,6 +43,8 @@ interface WebSocketState {
// Multi-agent state
activeAgents: ActiveAgent[]
recentActivity: ActivityItem[]
// Per-agent logs for debugging (indexed by agentIndex)
agentLogs: Map<number, AgentLogEntry[]>
// Celebration queue to handle rapid successes without race conditions
celebrationQueue: CelebrationTrigger[]
celebration: CelebrationTrigger | null
@@ -49,6 +52,7 @@ interface WebSocketState {
const MAX_LOGS = 100 // Keep last 100 log lines
const MAX_ACTIVITY = 20 // Keep last 20 activity items
const MAX_AGENT_LOGS = 500 // Keep last 500 log lines per agent
export function useProjectWebSocket(projectName: string | null) {
const [state, setState] = useState<WebSocketState>({
@@ -61,6 +65,7 @@ export function useProjectWebSocket(projectName: string | null) {
devLogs: [],
activeAgents: [],
recentActivity: [],
agentLogs: new Map(),
celebrationQueue: [],
celebration: null,
})
@@ -111,9 +116,9 @@ export function useProjectWebSocket(projectName: string | null) {
break
case 'log':
setState(prev => ({
...prev,
logs: [
setState(prev => {
// Update global logs
const newLogs = [
...prev.logs.slice(-MAX_LOGS + 1),
{
line: message.line,
@@ -121,8 +126,26 @@ export function useProjectWebSocket(projectName: string | null) {
featureId: message.featureId,
agentIndex: message.agentIndex,
},
],
}))
]
// Also store in per-agent logs if we have an agentIndex
let newAgentLogs = prev.agentLogs
if (message.agentIndex !== undefined) {
newAgentLogs = new Map(prev.agentLogs)
const existingLogs = newAgentLogs.get(message.agentIndex) || []
const logEntry: AgentLogEntry = {
line: message.line,
timestamp: message.timestamp,
type: 'output',
}
newAgentLogs.set(
message.agentIndex,
[...existingLogs.slice(-MAX_AGENT_LOGS + 1), logEntry]
)
}
return { ...prev, logs: newLogs, agentLogs: newAgentLogs }
})
break
case 'feature_update':
@@ -131,21 +154,38 @@ export function useProjectWebSocket(projectName: string | null) {
case 'agent_update':
setState(prev => {
// Log state change to per-agent logs
const newAgentLogs = new Map(prev.agentLogs)
const existingLogs = newAgentLogs.get(message.agentIndex) || []
const stateLogEntry: AgentLogEntry = {
line: `[STATE] ${message.state}${message.thought ? `: ${message.thought}` : ''}`,
timestamp: message.timestamp,
type: message.state === 'error' ? 'error' : 'state_change',
}
newAgentLogs.set(
message.agentIndex,
[...existingLogs.slice(-MAX_AGENT_LOGS + 1), stateLogEntry]
)
// Get current logs for this agent to attach to ActiveAgent
const agentLogsArray = newAgentLogs.get(message.agentIndex) || []
// Update or add the agent in activeAgents
const agentIndex = prev.activeAgents.findIndex(
const existingAgentIdx = prev.activeAgents.findIndex(
a => a.agentIndex === message.agentIndex
)
let newAgents: ActiveAgent[]
if (message.state === 'success') {
// Remove agent from active list on success
if (message.state === 'success' || message.state === 'error') {
// Remove agent from active list on completion (success or failure)
// But keep the logs in agentLogs map for debugging
newAgents = prev.activeAgents.filter(
a => a.agentIndex !== message.agentIndex
)
} else if (agentIndex >= 0) {
} else if (existingAgentIdx >= 0) {
// Update existing agent
newAgents = [...prev.activeAgents]
newAgents[agentIndex] = {
newAgents[existingAgentIdx] = {
agentIndex: message.agentIndex,
agentName: message.agentName,
featureId: message.featureId,
@@ -153,6 +193,7 @@ export function useProjectWebSocket(projectName: string | null) {
state: message.state,
thought: message.thought,
timestamp: message.timestamp,
logs: agentLogsArray,
}
} else {
// Add new agent
@@ -166,6 +207,7 @@ export function useProjectWebSocket(projectName: string | null) {
state: message.state,
thought: message.thought,
timestamp: message.timestamp,
logs: agentLogsArray,
},
]
}
@@ -207,6 +249,7 @@ export function useProjectWebSocket(projectName: string | null) {
return {
...prev,
activeAgents: newAgents,
agentLogs: newAgentLogs,
recentActivity: newActivity,
celebrationQueue: newCelebrationQueue,
celebration: newCelebration,
@@ -295,6 +338,7 @@ export function useProjectWebSocket(projectName: string | null) {
devLogs: [],
activeAgents: [],
recentActivity: [],
agentLogs: new Map(),
celebrationQueue: [],
celebration: null,
})
@@ -335,10 +379,26 @@ export function useProjectWebSocket(projectName: string | null) {
setState(prev => ({ ...prev, devLogs: [] }))
}, [])
// Get logs for a specific agent (useful for debugging even after agent completes/fails)
const getAgentLogs = useCallback((agentIndex: number): AgentLogEntry[] => {
return state.agentLogs.get(agentIndex) || []
}, [state.agentLogs])
// Clear logs for a specific agent
const clearAgentLogs = useCallback((agentIndex: number) => {
setState(prev => {
const newAgentLogs = new Map(prev.agentLogs)
newAgentLogs.delete(agentIndex)
return { ...prev, agentLogs: newAgentLogs }
})
}, [])
return {
...state,
clearLogs,
clearDevLogs,
clearCelebration,
getAgentLogs,
clearAgentLogs,
}
}