mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-01 08:13:37 +00:00
Merge pull request #493 from AutoMaker-Org/feature/v0.11.0rc-1768413909856-a0al
fix: agent output modal ui/ux and task list with spec/full plan mode
This commit is contained in:
@@ -230,7 +230,7 @@ export function TaskProgressPanel({
|
||||
)}
|
||||
>
|
||||
<div className="overflow-hidden">
|
||||
<div className="p-4 pt-2 relative max-h-[300px] overflow-y-auto scrollbar-visible">
|
||||
<div className="p-4 pt-2 relative max-h-[200px] overflow-y-auto scrollbar-visible">
|
||||
{/* Vertical Connector Line */}
|
||||
<div className="absolute left-[2.35rem] top-4 bottom-8 w-px bg-linear-to-b from-border/80 via-border/40 to-transparent" />
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// @ts-nocheck
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Feature, ThinkingLevel } from '@/store/app-store';
|
||||
import { useEffect, useState, useMemo } from 'react';
|
||||
import { Feature, ThinkingLevel, ParsedTask } from '@/store/app-store';
|
||||
import type { ReasoningEffort } from '@automaker/types';
|
||||
import { getProviderFromModel } from '@/lib/utils';
|
||||
import {
|
||||
@@ -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,6 +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
|
||||
// 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 (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 || [];
|
||||
}, [
|
||||
freshPlanSpec,
|
||||
feature.planSpec?.tasks,
|
||||
feature.planSpec?.tasksCompleted,
|
||||
feature.planSpec?.currentTaskId,
|
||||
agentInfo?.todos,
|
||||
taskStatusMap,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const loadContext = async () => {
|
||||
@@ -82,6 +143,7 @@ export function AgentInfoPanel({
|
||||
|
||||
if (feature.status === 'backlog') {
|
||||
setAgentInfo(null);
|
||||
setFreshPlanSpec(null);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -91,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) {
|
||||
@@ -113,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);
|
||||
@@ -158,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">
|
||||
@@ -171,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',
|
||||
@@ -189,13 +317,13 @@ export function AgentInfoPanel({
|
||||
</div>
|
||||
|
||||
{/* Task List Progress */}
|
||||
{agentInfo.todos.length > 0 && (
|
||||
{effectiveTodos.length > 0 && (
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-1 text-[10px] text-muted-foreground/70">
|
||||
<ListTodo className="w-3 h-3" />
|
||||
<span>
|
||||
{agentInfo.todos.filter((t) => t.status === 'completed').length}/
|
||||
{agentInfo.todos.length} tasks
|
||||
{effectiveTodos.filter((t) => t.status === 'completed').length}/
|
||||
{effectiveTodos.length} tasks
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
@@ -204,7 +332,7 @@ export function AgentInfoPanel({
|
||||
isTodosExpanded ? 'max-h-40' : 'max-h-16'
|
||||
)}
|
||||
>
|
||||
{(isTodosExpanded ? agentInfo.todos : agentInfo.todos.slice(0, 3)).map(
|
||||
{(isTodosExpanded ? effectiveTodos : effectiveTodos.slice(0, 3)).map(
|
||||
(todo, idx) => (
|
||||
<div key={idx} className="flex items-center gap-1.5 text-[10px]">
|
||||
{todo.status === 'completed' ? (
|
||||
@@ -227,7 +355,7 @@ export function AgentInfoPanel({
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
{agentInfo.todos.length > 3 && (
|
||||
{effectiveTodos.length > 3 && (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
@@ -237,7 +365,7 @@ export function AgentInfoPanel({
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
className="text-[10px] text-muted-foreground/60 pl-4 hover:text-muted-foreground transition-colors cursor-pointer"
|
||||
>
|
||||
{isTodosExpanded ? 'Show less' : `+${agentInfo.todos.length - 3} more`}
|
||||
{isTodosExpanded ? 'Show less' : `+${effectiveTodos.length - 3} more`}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -247,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">
|
||||
@@ -273,23 +401,23 @@ 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>
|
||||
{agentInfo.todos.length > 0 && (
|
||||
{effectiveTodos.length > 0 && (
|
||||
<span className="flex items-center gap-1">
|
||||
<CheckCircle2 className="w-2.5 h-2.5 text-[var(--status-success)]" />
|
||||
{agentInfo.todos.filter((t) => t.status === 'completed').length} tasks done
|
||||
{effectiveTodos.filter((t) => t.status === 'completed').length} tasks done
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -380,11 +380,11 @@ export function AgentOutputModal({
|
||||
<TaskProgressPanel
|
||||
featureId={featureId}
|
||||
projectPath={projectPath}
|
||||
className="flex-shrink-0 mx-1"
|
||||
className="flex-shrink-0 mx-3 my-2"
|
||||
/>
|
||||
|
||||
{effectiveViewMode === 'changes' ? (
|
||||
<div className="flex-1 min-h-0 sm:min-h-[400px] sm:max-h-[60vh] overflow-y-auto scrollbar-visible">
|
||||
<div className="flex-1 min-h-0 sm:min-h-[200px] sm:max-h-[60vh] overflow-y-auto scrollbar-visible">
|
||||
{projectPath ? (
|
||||
<GitDiffPanel
|
||||
projectPath={projectPath}
|
||||
@@ -401,7 +401,7 @@ export function AgentOutputModal({
|
||||
)}
|
||||
</div>
|
||||
) : effectiveViewMode === 'summary' && summary ? (
|
||||
<div className="flex-1 min-h-0 sm:min-h-[400px] sm:max-h-[60vh] overflow-y-auto bg-card border border-border/50 rounded-lg p-4 scrollbar-visible">
|
||||
<div className="flex-1 min-h-0 sm:min-h-[200px] sm:max-h-[60vh] overflow-y-auto bg-card border border-border/50 rounded-lg p-4 scrollbar-visible">
|
||||
<Markdown>{summary}</Markdown>
|
||||
</div>
|
||||
) : (
|
||||
@@ -409,7 +409,7 @@ export function AgentOutputModal({
|
||||
<div
|
||||
ref={scrollRef}
|
||||
onScroll={handleScroll}
|
||||
className="flex-1 min-h-0 sm:min-h-[400px] sm:max-h-[60vh] overflow-y-auto bg-zinc-950 rounded-lg p-4 font-mono text-xs scrollbar-visible"
|
||||
className="flex-1 min-h-0 sm:min-h-[200px] sm:max-h-[60vh] overflow-y-auto bg-zinc-950 rounded-lg p-4 font-mono text-xs scrollbar-visible"
|
||||
>
|
||||
{isLoading && !output ? (
|
||||
<div className="flex items-center justify-center h-full text-muted-foreground">
|
||||
|
||||
Reference in New Issue
Block a user