mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 06:12:03 +00:00
feat: enhance agent info panel with real-time task status updates and fresh planSpec integration
- Added support for real-time task status updates using WebSocket events, allowing the Kanban card to reflect current task progress accurately. - Introduced a new state for fresh planSpec data fetched from the API to ensure the agent info panel displays up-to-date task information. - Updated the effectiveTodos calculation to prioritize fresh planSpec data and incorporate real-time status, improving task display accuracy. - Enhanced the logic to listen for relevant WebSocket events and update task statuses accordingly, ensuring synchronization with the agent output modal.
This commit is contained in:
@@ -10,6 +10,7 @@ import {
|
||||
DEFAULT_MODEL,
|
||||
} from '@/lib/agent-context-parser';
|
||||
import { cn } from '@/lib/utils';
|
||||
import type { AutoModeEvent } from '@/types/electron';
|
||||
import {
|
||||
Brain,
|
||||
ListTodo,
|
||||
@@ -71,22 +72,66 @@ export function AgentInfoPanel({
|
||||
const [agentInfo, setAgentInfo] = useState<AgentTaskInfo | null>(null);
|
||||
const [isSummaryDialogOpen, setIsSummaryDialogOpen] = useState(false);
|
||||
const [isTodosExpanded, setIsTodosExpanded] = useState(false);
|
||||
// Track real-time task status updates from WebSocket events
|
||||
const [taskStatusMap, setTaskStatusMap] = useState<
|
||||
Map<string, 'pending' | 'in_progress' | 'completed'>
|
||||
>(new Map());
|
||||
// Fresh planSpec data fetched from API (store data is stale for task progress)
|
||||
const [freshPlanSpec, setFreshPlanSpec] = useState<{
|
||||
tasks?: ParsedTask[];
|
||||
tasksCompleted?: number;
|
||||
currentTaskId?: string;
|
||||
} | null>(null);
|
||||
|
||||
// Derive effective todos from planSpec.tasks when available, fallback to agentInfo.todos
|
||||
// This fixes the issue where Kanban cards show "0/0 tasks" when the agent uses planSpec
|
||||
// instead of emitting TodoWrite tool calls in the output log
|
||||
// Uses freshPlanSpec (from API) for accurate progress, with taskStatusMap for real-time updates
|
||||
const effectiveTodos = useMemo(() => {
|
||||
// Use freshPlanSpec if available (fetched from API), fallback to store's feature.planSpec
|
||||
const planSpec = freshPlanSpec?.tasks?.length ? freshPlanSpec : feature.planSpec;
|
||||
|
||||
// First priority: use planSpec.tasks if available (modern approach)
|
||||
if (feature.planSpec?.tasks && feature.planSpec.tasks.length > 0) {
|
||||
return feature.planSpec.tasks.map((task: ParsedTask) => ({
|
||||
content: task.description,
|
||||
// Map 'failed' status to 'pending' since todo display doesn't support 'failed'
|
||||
status: task.status === 'failed' ? 'pending' : task.status,
|
||||
}));
|
||||
if (planSpec?.tasks && planSpec.tasks.length > 0) {
|
||||
const completedCount = planSpec.tasksCompleted || 0;
|
||||
const currentTaskId = planSpec.currentTaskId;
|
||||
|
||||
return planSpec.tasks.map((task: ParsedTask, index: number) => {
|
||||
// Use real-time status from WebSocket events if available
|
||||
const realtimeStatus = taskStatusMap.get(task.id);
|
||||
|
||||
// Calculate status: WebSocket status > index-based status > task.status
|
||||
let effectiveStatus: 'pending' | 'in_progress' | 'completed';
|
||||
if (realtimeStatus) {
|
||||
effectiveStatus = realtimeStatus;
|
||||
} else if (index < completedCount) {
|
||||
effectiveStatus = 'completed';
|
||||
} else if (task.id === currentTaskId) {
|
||||
effectiveStatus = 'in_progress';
|
||||
} else {
|
||||
// Fallback to task.status if available, otherwise pending
|
||||
effectiveStatus =
|
||||
task.status === 'completed'
|
||||
? 'completed'
|
||||
: task.status === 'in_progress'
|
||||
? 'in_progress'
|
||||
: 'pending';
|
||||
}
|
||||
|
||||
return {
|
||||
content: task.description,
|
||||
status: effectiveStatus,
|
||||
};
|
||||
});
|
||||
}
|
||||
// Fallback: use parsed agentInfo.todos from agent-output.md
|
||||
return agentInfo?.todos || [];
|
||||
}, [feature.planSpec?.tasks, agentInfo?.todos]);
|
||||
}, [
|
||||
freshPlanSpec,
|
||||
feature.planSpec?.tasks,
|
||||
feature.planSpec?.tasksCompleted,
|
||||
feature.planSpec?.currentTaskId,
|
||||
agentInfo?.todos,
|
||||
taskStatusMap,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const loadContext = async () => {
|
||||
@@ -98,6 +143,7 @@ export function AgentInfoPanel({
|
||||
|
||||
if (feature.status === 'backlog') {
|
||||
setAgentInfo(null);
|
||||
setFreshPlanSpec(null);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -107,6 +153,21 @@ export function AgentInfoPanel({
|
||||
if (!currentProject?.path) return;
|
||||
|
||||
if (api.features) {
|
||||
// Fetch fresh feature data to get up-to-date planSpec (store data is stale)
|
||||
try {
|
||||
const featureResult = await api.features.get(currentProject.path, feature.id);
|
||||
const freshFeature: any = (featureResult as any).feature;
|
||||
if (featureResult.success && freshFeature?.planSpec) {
|
||||
setFreshPlanSpec({
|
||||
tasks: freshFeature.planSpec.tasks,
|
||||
tasksCompleted: freshFeature.planSpec.tasksCompleted || 0,
|
||||
currentTaskId: freshFeature.planSpec.currentTaskId,
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// Ignore errors fetching fresh planSpec
|
||||
}
|
||||
|
||||
const result = await api.features.getAgentOutput(currentProject.path, feature.id);
|
||||
|
||||
if (result.success && result.content) {
|
||||
@@ -129,13 +190,62 @@ export function AgentInfoPanel({
|
||||
|
||||
loadContext();
|
||||
|
||||
if (isCurrentAutoTask) {
|
||||
// Poll for updates when feature is in_progress (not just isCurrentAutoTask)
|
||||
// This ensures planSpec progress stays in sync
|
||||
if (isCurrentAutoTask || feature.status === 'in_progress') {
|
||||
const interval = setInterval(loadContext, 3000);
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
};
|
||||
}
|
||||
}, [feature.id, feature.status, contextContent, isCurrentAutoTask]);
|
||||
|
||||
// Listen to WebSocket events for real-time task status updates
|
||||
// This ensures the Kanban card shows the same progress as the Agent Output modal
|
||||
// Listen for ANY in-progress feature with planSpec tasks, not just isCurrentAutoTask
|
||||
const hasPlanSpecTasks =
|
||||
(freshPlanSpec?.tasks?.length ?? 0) > 0 || (feature.planSpec?.tasks?.length ?? 0) > 0;
|
||||
const shouldListenToEvents = feature.status === 'in_progress' && hasPlanSpecTasks;
|
||||
|
||||
useEffect(() => {
|
||||
if (!shouldListenToEvents) return;
|
||||
|
||||
const api = getElectronAPI();
|
||||
if (!api?.autoMode) return;
|
||||
|
||||
const unsubscribe = api.autoMode.onEvent((event: AutoModeEvent) => {
|
||||
// Only handle events for this feature
|
||||
if (!('featureId' in event) || event.featureId !== feature.id) return;
|
||||
|
||||
switch (event.type) {
|
||||
case 'auto_mode_task_started':
|
||||
if ('taskId' in event) {
|
||||
const taskEvent = event as Extract<AutoModeEvent, { type: 'auto_mode_task_started' }>;
|
||||
setTaskStatusMap((prev) => {
|
||||
const newMap = new Map(prev);
|
||||
// Mark current task as in_progress
|
||||
newMap.set(taskEvent.taskId, 'in_progress');
|
||||
return newMap;
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case 'auto_mode_task_complete':
|
||||
if ('taskId' in event) {
|
||||
const taskEvent = event as Extract<AutoModeEvent, { type: 'auto_mode_task_complete' }>;
|
||||
setTaskStatusMap((prev) => {
|
||||
const newMap = new Map(prev);
|
||||
newMap.set(taskEvent.taskId, 'completed');
|
||||
return newMap;
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
return unsubscribe;
|
||||
}, [feature.id, shouldListenToEvents]);
|
||||
|
||||
// Model/Preset Info for Backlog Cards
|
||||
if (feature.status === 'backlog') {
|
||||
const provider = getProviderFromModel(feature.model);
|
||||
@@ -174,7 +284,9 @@ export function AgentInfoPanel({
|
||||
}
|
||||
|
||||
// Agent Info Panel for non-backlog cards
|
||||
if (feature.status !== 'backlog' && agentInfo) {
|
||||
// Show panel if we have agentInfo OR planSpec.tasks (for spec/full mode)
|
||||
// Note: hasPlanSpecTasks is already defined above and includes freshPlanSpec
|
||||
if (feature.status !== 'backlog' && (agentInfo || hasPlanSpecTasks)) {
|
||||
return (
|
||||
<>
|
||||
<div className="mb-3 space-y-2 overflow-hidden">
|
||||
@@ -187,7 +299,7 @@ export function AgentInfoPanel({
|
||||
})()}
|
||||
<span className="font-medium">{formatModelName(feature.model ?? DEFAULT_MODEL)}</span>
|
||||
</div>
|
||||
{agentInfo.currentPhase && (
|
||||
{agentInfo?.currentPhase && (
|
||||
<div
|
||||
className={cn(
|
||||
'px-1.5 py-0.5 rounded-md text-[10px] font-medium',
|
||||
@@ -263,7 +375,7 @@ export function AgentInfoPanel({
|
||||
{/* Summary for waiting_approval and verified */}
|
||||
{(feature.status === 'waiting_approval' || feature.status === 'verified') && (
|
||||
<>
|
||||
{(feature.summary || summary || agentInfo.summary) && (
|
||||
{(feature.summary || summary || agentInfo?.summary) && (
|
||||
<div className="space-y-1.5 pt-2 border-t border-border/30 overflow-hidden">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-1 text-[10px] text-[var(--status-success)] min-w-0">
|
||||
@@ -289,18 +401,18 @@ export function AgentInfoPanel({
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
>
|
||||
{feature.summary || summary || agentInfo.summary}
|
||||
{feature.summary || summary || agentInfo?.summary}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{!feature.summary &&
|
||||
!summary &&
|
||||
!agentInfo.summary &&
|
||||
agentInfo.toolCallCount > 0 && (
|
||||
!agentInfo?.summary &&
|
||||
(agentInfo?.toolCallCount ?? 0) > 0 && (
|
||||
<div className="flex items-center gap-2 text-[10px] text-muted-foreground/60 pt-2 border-t border-border/30">
|
||||
<span className="flex items-center gap-1">
|
||||
<Wrench className="w-2.5 h-2.5" />
|
||||
{agentInfo.toolCallCount} tool calls
|
||||
{agentInfo?.toolCallCount ?? 0} tool calls
|
||||
</span>
|
||||
{effectiveTodos.length > 0 && (
|
||||
<span className="flex items-center gap-1">
|
||||
|
||||
Reference in New Issue
Block a user