mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-03-18 22:33:08 +00:00
feat: Add error handling to auto-mode facade and implement followUp feature. Fix Claude weekly usage indicator. Fix mobile card drag
This commit is contained in:
@@ -59,24 +59,19 @@ export function TaskProgressPanel({
|
||||
const planSpec = feature.planSpec;
|
||||
const planTasks = planSpec.tasks; // Already guarded by the if condition above
|
||||
const currentId = planSpec.currentTaskId;
|
||||
const completedCount = planSpec.tasksCompleted || 0;
|
||||
|
||||
// Convert planSpec tasks to TaskInfo with proper status
|
||||
// Convert planSpec tasks to TaskInfo using their persisted status
|
||||
// planTasks is guaranteed to be defined due to the if condition check
|
||||
const initialTasks: TaskInfo[] = (planTasks as ParsedTask[]).map(
|
||||
(t: ParsedTask, index: number) => ({
|
||||
id: t.id,
|
||||
description: t.description,
|
||||
filePath: t.filePath,
|
||||
phase: t.phase,
|
||||
status:
|
||||
index < completedCount
|
||||
? ('completed' as const)
|
||||
: t.id === currentId
|
||||
? ('in_progress' as const)
|
||||
: ('pending' as const),
|
||||
})
|
||||
);
|
||||
const initialTasks: TaskInfo[] = (planTasks as ParsedTask[]).map((t: ParsedTask) => ({
|
||||
id: t.id,
|
||||
description: t.description,
|
||||
filePath: t.filePath,
|
||||
phase: t.phase,
|
||||
status:
|
||||
t.id === currentId
|
||||
? ('in_progress' as const)
|
||||
: (t.status as TaskInfo['status']) || ('pending' as const),
|
||||
}));
|
||||
|
||||
setTasks(initialTasks);
|
||||
setCurrentTaskId(currentId || null);
|
||||
@@ -113,16 +108,12 @@ export function TaskProgressPanel({
|
||||
const existingIndex = prev.findIndex((t) => t.id === taskEvent.taskId);
|
||||
|
||||
if (existingIndex !== -1) {
|
||||
// Update status to in_progress and mark previous as completed
|
||||
return prev.map((t, idx) => {
|
||||
// Update only the started task to in_progress
|
||||
// Do NOT assume previous tasks are completed - rely on actual task_complete events
|
||||
return prev.map((t) => {
|
||||
if (t.id === taskEvent.taskId) {
|
||||
return { ...t, status: 'in_progress' as const };
|
||||
}
|
||||
// If we are moving to a task that is further down the list, assume previous ones are completed
|
||||
// This is a heuristic, but usually correct for sequential execution
|
||||
if (idx < existingIndex && t.status !== 'completed') {
|
||||
return { ...t, status: 'completed' as const };
|
||||
}
|
||||
return t;
|
||||
});
|
||||
}
|
||||
@@ -151,6 +142,24 @@ export function TaskProgressPanel({
|
||||
setCurrentTaskId(null);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'auto_mode_task_status':
|
||||
if ('taskId' in event && 'status' in event) {
|
||||
const taskEvent = event as Extract<AutoModeEvent, { type: 'auto_mode_task_status' }>;
|
||||
setTasks((prev) =>
|
||||
prev.map((t) =>
|
||||
t.id === taskEvent.taskId
|
||||
? { ...t, status: taskEvent.status as TaskInfo['status'] }
|
||||
: t
|
||||
)
|
||||
);
|
||||
if (taskEvent.status === 'in_progress') {
|
||||
setCurrentTaskId(taskEvent.taskId);
|
||||
} else if (taskEvent.status === 'completed') {
|
||||
setCurrentTaskId((current) => (current === taskEvent.taskId ? null : current));
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { cn } from '@/lib/utils';
|
||||
import { useSetupStore } from '@/store/setup-store';
|
||||
import { AnthropicIcon, OpenAIIcon } from '@/components/ui/provider-icon';
|
||||
import { useClaudeUsage, useCodexUsage } from '@/hooks/queries';
|
||||
import { getExpectedWeeklyPacePercentage, getPaceStatusLabel } from '@/store/utils/usage-utils';
|
||||
|
||||
// Error codes for distinguishing failure modes
|
||||
const ERROR_CODES = {
|
||||
@@ -146,13 +147,28 @@ export function UsagePopover() {
|
||||
return { color: 'text-green-500', icon: CheckCircle, bg: 'bg-green-500' };
|
||||
};
|
||||
|
||||
// Helper component for the progress bar
|
||||
const ProgressBar = ({ percentage, colorClass }: { percentage: number; colorClass: string }) => (
|
||||
<div className="h-2 w-full bg-secondary/50 rounded-full overflow-hidden">
|
||||
// Helper component for the progress bar with optional pace indicator
|
||||
const ProgressBar = ({
|
||||
percentage,
|
||||
colorClass,
|
||||
pacePercentage,
|
||||
}: {
|
||||
percentage: number;
|
||||
colorClass: string;
|
||||
pacePercentage?: number | null;
|
||||
}) => (
|
||||
<div className="relative h-2 w-full bg-secondary/50 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={cn('h-full transition-all duration-500', colorClass)}
|
||||
style={{ width: `${Math.min(percentage, 100)}%` }}
|
||||
/>
|
||||
{pacePercentage != null && pacePercentage > 0 && pacePercentage < 100 && (
|
||||
<div
|
||||
className="absolute top-0 h-full w-0.5 bg-foreground/60"
|
||||
style={{ left: `${pacePercentage}%` }}
|
||||
title={`Expected: ${Math.round(pacePercentage)}%`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -163,6 +179,7 @@ export function UsagePopover() {
|
||||
resetText,
|
||||
isPrimary = false,
|
||||
stale = false,
|
||||
pacePercentage,
|
||||
}: {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
@@ -170,6 +187,7 @@ export function UsagePopover() {
|
||||
resetText?: string;
|
||||
isPrimary?: boolean;
|
||||
stale?: boolean;
|
||||
pacePercentage?: number | null;
|
||||
}) => {
|
||||
const isValidPercentage =
|
||||
typeof percentage === 'number' && !isNaN(percentage) && isFinite(percentage);
|
||||
@@ -177,6 +195,10 @@ export function UsagePopover() {
|
||||
|
||||
const status = getStatusInfo(safePercentage);
|
||||
const StatusIcon = status.icon;
|
||||
const paceLabel =
|
||||
isValidPercentage && pacePercentage != null
|
||||
? getPaceStatusLabel(safePercentage, pacePercentage)
|
||||
: null;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -211,15 +233,28 @@ export function UsagePopover() {
|
||||
<ProgressBar
|
||||
percentage={safePercentage}
|
||||
colorClass={isValidPercentage ? status.bg : 'bg-muted-foreground/30'}
|
||||
pacePercentage={pacePercentage}
|
||||
/>
|
||||
{resetText && (
|
||||
<div className="mt-2 flex justify-end">
|
||||
<div className="mt-2 flex items-center justify-between">
|
||||
{paceLabel ? (
|
||||
<p
|
||||
className={cn(
|
||||
'text-[10px] font-medium',
|
||||
safePercentage > (pacePercentage ?? 0) ? 'text-orange-500' : 'text-green-500'
|
||||
)}
|
||||
>
|
||||
{paceLabel}
|
||||
</p>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
{resetText && (
|
||||
<p className="text-xs text-muted-foreground flex items-center gap-1">
|
||||
<Clock className="w-3 h-3" />
|
||||
{resetText}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -384,6 +419,7 @@ export function UsagePopover() {
|
||||
percentage={claudeUsage.sonnetWeeklyPercentage}
|
||||
resetText={claudeUsage.sonnetResetText}
|
||||
stale={isClaudeStale}
|
||||
pacePercentage={getExpectedWeeklyPacePercentage(claudeUsage.weeklyResetTime)}
|
||||
/>
|
||||
<UsageCard
|
||||
title="Weekly"
|
||||
@@ -391,6 +427,7 @@ export function UsagePopover() {
|
||||
percentage={claudeUsage.weeklyPercentage}
|
||||
resetText={claudeUsage.weeklyResetText}
|
||||
stale={isClaudeStale}
|
||||
pacePercentage={getExpectedWeeklyPacePercentage(claudeUsage.weeklyResetTime)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -153,6 +153,7 @@ export const AgentInfoPanel = memo(function AgentInfoPanel({
|
||||
|
||||
// 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 isFeatureFinished = feature.status === 'waiting_approval' || feature.status === 'verified';
|
||||
const effectiveTodos = useMemo(() => {
|
||||
// Use freshPlanSpec if available (fetched from API), fallback to store's feature.planSpec
|
||||
const planSpec = freshPlanSpec?.tasks?.length ? freshPlanSpec : feature.planSpec;
|
||||
@@ -163,6 +164,16 @@ export const AgentInfoPanel = memo(function AgentInfoPanel({
|
||||
const currentTaskId = planSpec.currentTaskId;
|
||||
|
||||
return planSpec.tasks.map((task: ParsedTask, index: number) => {
|
||||
// If the feature is done (waiting_approval/verified), all tasks are completed
|
||||
// This is a defensive UI-side check: the server should have already finalized
|
||||
// task statuses, but stale data from before the fix could still show spinners
|
||||
if (isFeatureFinished) {
|
||||
return {
|
||||
content: task.description,
|
||||
status: 'completed' as const,
|
||||
};
|
||||
}
|
||||
|
||||
// Use real-time status from WebSocket events if available
|
||||
const realtimeStatus = taskStatusMap.get(task.id);
|
||||
|
||||
@@ -199,6 +210,7 @@ export const AgentInfoPanel = memo(function AgentInfoPanel({
|
||||
feature.planSpec?.currentTaskId,
|
||||
agentInfo?.todos,
|
||||
taskStatusMap,
|
||||
isFeatureFinished,
|
||||
]);
|
||||
|
||||
// Listen to WebSocket events for real-time task status updates
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// @ts-nocheck - header component props with optional handlers and status variants
|
||||
import { memo, useState } from 'react';
|
||||
import type { DraggableAttributes, DraggableSyntheticListeners } from '@dnd-kit/core';
|
||||
import { Feature } from '@/store/app-store';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
@@ -35,6 +36,8 @@ interface CardHeaderProps {
|
||||
onDelete: () => void;
|
||||
onViewOutput?: () => void;
|
||||
onSpawnTask?: () => void;
|
||||
dragHandleListeners?: DraggableSyntheticListeners;
|
||||
dragHandleAttributes?: DraggableAttributes;
|
||||
}
|
||||
|
||||
export const CardHeaderSection = memo(function CardHeaderSection({
|
||||
@@ -46,6 +49,8 @@ export const CardHeaderSection = memo(function CardHeaderSection({
|
||||
onDelete,
|
||||
onViewOutput,
|
||||
onSpawnTask,
|
||||
dragHandleListeners,
|
||||
dragHandleAttributes,
|
||||
}: CardHeaderProps) {
|
||||
const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false);
|
||||
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
|
||||
@@ -319,8 +324,10 @@ export const CardHeaderSection = memo(function CardHeaderSection({
|
||||
<div className="flex items-start gap-2">
|
||||
{isDraggable && (
|
||||
<div
|
||||
className="-ml-2 -mt-1 p-2 touch-none opacity-40 hover:opacity-70 transition-opacity"
|
||||
className="-ml-2 -mt-1 p-2 touch-none cursor-grab active:cursor-grabbing opacity-40 hover:opacity-70 transition-opacity"
|
||||
data-testid={`drag-handle-${feature.id}`}
|
||||
{...dragHandleAttributes}
|
||||
{...dragHandleListeners}
|
||||
>
|
||||
<GripVertical className="w-3.5 h-3.5 text-muted-foreground" />
|
||||
</div>
|
||||
|
||||
@@ -32,7 +32,7 @@ function getCursorClass(
|
||||
): string {
|
||||
if (isSelectionMode) return 'cursor-pointer';
|
||||
if (isOverlay) return 'cursor-grabbing';
|
||||
if (isDraggable) return 'cursor-grab active:cursor-grabbing';
|
||||
// Drag cursor is now only on the drag handle, not the full card
|
||||
return 'cursor-default';
|
||||
}
|
||||
|
||||
@@ -172,7 +172,7 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
const isSelectable = isSelectionMode && feature.status === selectionTarget;
|
||||
|
||||
const wrapperClasses = cn(
|
||||
'relative select-none outline-none touch-none transition-transform duration-200 ease-out',
|
||||
'relative select-none outline-none transition-transform duration-200 ease-out',
|
||||
getCursorClass(isOverlay, isDraggable, isSelectable),
|
||||
isOverlay && isLifted && 'scale-105 rotate-1 z-50',
|
||||
// Visual feedback when another card is being dragged over this one
|
||||
@@ -254,6 +254,8 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
onDelete={onDelete}
|
||||
onViewOutput={onViewOutput}
|
||||
onSpawnTask={onSpawnTask}
|
||||
dragHandleListeners={isDraggable ? listeners : undefined}
|
||||
dragHandleAttributes={isDraggable ? attributes : undefined}
|
||||
/>
|
||||
|
||||
<CardContent className="px-3 pt-0 pb-0">
|
||||
@@ -296,8 +298,6 @@ export const KanbanCard = memo(function KanbanCard({
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={dndStyle}
|
||||
{...attributes}
|
||||
{...(isDraggable ? listeners : {})}
|
||||
className={wrapperClasses}
|
||||
data-testid={`kanban-card-${feature.id}`}
|
||||
>
|
||||
|
||||
@@ -5,6 +5,7 @@ import { Spinner } from '@/components/ui/spinner';
|
||||
import { getElectronAPI } from '@/lib/electron';
|
||||
import { useAppStore } from '@/store/app-store';
|
||||
import { AnthropicIcon, OpenAIIcon } from '@/components/ui/provider-icon';
|
||||
import { getExpectedWeeklyPacePercentage, getPaceStatusLabel } from '@/store/utils/usage-utils';
|
||||
|
||||
interface MobileUsageBarProps {
|
||||
showClaudeUsage: boolean;
|
||||
@@ -23,11 +24,15 @@ function UsageBar({
|
||||
label,
|
||||
percentage,
|
||||
isStale,
|
||||
pacePercentage,
|
||||
}: {
|
||||
label: string;
|
||||
percentage: number;
|
||||
isStale: boolean;
|
||||
pacePercentage?: number | null;
|
||||
}) {
|
||||
const paceLabel = pacePercentage != null ? getPaceStatusLabel(percentage, pacePercentage) : null;
|
||||
|
||||
return (
|
||||
<div className="mt-1.5 first:mt-0">
|
||||
<div className="flex items-center justify-between mb-0.5">
|
||||
@@ -49,7 +54,7 @@ function UsageBar({
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
'h-1 w-full bg-muted-foreground/10 rounded-full overflow-hidden transition-opacity',
|
||||
'relative h-1 w-full bg-muted-foreground/10 rounded-full overflow-hidden transition-opacity',
|
||||
isStale && 'opacity-60'
|
||||
)}
|
||||
>
|
||||
@@ -57,7 +62,24 @@ function UsageBar({
|
||||
className={cn('h-full transition-all duration-500', getProgressBarColor(percentage))}
|
||||
style={{ width: `${Math.min(percentage, 100)}%` }}
|
||||
/>
|
||||
{pacePercentage != null && pacePercentage > 0 && pacePercentage < 100 && (
|
||||
<div
|
||||
className="absolute top-0 h-full w-0.5 bg-foreground/60"
|
||||
style={{ left: `${pacePercentage}%` }}
|
||||
title={`Expected: ${Math.round(pacePercentage)}%`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{paceLabel && (
|
||||
<p
|
||||
className={cn(
|
||||
'text-[9px] mt-0.5',
|
||||
percentage > (pacePercentage ?? 0) ? 'text-orange-500' : 'text-green-500'
|
||||
)}
|
||||
>
|
||||
{paceLabel}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -190,6 +212,7 @@ export function MobileUsageBar({ showClaudeUsage, showCodexUsage }: MobileUsageB
|
||||
label="Weekly"
|
||||
percentage={claudeUsage.weeklyPercentage}
|
||||
isStale={isClaudeStale}
|
||||
pacePercentage={getExpectedWeeklyPacePercentage(claudeUsage.weeklyResetTime)}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
|
||||
Reference in New Issue
Block a user