mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-03 21:03:08 +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,
|
DEFAULT_MODEL,
|
||||||
} from '@/lib/agent-context-parser';
|
} from '@/lib/agent-context-parser';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
import type { AutoModeEvent } from '@/types/electron';
|
||||||
import {
|
import {
|
||||||
Brain,
|
Brain,
|
||||||
ListTodo,
|
ListTodo,
|
||||||
@@ -71,22 +72,66 @@ export function AgentInfoPanel({
|
|||||||
const [agentInfo, setAgentInfo] = useState<AgentTaskInfo | null>(null);
|
const [agentInfo, setAgentInfo] = useState<AgentTaskInfo | null>(null);
|
||||||
const [isSummaryDialogOpen, setIsSummaryDialogOpen] = useState(false);
|
const [isSummaryDialogOpen, setIsSummaryDialogOpen] = useState(false);
|
||||||
const [isTodosExpanded, setIsTodosExpanded] = 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
|
// 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
|
// Uses freshPlanSpec (from API) for accurate progress, with taskStatusMap for real-time updates
|
||||||
// instead of emitting TodoWrite tool calls in the output log
|
|
||||||
const effectiveTodos = useMemo(() => {
|
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)
|
// First priority: use planSpec.tasks if available (modern approach)
|
||||||
if (feature.planSpec?.tasks && feature.planSpec.tasks.length > 0) {
|
if (planSpec?.tasks && planSpec.tasks.length > 0) {
|
||||||
return feature.planSpec.tasks.map((task: ParsedTask) => ({
|
const completedCount = planSpec.tasksCompleted || 0;
|
||||||
content: task.description,
|
const currentTaskId = planSpec.currentTaskId;
|
||||||
// Map 'failed' status to 'pending' since todo display doesn't support 'failed'
|
|
||||||
status: task.status === 'failed' ? 'pending' : task.status,
|
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
|
// Fallback: use parsed agentInfo.todos from agent-output.md
|
||||||
return agentInfo?.todos || [];
|
return agentInfo?.todos || [];
|
||||||
}, [feature.planSpec?.tasks, agentInfo?.todos]);
|
}, [
|
||||||
|
freshPlanSpec,
|
||||||
|
feature.planSpec?.tasks,
|
||||||
|
feature.planSpec?.tasksCompleted,
|
||||||
|
feature.planSpec?.currentTaskId,
|
||||||
|
agentInfo?.todos,
|
||||||
|
taskStatusMap,
|
||||||
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadContext = async () => {
|
const loadContext = async () => {
|
||||||
@@ -98,6 +143,7 @@ export function AgentInfoPanel({
|
|||||||
|
|
||||||
if (feature.status === 'backlog') {
|
if (feature.status === 'backlog') {
|
||||||
setAgentInfo(null);
|
setAgentInfo(null);
|
||||||
|
setFreshPlanSpec(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,6 +153,21 @@ export function AgentInfoPanel({
|
|||||||
if (!currentProject?.path) return;
|
if (!currentProject?.path) return;
|
||||||
|
|
||||||
if (api.features) {
|
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);
|
const result = await api.features.getAgentOutput(currentProject.path, feature.id);
|
||||||
|
|
||||||
if (result.success && result.content) {
|
if (result.success && result.content) {
|
||||||
@@ -129,13 +190,62 @@ export function AgentInfoPanel({
|
|||||||
|
|
||||||
loadContext();
|
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);
|
const interval = setInterval(loadContext, 3000);
|
||||||
return () => {
|
return () => {
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}, [feature.id, feature.status, contextContent, isCurrentAutoTask]);
|
}, [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
|
// Model/Preset Info for Backlog Cards
|
||||||
if (feature.status === 'backlog') {
|
if (feature.status === 'backlog') {
|
||||||
const provider = getProviderFromModel(feature.model);
|
const provider = getProviderFromModel(feature.model);
|
||||||
@@ -174,7 +284,9 @@ export function AgentInfoPanel({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Agent Info Panel for non-backlog cards
|
// 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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="mb-3 space-y-2 overflow-hidden">
|
<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>
|
<span className="font-medium">{formatModelName(feature.model ?? DEFAULT_MODEL)}</span>
|
||||||
</div>
|
</div>
|
||||||
{agentInfo.currentPhase && (
|
{agentInfo?.currentPhase && (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'px-1.5 py-0.5 rounded-md text-[10px] font-medium',
|
'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 */}
|
{/* Summary for waiting_approval and verified */}
|
||||||
{(feature.status === 'waiting_approval' || feature.status === '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="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 justify-between gap-2">
|
||||||
<div className="flex items-center gap-1 text-[10px] text-[var(--status-success)] min-w-0">
|
<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()}
|
onPointerDown={(e) => e.stopPropagation()}
|
||||||
onMouseDown={(e) => e.stopPropagation()}
|
onMouseDown={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
{feature.summary || summary || agentInfo.summary}
|
{feature.summary || summary || agentInfo?.summary}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!feature.summary &&
|
{!feature.summary &&
|
||||||
!summary &&
|
!summary &&
|
||||||
!agentInfo.summary &&
|
!agentInfo?.summary &&
|
||||||
agentInfo.toolCallCount > 0 && (
|
(agentInfo?.toolCallCount ?? 0) > 0 && (
|
||||||
<div className="flex items-center gap-2 text-[10px] text-muted-foreground/60 pt-2 border-t border-border/30">
|
<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">
|
<span className="flex items-center gap-1">
|
||||||
<Wrench className="w-2.5 h-2.5" />
|
<Wrench className="w-2.5 h-2.5" />
|
||||||
{agentInfo.toolCallCount} tool calls
|
{agentInfo?.toolCallCount ?? 0} tool calls
|
||||||
</span>
|
</span>
|
||||||
{effectiveTodos.length > 0 && (
|
{effectiveTodos.length > 0 && (
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
|
|||||||
Reference in New Issue
Block a user