Claude Code Plan mode fix and better UI handling (#324)
* Claude Code Plan mode fix and better UI handling Fix plan detections. UI improvements: - Added Plans tab for plan tasks which show copyable plans higlighting the current. - Disable create task when no plan is detected and show a clear warning in the log view. * fix tests
This commit is contained in:
@@ -32,6 +32,8 @@ import {
|
||||
TaskRelatedTasksContext,
|
||||
TaskSelectedAttemptContext,
|
||||
} from './taskDetailsContext.ts';
|
||||
import { TaskPlanContext } from './TaskPlanContext.ts';
|
||||
import { is_planning_executor_type } from '@/lib/utils.ts';
|
||||
import type { AttemptData } from '@/lib/types.ts';
|
||||
|
||||
const TaskDetailsProvider: FC<{
|
||||
@@ -436,6 +438,56 @@ const TaskDetailsProvider: FC<{
|
||||
]
|
||||
);
|
||||
|
||||
// Plan context value
|
||||
const planValue = useMemo(() => {
|
||||
const isPlanningMode =
|
||||
attemptData.processes?.some(
|
||||
(process) =>
|
||||
process.executor_type &&
|
||||
is_planning_executor_type(process.executor_type)
|
||||
) ?? false;
|
||||
|
||||
const planCount =
|
||||
attemptData.allLogs?.reduce((count, processLog) => {
|
||||
const planEntries =
|
||||
processLog.normalized_conversation?.entries.filter(
|
||||
(entry) =>
|
||||
entry.entry_type.type === 'tool_use' &&
|
||||
entry.entry_type.action_type.action === 'plan_presentation'
|
||||
) ?? [];
|
||||
return count + planEntries.length;
|
||||
}, 0) ?? 0;
|
||||
|
||||
const hasPlans = planCount > 0;
|
||||
|
||||
const latestProcessHasNoPlan = (() => {
|
||||
if (!attemptData.allLogs || attemptData.allLogs.length === 0)
|
||||
return false;
|
||||
const latestProcessLog =
|
||||
attemptData.allLogs[attemptData.allLogs.length - 1];
|
||||
if (!latestProcessLog.normalized_conversation?.entries) return true;
|
||||
|
||||
return !latestProcessLog.normalized_conversation.entries.some(
|
||||
(entry) =>
|
||||
entry.entry_type.type === 'tool_use' &&
|
||||
entry.entry_type.action_type.action === 'plan_presentation'
|
||||
);
|
||||
})();
|
||||
|
||||
// Can create task if not in planning mode, or if in planning mode and has plans
|
||||
const canCreateTask =
|
||||
!isPlanningMode ||
|
||||
(isPlanningMode && hasPlans && !latestProcessHasNoPlan);
|
||||
|
||||
return {
|
||||
isPlanningMode,
|
||||
hasPlans,
|
||||
planCount,
|
||||
latestProcessHasNoPlan,
|
||||
canCreateTask,
|
||||
};
|
||||
}, [attemptData.processes, attemptData.allLogs]);
|
||||
|
||||
return (
|
||||
<TaskDetailsContext.Provider value={value}>
|
||||
<TaskAttemptLoadingContext.Provider value={taskAttemptLoadingValue}>
|
||||
@@ -453,7 +505,9 @@ const TaskDetailsProvider: FC<{
|
||||
<TaskRelatedTasksContext.Provider
|
||||
value={relatedTasksValue}
|
||||
>
|
||||
{children}
|
||||
<TaskPlanContext.Provider value={planValue}>
|
||||
{children}
|
||||
</TaskPlanContext.Provider>
|
||||
</TaskRelatedTasksContext.Provider>
|
||||
</TaskBackgroundRefreshContext.Provider>
|
||||
</TaskExecutionStateContext.Provider>
|
||||
|
||||
33
frontend/src/components/context/TaskPlanContext.ts
Normal file
33
frontend/src/components/context/TaskPlanContext.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { createContext, useContext } from 'react';
|
||||
|
||||
interface TaskPlanContextValue {
|
||||
isPlanningMode: boolean;
|
||||
hasPlans: boolean;
|
||||
planCount: number;
|
||||
latestProcessHasNoPlan: boolean;
|
||||
canCreateTask: boolean;
|
||||
}
|
||||
|
||||
export const TaskPlanContext = createContext<TaskPlanContextValue>({
|
||||
isPlanningMode: false,
|
||||
hasPlans: false,
|
||||
planCount: 0,
|
||||
latestProcessHasNoPlan: false,
|
||||
canCreateTask: true,
|
||||
});
|
||||
|
||||
export const useTaskPlan = () => {
|
||||
const context = useContext(TaskPlanContext);
|
||||
if (!context) {
|
||||
// Return defaults when used outside of TaskPlanProvider (e.g., on project-tasks page)
|
||||
// In this case, we assume not in planning mode, so task creation should be allowed
|
||||
return {
|
||||
isPlanningMode: false,
|
||||
hasPlans: false,
|
||||
planCount: 0,
|
||||
latestProcessHasNoPlan: false,
|
||||
canCreateTask: true,
|
||||
};
|
||||
}
|
||||
return context;
|
||||
};
|
||||
@@ -8,14 +8,17 @@ import {
|
||||
useState,
|
||||
} from 'react';
|
||||
import { TaskAttemptDataContext } from '@/components/context/taskDetailsContext.ts';
|
||||
import { useTaskPlan } from '@/components/context/TaskPlanContext.ts';
|
||||
import { Loader } from '@/components/ui/loader.tsx';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { AlertTriangle } from 'lucide-react';
|
||||
import Prompt from './Prompt';
|
||||
import ConversationEntry from './ConversationEntry';
|
||||
import { ConversationEntryDisplayType } from '@/lib/types';
|
||||
|
||||
function Conversation() {
|
||||
const { attemptData } = useContext(TaskAttemptDataContext);
|
||||
const { attemptData, isAttemptRunning } = useContext(TaskAttemptDataContext);
|
||||
const { isPlanningMode, latestProcessHasNoPlan } = useTaskPlan();
|
||||
const [shouldAutoScrollLogs, setShouldAutoScrollLogs] = useState(true);
|
||||
const [conversationUpdateTrigger, setConversationUpdateTrigger] = useState(0);
|
||||
const [visibleCount, setVisibleCount] = useState(100);
|
||||
@@ -249,6 +252,23 @@ function Conversation() {
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Warning banner for planning mode without plan */}
|
||||
{isPlanningMode && latestProcessHasNoPlan && !isAttemptRunning && (
|
||||
<div className="mt-4 p-4 rounded-lg border border-orange-200 dark:border-orange-800 bg-orange-50 dark:bg-orange-950/20">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<AlertTriangle className="h-5 w-5 text-orange-600 dark:text-orange-400" />
|
||||
<p className="text-lg font-semibold text-orange-800 dark:text-orange-300">
|
||||
No Plan Generated
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-orange-700 dark:text-orange-400">
|
||||
The last execution attempt did not produce a plan. Task creation is
|
||||
disabled until a plan is available. Try providing more specific
|
||||
instructions or check the conversation for any errors.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
258
frontend/src/components/tasks/TaskDetails/PlanTab.tsx
Normal file
258
frontend/src/components/tasks/TaskDetails/PlanTab.tsx
Normal file
@@ -0,0 +1,258 @@
|
||||
import { useContext, useMemo, useState } from 'react';
|
||||
import {
|
||||
FileText,
|
||||
Copy,
|
||||
AlertTriangle,
|
||||
CheckCircle,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
TaskAttemptDataContext,
|
||||
TaskAttemptLoadingContext,
|
||||
} from '@/components/context/taskDetailsContext.ts';
|
||||
import { useTaskPlan } from '@/components/context/TaskPlanContext.ts';
|
||||
import { Loader } from '@/components/ui/loader';
|
||||
import MarkdownRenderer from '@/components/ui/markdown-renderer.tsx';
|
||||
import { NormalizedEntry } from 'shared/types.ts';
|
||||
|
||||
interface PlanEntry {
|
||||
entry: NormalizedEntry;
|
||||
processId: string;
|
||||
processIndex: number;
|
||||
planIndex: number;
|
||||
isCurrent: boolean;
|
||||
}
|
||||
|
||||
function PlanTab() {
|
||||
const { loading } = useContext(TaskAttemptLoadingContext);
|
||||
const { attemptData } = useContext(TaskAttemptDataContext);
|
||||
const { isPlanningMode, hasPlans, latestProcessHasNoPlan } = useTaskPlan();
|
||||
const [copiedPlan, setCopiedPlan] = useState<string | null>(null);
|
||||
const [expandedPlans, setExpandedPlans] = useState<Set<string>>(new Set());
|
||||
|
||||
// Extract all plans from all processes
|
||||
const plans = useMemo(() => {
|
||||
if (!attemptData.allLogs) return [];
|
||||
|
||||
const planEntries: PlanEntry[] = [];
|
||||
let globalPlanIndex = 1;
|
||||
|
||||
attemptData.allLogs.forEach((processLog, processIndex) => {
|
||||
if (!processLog.normalized_conversation?.entries) return;
|
||||
|
||||
let localPlanIndex = 1;
|
||||
processLog.normalized_conversation.entries.forEach((entry) => {
|
||||
if (
|
||||
entry.entry_type.type === 'tool_use' &&
|
||||
entry.entry_type.action_type.action === 'plan_presentation'
|
||||
) {
|
||||
planEntries.push({
|
||||
entry,
|
||||
processId: processLog.id,
|
||||
processIndex,
|
||||
planIndex: localPlanIndex,
|
||||
isCurrent: globalPlanIndex === planEntries.length + 1, // Last plan is current
|
||||
});
|
||||
localPlanIndex++;
|
||||
globalPlanIndex++;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Mark the last plan as current
|
||||
if (planEntries.length > 0) {
|
||||
planEntries.forEach((plan) => {
|
||||
plan.isCurrent = false;
|
||||
});
|
||||
planEntries[planEntries.length - 1].isCurrent = true;
|
||||
}
|
||||
|
||||
return planEntries;
|
||||
}, [attemptData.allLogs]);
|
||||
|
||||
const handleCopyPlan = async (planContent: string, planId: string) => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(planContent);
|
||||
setCopiedPlan(planId);
|
||||
setTimeout(() => setCopiedPlan(null), 2000);
|
||||
} catch (error) {
|
||||
console.error('Failed to copy plan:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const togglePlanExpansion = (planId: string) => {
|
||||
setExpandedPlans((prev) => {
|
||||
const newSet = new Set(prev);
|
||||
if (newSet.has(planId)) {
|
||||
newSet.delete(planId);
|
||||
} else {
|
||||
newSet.add(planId);
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<Loader message="Loading..." size={32} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isPlanningMode) {
|
||||
return (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
<FileText className="h-12 w-12 mx-auto mb-4 opacity-50" />
|
||||
<p className="text-lg font-medium mb-2">Not in planning mode</p>
|
||||
<p className="text-sm">
|
||||
This tab is only available when using a planning executor
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!hasPlans && latestProcessHasNoPlan) {
|
||||
return (
|
||||
<div className="p-4">
|
||||
<div className="text-center py-8">
|
||||
<AlertTriangle className="h-12 w-12 mx-auto mb-4 text-orange-500" />
|
||||
<p className="text-lg font-medium mb-2 text-orange-800 dark:text-orange-300">
|
||||
No plan generated
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground mb-4">
|
||||
The last execution attempt did not produce a plan. Task creation is
|
||||
disabled until a plan is available.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!hasPlans) {
|
||||
return (
|
||||
<div className="text-center py-8 text-muted-foreground">
|
||||
<FileText className="h-12 w-12 mx-auto mb-4 opacity-50" />
|
||||
<p className="text-lg font-medium mb-2">No plans available</p>
|
||||
<p className="text-sm">
|
||||
Plans will appear here once they are generated
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-4 space-y-6 h-full flex flex-col">
|
||||
<div className="flex items-center justify-between flex-shrink-0">
|
||||
<h3 className="text-lg font-semibold">Plans ({plans.length})</h3>
|
||||
{latestProcessHasNoPlan && (
|
||||
<div className="flex items-center gap-2 text-orange-600 dark:text-orange-400 text-sm">
|
||||
<AlertTriangle className="h-4 w-4" />
|
||||
Last attempt produced no plan
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto space-y-4 min-h-0">
|
||||
{plans.map((planEntry, index) => {
|
||||
const planId = `${planEntry.processId}-${planEntry.planIndex}`;
|
||||
const planContent =
|
||||
planEntry.entry.entry_type.type === 'tool_use' &&
|
||||
planEntry.entry.entry_type.action_type.action ===
|
||||
'plan_presentation'
|
||||
? planEntry.entry.entry_type.action_type.plan
|
||||
: planEntry.entry.content;
|
||||
const isExpanded = expandedPlans.has(planId);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={planId}
|
||||
className={`border rounded-lg transition-all ${
|
||||
planEntry.isCurrent
|
||||
? 'border-blue-400 bg-blue-50/50 dark:bg-blue-950/20 shadow-md'
|
||||
: 'border-gray-200 dark:border-gray-700 bg-gray-50/50 dark:bg-gray-950/20 opacity-75'
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className="flex items-center justify-between p-4 cursor-pointer hover:bg-gray-50/50 dark:hover:bg-gray-800/30 transition-colors"
|
||||
onClick={() => togglePlanExpansion(planId)}
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
className="flex items-center justify-center w-5 h-5 hover:bg-gray-200 dark:hover:bg-gray-600 rounded transition-colors"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
togglePlanExpansion(planId);
|
||||
}}
|
||||
>
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="h-3 w-3" />
|
||||
) : (
|
||||
<ChevronRight className="h-3 w-3" />
|
||||
)}
|
||||
</button>
|
||||
<span
|
||||
className={`px-2 py-1 rounded text-xs font-medium ${
|
||||
planEntry.isCurrent
|
||||
? 'bg-blue-100 text-blue-800 dark:bg-blue-900/50 dark:text-blue-200'
|
||||
: 'bg-gray-100 text-gray-600 dark:bg-gray-800 dark:text-gray-400'
|
||||
}`}
|
||||
>
|
||||
Plan {index + 1}
|
||||
</span>
|
||||
{planEntry.isCurrent && (
|
||||
<div className="flex items-center gap-1 text-green-600 dark:text-green-400">
|
||||
<CheckCircle className="h-4 w-4" />
|
||||
<span className="text-xs font-medium">Current</span>
|
||||
</div>
|
||||
)}
|
||||
{planEntry.entry.timestamp && (
|
||||
<span className="text-xs text-gray-500">
|
||||
{new Date(planEntry.entry.timestamp).toLocaleString()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleCopyPlan(planContent, planId);
|
||||
}}
|
||||
className="flex items-center gap-1 px-2 py-1 text-xs text-gray-600 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800 rounded transition-colors"
|
||||
title="Copy plan as markdown"
|
||||
>
|
||||
<Copy className="h-3 w-3" />
|
||||
{copiedPlan === planId ? 'Copied!' : 'Copy'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{isExpanded && (
|
||||
<div
|
||||
className={`px-4 pb-4 border-t ${planEntry.isCurrent ? 'border-blue-200' : 'border-gray-200 dark:border-gray-700'}`}
|
||||
>
|
||||
<div
|
||||
className={`mt-3 ${planEntry.isCurrent ? '' : 'opacity-80'}`}
|
||||
>
|
||||
<MarkdownRenderer
|
||||
content={planContent}
|
||||
className="whitespace-pre-wrap break-words"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{plans.length > 1 && (
|
||||
<div className="text-xs text-gray-500 text-center pt-4 border-t flex-shrink-0">
|
||||
Previous plans are shown with reduced emphasis. Click to
|
||||
expand/collapse plans.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default PlanTab;
|
||||
@@ -1,20 +1,30 @@
|
||||
import { GitCompare, MessageSquare, Network, Cog } from 'lucide-react';
|
||||
import {
|
||||
GitCompare,
|
||||
MessageSquare,
|
||||
Network,
|
||||
Cog,
|
||||
FileText,
|
||||
} from 'lucide-react';
|
||||
import { useContext } from 'react';
|
||||
import {
|
||||
TaskAttemptDataContext,
|
||||
TaskDiffContext,
|
||||
TaskRelatedTasksContext,
|
||||
} from '@/components/context/taskDetailsContext.ts';
|
||||
import { useTaskPlan } from '@/components/context/TaskPlanContext.ts';
|
||||
|
||||
type Props = {
|
||||
activeTab: 'logs' | 'diffs' | 'related' | 'processes';
|
||||
setActiveTab: (tab: 'logs' | 'diffs' | 'related' | 'processes') => void;
|
||||
activeTab: 'logs' | 'diffs' | 'related' | 'processes' | 'plan';
|
||||
setActiveTab: (
|
||||
tab: 'logs' | 'diffs' | 'related' | 'processes' | 'plan'
|
||||
) => void;
|
||||
};
|
||||
|
||||
function TabNavigation({ activeTab, setActiveTab }: Props) {
|
||||
const { diff } = useContext(TaskDiffContext);
|
||||
const { totalRelatedCount } = useContext(TaskRelatedTasksContext);
|
||||
const { attemptData } = useContext(TaskAttemptDataContext);
|
||||
const { isPlanningMode, planCount } = useTaskPlan();
|
||||
return (
|
||||
<div className="border-b bg-muted/30">
|
||||
<div className="flex px-4">
|
||||
@@ -31,6 +41,24 @@ function TabNavigation({ activeTab, setActiveTab }: Props) {
|
||||
<MessageSquare className="h-4 w-4 mr-2" />
|
||||
Logs
|
||||
</button>
|
||||
{isPlanningMode && (
|
||||
<button
|
||||
onClick={() => {
|
||||
setActiveTab('plan');
|
||||
}}
|
||||
className={`flex items-center px-4 py-2 text-sm font-medium border-b-2 transition-colors ${
|
||||
activeTab === 'plan'
|
||||
? 'border-primary text-primary bg-background'
|
||||
: 'border-transparent text-muted-foreground hover:text-foreground hover:bg-muted/50'
|
||||
}`}
|
||||
>
|
||||
<FileText className="h-4 w-4 mr-2" />
|
||||
Plans
|
||||
<span className="ml-2 px-1.5 py-0.5 text-xs bg-primary/10 text-primary rounded-full">
|
||||
{planCount}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => {
|
||||
setActiveTab('diffs');
|
||||
|
||||
@@ -11,6 +11,7 @@ import DiffTab from '@/components/tasks/TaskDetails/DiffTab.tsx';
|
||||
import LogsTab from '@/components/tasks/TaskDetails/LogsTab.tsx';
|
||||
import RelatedTasksTab from '@/components/tasks/TaskDetails/RelatedTasksTab.tsx';
|
||||
import ProcessesTab from '@/components/tasks/TaskDetails/ProcessesTab.tsx';
|
||||
import PlanTab from '@/components/tasks/TaskDetails/PlanTab.tsx';
|
||||
import DeleteFileConfirmationDialog from '@/components/tasks/DeleteFileConfirmationDialog.tsx';
|
||||
import TabNavigation from '@/components/tasks/TaskDetails/TabNavigation.tsx';
|
||||
import CollapsibleToolbar from '@/components/tasks/TaskDetails/CollapsibleToolbar.tsx';
|
||||
@@ -39,7 +40,7 @@ export function TaskDetailsPanel({
|
||||
|
||||
// Tab and collapsible state
|
||||
const [activeTab, setActiveTab] = useState<
|
||||
'logs' | 'diffs' | 'related' | 'processes'
|
||||
'logs' | 'diffs' | 'related' | 'processes' | 'plan'
|
||||
>('logs');
|
||||
|
||||
// Reset to logs tab when task changes
|
||||
@@ -104,6 +105,8 @@ export function TaskDetailsPanel({
|
||||
<RelatedTasksTab />
|
||||
) : activeTab === 'processes' ? (
|
||||
<ProcessesTab />
|
||||
) : activeTab === 'plan' ? (
|
||||
<PlanTab />
|
||||
) : (
|
||||
<LogsTab />
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { Globe2 } from 'lucide-react';
|
||||
import { Globe2, AlertTriangle } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
Dialog,
|
||||
@@ -48,6 +48,12 @@ interface TaskFormDialogProps {
|
||||
description: string,
|
||||
status: TaskStatus
|
||||
) => Promise<void>;
|
||||
// Plan context for disabling task creation when no plan exists
|
||||
planContext?: {
|
||||
isPlanningMode: boolean;
|
||||
canCreateTask: boolean;
|
||||
latestProcessHasNoPlan: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export function TaskFormDialog({
|
||||
@@ -59,6 +65,7 @@ export function TaskFormDialog({
|
||||
onCreateTask,
|
||||
onCreateAndStartTask,
|
||||
onUpdateTask,
|
||||
planContext,
|
||||
}: TaskFormDialogProps) {
|
||||
const [title, setTitle] = useState('');
|
||||
const [description, setDescription] = useState('');
|
||||
@@ -71,6 +78,12 @@ export function TaskFormDialog({
|
||||
const { config } = useConfig();
|
||||
const isEditMode = Boolean(task);
|
||||
|
||||
// Check if task creation should be disabled based on plan context
|
||||
const isPlanningModeWithoutPlan =
|
||||
planContext?.isPlanningMode && !planContext?.canCreateTask;
|
||||
const showPlanWarning =
|
||||
planContext?.isPlanningMode && planContext?.latestProcessHasNoPlan;
|
||||
|
||||
useEffect(() => {
|
||||
if (task) {
|
||||
// Edit mode - populate with existing task data
|
||||
@@ -248,6 +261,23 @@ export function TaskFormDialog({
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
{/* Plan warning when in planning mode without plan */}
|
||||
{showPlanWarning && (
|
||||
<div className="p-4 rounded-lg border border-orange-200 dark:border-orange-800 bg-orange-50 dark:bg-orange-950/20">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<AlertTriangle className="h-4 w-4 text-orange-600 dark:text-orange-400" />
|
||||
<p className="text-sm font-semibold text-orange-800 dark:text-orange-300">
|
||||
Plan Required
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-sm text-orange-700 dark:text-orange-400">
|
||||
No plan was generated in the last execution attempt. Task
|
||||
creation is disabled until a plan is available. Please generate
|
||||
a plan first.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<Label htmlFor="task-title" className="text-sm font-medium">
|
||||
Title
|
||||
@@ -372,19 +402,46 @@ export function TaskFormDialog({
|
||||
variant="secondary"
|
||||
onClick={handleSubmit}
|
||||
disabled={
|
||||
isSubmitting || isSubmittingAndStart || !title.trim()
|
||||
isSubmitting ||
|
||||
isSubmittingAndStart ||
|
||||
!title.trim() ||
|
||||
isPlanningModeWithoutPlan
|
||||
}
|
||||
className={
|
||||
isPlanningModeWithoutPlan
|
||||
? 'opacity-60 cursor-not-allowed'
|
||||
: ''
|
||||
}
|
||||
title={
|
||||
isPlanningModeWithoutPlan
|
||||
? 'Plan required before creating task'
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{isPlanningModeWithoutPlan && (
|
||||
<AlertTriangle className="h-4 w-4 mr-2" />
|
||||
)}
|
||||
{isSubmitting ? 'Creating...' : 'Create Task'}
|
||||
</Button>
|
||||
{onCreateAndStartTask && (
|
||||
<Button
|
||||
onClick={handleCreateAndStart}
|
||||
disabled={
|
||||
isSubmitting || isSubmittingAndStart || !title.trim()
|
||||
isSubmitting ||
|
||||
isSubmittingAndStart ||
|
||||
!title.trim() ||
|
||||
isPlanningModeWithoutPlan
|
||||
}
|
||||
className={`font-medium ${isPlanningModeWithoutPlan ? 'opacity-60 cursor-not-allowed bg-red-600 hover:bg-red-600' : ''}`}
|
||||
title={
|
||||
isPlanningModeWithoutPlan
|
||||
? 'Plan required before creating and starting task'
|
||||
: undefined
|
||||
}
|
||||
className="font-medium"
|
||||
>
|
||||
{isPlanningModeWithoutPlan && (
|
||||
<AlertTriangle className="h-4 w-4 mr-2" />
|
||||
)}
|
||||
{isSubmittingAndStart
|
||||
? 'Creating & Starting...'
|
||||
: 'Create & Start'}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Dispatch, SetStateAction, useCallback, useContext } from 'react';
|
||||
import { Button } from '@/components/ui/button.tsx';
|
||||
import { ArrowDown, Play, Settings2, X } from 'lucide-react';
|
||||
import { ArrowDown, Play, Settings2, X, AlertTriangle } from 'lucide-react';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
TaskAttemptDataContext,
|
||||
TaskDetailsContext,
|
||||
} from '@/components/context/taskDetailsContext.ts';
|
||||
import { useTaskPlan } from '@/components/context/TaskPlanContext.ts';
|
||||
import { useConfig } from '@/components/config-provider.tsx';
|
||||
import BranchSelector from '@/components/tasks/BranchSelector.tsx';
|
||||
import { useKeyboardShortcuts } from '@/lib/keyboard-shortcuts.ts';
|
||||
@@ -58,6 +59,7 @@ function CreateAttempt({
|
||||
}: Props) {
|
||||
const { task, projectId } = useContext(TaskDetailsContext);
|
||||
const { isAttemptRunning } = useContext(TaskAttemptDataContext);
|
||||
const { isPlanningMode, canCreateTask } = useTaskPlan();
|
||||
const { config } = useConfig();
|
||||
|
||||
const [showCreateAttemptConfirmation, setShowCreateAttemptConfirmation] =
|
||||
@@ -155,6 +157,22 @@ function CreateAttempt({
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Plan warning when in planning mode without plan */}
|
||||
{isPlanningMode && !canCreateTask && (
|
||||
<div className="p-3 rounded-lg border border-orange-200 dark:border-orange-800 bg-orange-50 dark:bg-orange-950/20">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<AlertTriangle className="h-4 w-4 text-orange-600 dark:text-orange-400" />
|
||||
<p className="text-sm font-semibold text-orange-800 dark:text-orange-300">
|
||||
Plan Required
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-xs text-orange-700 dark:text-orange-400">
|
||||
Cannot start attempt - no plan was generated in the last
|
||||
execution. Please generate a plan first.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-3 gap-3 items-end">
|
||||
{/* Step 1: Choose Base Branch */}
|
||||
<div className="space-y-1">
|
||||
@@ -217,11 +235,29 @@ function CreateAttempt({
|
||||
<div className="space-y-1">
|
||||
<Button
|
||||
onClick={handleCreateAttempt}
|
||||
disabled={!createAttemptExecutor || isAttemptRunning}
|
||||
disabled={
|
||||
!createAttemptExecutor ||
|
||||
isAttemptRunning ||
|
||||
(isPlanningMode && !canCreateTask)
|
||||
}
|
||||
size="sm"
|
||||
className="w-full text-xs gap-2"
|
||||
className={`w-full text-xs gap-2 ${
|
||||
isPlanningMode && !canCreateTask
|
||||
? 'opacity-60 cursor-not-allowed bg-red-600 hover:bg-red-600'
|
||||
: ''
|
||||
}`}
|
||||
title={
|
||||
isPlanningMode && !canCreateTask
|
||||
? 'Plan required before starting attempt'
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<Play className="h-3 w-3 mr-1.5" />
|
||||
{isPlanningMode && !canCreateTask && (
|
||||
<AlertTriangle className="h-3 w-3 mr-1.5" />
|
||||
)}
|
||||
{!(isPlanningMode && !canCreateTask) && (
|
||||
<Play className="h-3 w-3 mr-1.5" />
|
||||
)}
|
||||
Start
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -63,6 +63,7 @@ import {
|
||||
TaskRelatedTasksContext,
|
||||
TaskSelectedAttemptContext,
|
||||
} from '@/components/context/taskDetailsContext.ts';
|
||||
import { useTaskPlan } from '@/components/context/TaskPlanContext.ts';
|
||||
import { useConfig } from '@/components/config-provider.tsx';
|
||||
import { useKeyboardShortcuts } from '@/lib/keyboard-shortcuts.ts';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
@@ -126,6 +127,7 @@ function CurrentAttempt({
|
||||
const { executionState, fetchExecutionState } = useContext(
|
||||
TaskExecutionStateContext
|
||||
);
|
||||
const { isPlanningMode, canCreateTask } = useTaskPlan();
|
||||
|
||||
const [isStartingDevServer, setIsStartingDevServer] = useState(false);
|
||||
const [merging, setMerging] = useState(false);
|
||||
@@ -741,7 +743,8 @@ function CurrentAttempt({
|
||||
disabled={
|
||||
isAttemptRunning ||
|
||||
executionState?.execution_state === 'CodingAgentFailed' ||
|
||||
executionState?.execution_state === 'SetupFailed'
|
||||
executionState?.execution_state === 'SetupFailed' ||
|
||||
(isPlanningMode && !canCreateTask)
|
||||
}
|
||||
size="sm"
|
||||
className="bg-green-600 hover:bg-green-700 disabled:bg-gray-400 gap-1"
|
||||
|
||||
@@ -10,6 +10,7 @@ import { TaskFormDialog } from '@/components/tasks/TaskFormDialog';
|
||||
import { ProjectForm } from '@/components/projects/project-form';
|
||||
import { TaskTemplateManager } from '@/components/TaskTemplateManager';
|
||||
import { useKeyboardShortcuts } from '@/lib/keyboard-shortcuts';
|
||||
import { useTaskPlan } from '@/components/context/TaskPlanContext';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -70,6 +71,10 @@ export function ProjectTasks() {
|
||||
const [selectedTask, setSelectedTask] = useState<Task | null>(null);
|
||||
const [isPanelOpen, setIsPanelOpen] = useState(false);
|
||||
|
||||
// Plan context for task creation
|
||||
const { isPlanningMode, canCreateTask, latestProcessHasNoPlan } =
|
||||
useTaskPlan();
|
||||
|
||||
// Define task creation handler
|
||||
const handleCreateNewTask = useCallback(() => {
|
||||
setEditingTask(null);
|
||||
@@ -540,6 +545,11 @@ export function ProjectTasks() {
|
||||
onCreateAndStartTask={handleCreateAndStartTask}
|
||||
onUpdateTask={handleUpdateTask}
|
||||
initialTemplate={selectedTemplate}
|
||||
planContext={{
|
||||
isPlanningMode,
|
||||
canCreateTask,
|
||||
latestProcessHasNoPlan,
|
||||
}}
|
||||
/>
|
||||
|
||||
<ProjectForm
|
||||
|
||||
Reference in New Issue
Block a user