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:
gsxdsm
2026-02-16 18:58:42 -08:00
parent 2805c0ea53
commit 416ef3a394
15 changed files with 552 additions and 76 deletions

View File

@@ -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

View File

@@ -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>

View File

@@ -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}`}
>

View File

@@ -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)}
/>
</>
) : (