Task 104: Implement 'scope-up' and 'scope-down' CLI Commands for Dynamic Task Complexity Adjustment (#1069)
* feat(task-104): Complete task 104 - Implement scope-up and scope-down CLI Commands - Added new CLI commands 'scope-up' and 'scope-down' with comma-separated ID support - Implemented strength levels (light/regular/heavy) and custom prompt functionality - Created core complexity adjustment logic with AI integration - Added MCP tool equivalents for integrated environments - Comprehensive error handling and task validation - Full test coverage with TDD approach - Updated task manager core and UI components Task 104: Implement 'scope-up' and 'scope-down' CLI Commands for Dynamic Task Complexity Adjustment - Complete implementation with CLI, MCP integration, and testing * chore: Add changeset for scope-up and scope-down features - Comprehensive user-facing description with usage examples - Key features and benefits explanation - CLI and MCP integration details - Real-world use cases for agile workflows * feat(extension): Add scope-up and scope-down to VS Code extension task details - Added useScopeUpTask and useScopeDownTask hooks in useTaskQueries.ts - Enhanced AIActionsSection with Task Complexity Adjustment section - Added strength selection (light/regular/heavy) and custom prompt support - Integrated scope buttons with proper loading states and error handling - Uses existing mcpRequest handler for scope_up_task and scope_down_task tools - Maintains consistent UI patterns with existing AI actions Extension now supports dynamic task complexity adjustment directly from task details view.
This commit is contained in:
@@ -4,10 +4,12 @@ import { Button } from '@/components/ui/button';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { CollapsibleSection } from '@/components/ui/CollapsibleSection';
|
||||
import { Wand2, Loader2, PlusCircle } from 'lucide-react';
|
||||
import { Wand2, Loader2, PlusCircle, TrendingUp, TrendingDown } from 'lucide-react';
|
||||
import {
|
||||
useUpdateTask,
|
||||
useUpdateSubtask
|
||||
useUpdateSubtask,
|
||||
useScopeUpTask,
|
||||
useScopeDownTask
|
||||
} from '../../webview/hooks/useTaskQueries';
|
||||
import type { TaskMasterTask } from '../../webview/types';
|
||||
|
||||
@@ -31,11 +33,15 @@ export const AIActionsSection: React.FC<AIActionsSectionProps> = ({
|
||||
onAppendingChange
|
||||
}) => {
|
||||
const [prompt, setPrompt] = useState('');
|
||||
const [lastAction, setLastAction] = useState<'regenerate' | 'append' | null>(
|
||||
const [scopePrompt, setScopePrompt] = useState('');
|
||||
const [scopeStrength, setScopeStrength] = useState<'light' | 'regular' | 'heavy'>('regular');
|
||||
const [lastAction, setLastAction] = useState<'regenerate' | 'append' | 'scope-up' | 'scope-down' | null>(
|
||||
null
|
||||
);
|
||||
const updateTask = useUpdateTask();
|
||||
const updateSubtask = useUpdateSubtask();
|
||||
const scopeUpTask = useScopeUpTask();
|
||||
const scopeDownTask = useScopeDownTask();
|
||||
|
||||
const handleRegenerate = async () => {
|
||||
if (!currentTask || !prompt.trim()) {
|
||||
@@ -103,10 +109,64 @@ export const AIActionsSection: React.FC<AIActionsSectionProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleScopeUp = async () => {
|
||||
if (!currentTask) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLastAction('scope-up');
|
||||
|
||||
try {
|
||||
const taskId = isSubtask && parentTask ? `${parentTask.id}.${currentTask.id}` : currentTask.id;
|
||||
|
||||
await scopeUpTask.mutateAsync({
|
||||
taskId,
|
||||
strength: scopeStrength,
|
||||
prompt: scopePrompt.trim() || undefined,
|
||||
options: { research: false }
|
||||
});
|
||||
|
||||
setScopePrompt('');
|
||||
refreshComplexityAfterAI();
|
||||
} catch (error) {
|
||||
console.error('❌ AIActionsSection: Failed to scope up task:', error);
|
||||
} finally {
|
||||
setLastAction(null);
|
||||
}
|
||||
};
|
||||
|
||||
const handleScopeDown = async () => {
|
||||
if (!currentTask) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLastAction('scope-down');
|
||||
|
||||
try {
|
||||
const taskId = isSubtask && parentTask ? `${parentTask.id}.${currentTask.id}` : currentTask.id;
|
||||
|
||||
await scopeDownTask.mutateAsync({
|
||||
taskId,
|
||||
strength: scopeStrength,
|
||||
prompt: scopePrompt.trim() || undefined,
|
||||
options: { research: false }
|
||||
});
|
||||
|
||||
setScopePrompt('');
|
||||
refreshComplexityAfterAI();
|
||||
} catch (error) {
|
||||
console.error('❌ AIActionsSection: Failed to scope down task:', error);
|
||||
} finally {
|
||||
setLastAction(null);
|
||||
}
|
||||
};
|
||||
|
||||
// Track loading states based on the last action
|
||||
const isLoading = updateTask.isPending || updateSubtask.isPending;
|
||||
const isLoading = updateTask.isPending || updateSubtask.isPending || scopeUpTask.isPending || scopeDownTask.isPending;
|
||||
const isRegenerating = isLoading && lastAction === 'regenerate';
|
||||
const isAppending = isLoading && lastAction === 'append';
|
||||
const isScopingUp = isLoading && lastAction === 'scope-up';
|
||||
const isScopingDown = isLoading && lastAction === 'scope-down';
|
||||
|
||||
return (
|
||||
<CollapsibleSection
|
||||
@@ -115,73 +175,160 @@ export const AIActionsSection: React.FC<AIActionsSectionProps> = ({
|
||||
defaultExpanded={true}
|
||||
buttonClassName="text-vscode-foreground/80 hover:text-vscode-foreground"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label
|
||||
htmlFor="ai-prompt"
|
||||
className="block text-sm font-medium text-vscode-foreground/80 mb-2"
|
||||
>
|
||||
Enter your prompt
|
||||
</Label>
|
||||
<Textarea
|
||||
id="ai-prompt"
|
||||
placeholder={
|
||||
isSubtask
|
||||
? 'Describe implementation notes, progress updates, or findings to add to this subtask...'
|
||||
: 'Describe what you want to change or add to this task...'
|
||||
}
|
||||
value={prompt}
|
||||
onChange={(e) => setPrompt(e.target.value)}
|
||||
className="min-h-[100px] bg-vscode-input-background border-vscode-input-border text-vscode-input-foreground placeholder-vscode-input-foreground/50 focus:border-vscode-focusBorder focus:ring-vscode-focusBorder"
|
||||
disabled={isRegenerating || isAppending}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
{!isSubtask && (
|
||||
<Button
|
||||
onClick={handleRegenerate}
|
||||
disabled={!prompt.trim() || isRegenerating || isAppending}
|
||||
className="bg-primary text-primary-foreground hover:bg-primary/90"
|
||||
<div className="space-y-6">
|
||||
{/* Standard AI Actions Section */}
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label
|
||||
htmlFor="ai-prompt"
|
||||
className="block text-sm font-medium text-vscode-foreground/80 mb-2"
|
||||
>
|
||||
{isRegenerating ? (
|
||||
Enter your prompt
|
||||
</Label>
|
||||
<Textarea
|
||||
id="ai-prompt"
|
||||
placeholder={
|
||||
isSubtask
|
||||
? 'Describe implementation notes, progress updates, or findings to add to this subtask...'
|
||||
: 'Describe what you want to change or add to this task...'
|
||||
}
|
||||
value={prompt}
|
||||
onChange={(e) => setPrompt(e.target.value)}
|
||||
className="min-h-[100px] bg-vscode-input-background border-vscode-input-border text-vscode-input-foreground placeholder-vscode-input-foreground/50 focus:border-vscode-focusBorder focus:ring-vscode-focusBorder"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
{!isSubtask && (
|
||||
<Button
|
||||
onClick={handleRegenerate}
|
||||
disabled={!prompt.trim() || isLoading}
|
||||
className="bg-primary text-primary-foreground hover:bg-primary/90"
|
||||
>
|
||||
{isRegenerating ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
Regenerating...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Wand2 className="w-4 h-4 mr-2" />
|
||||
Regenerate Task
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
onClick={handleAppend}
|
||||
disabled={!prompt.trim() || isLoading}
|
||||
variant={isSubtask ? 'default' : 'outline'}
|
||||
className={
|
||||
isSubtask
|
||||
? 'bg-primary text-primary-foreground hover:bg-primary/90'
|
||||
: 'bg-secondary text-secondary-foreground hover:bg-secondary/90 border-widget-border'
|
||||
}
|
||||
>
|
||||
{isAppending ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
Regenerating...
|
||||
{isSubtask ? 'Updating...' : 'Appending...'}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Wand2 className="w-4 h-4 mr-2" />
|
||||
Regenerate Task
|
||||
<PlusCircle className="w-4 h-4 mr-2" />
|
||||
{isSubtask ? 'Add Notes to Subtask' : 'Append to Task'}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button
|
||||
onClick={handleAppend}
|
||||
disabled={!prompt.trim() || isRegenerating || isAppending}
|
||||
variant={isSubtask ? 'default' : 'outline'}
|
||||
className={
|
||||
isSubtask
|
||||
? 'bg-primary text-primary-foreground hover:bg-primary/90'
|
||||
: 'bg-secondary text-secondary-foreground hover:bg-secondary/90 border-widget-border'
|
||||
}
|
||||
>
|
||||
{isAppending ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
{isSubtask ? 'Updating...' : 'Appending...'}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<PlusCircle className="w-4 h-4 mr-2" />
|
||||
{isSubtask ? 'Add Notes to Subtask' : 'Append to Task'}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Scope Adjustment Section */}
|
||||
<div className="border-t border-vscode-widget-border pt-4 space-y-4">
|
||||
<div>
|
||||
<Label className="block text-sm font-medium text-vscode-foreground/80 mb-3">
|
||||
Task Complexity Adjustment
|
||||
</Label>
|
||||
|
||||
{/* Strength Selection */}
|
||||
<div className="mb-3">
|
||||
<Label className="block text-xs text-vscode-foreground/60 mb-2">
|
||||
Adjustment Strength
|
||||
</Label>
|
||||
<div className="flex gap-2">
|
||||
{(['light', 'regular', 'heavy'] as const).map((strength) => (
|
||||
<Button
|
||||
key={strength}
|
||||
onClick={() => setScopeStrength(strength)}
|
||||
variant={scopeStrength === strength ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
className={
|
||||
scopeStrength === strength
|
||||
? 'bg-accent text-accent-foreground border-accent'
|
||||
: 'border-widget-border text-vscode-foreground/80 hover:bg-vscode-list-hoverBackground'
|
||||
}
|
||||
disabled={isLoading}
|
||||
>
|
||||
{strength.charAt(0).toUpperCase() + strength.slice(1)}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Scope Prompt */}
|
||||
<Textarea
|
||||
placeholder="Optional: Specify how to adjust complexity (e.g., 'Focus on error handling', 'Remove unnecessary details', 'Add more implementation steps')"
|
||||
value={scopePrompt}
|
||||
onChange={(e) => setScopePrompt(e.target.value)}
|
||||
className="min-h-[80px] bg-vscode-input-background border-vscode-input-border text-vscode-input-foreground placeholder-vscode-input-foreground/50 focus:border-vscode-focusBorder focus:ring-vscode-focusBorder"
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<Button
|
||||
onClick={handleScopeUp}
|
||||
disabled={isLoading}
|
||||
variant="outline"
|
||||
className="flex-1 border-green-600/50 text-green-400 hover:bg-green-600/10 hover:border-green-500"
|
||||
>
|
||||
{isScopingUp ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
Scoping Up...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<TrendingUp className="w-4 h-4 mr-2" />
|
||||
Scope Up
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={handleScopeDown}
|
||||
disabled={isLoading}
|
||||
variant="outline"
|
||||
className="flex-1 border-blue-600/50 text-blue-400 hover:bg-blue-600/10 hover:border-blue-500"
|
||||
>
|
||||
{isScopingDown ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
Scoping Down...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<TrendingDown className="w-4 h-4 mr-2" />
|
||||
Scope Down
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Help Text */}
|
||||
<div className="text-xs text-vscode-foreground/60 space-y-1">
|
||||
{isSubtask ? (
|
||||
<p>
|
||||
@@ -200,6 +347,12 @@ export const AIActionsSection: React.FC<AIActionsSectionProps> = ({
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
<p>
|
||||
<strong>Scope Up:</strong> Increases task complexity with more details, requirements, or implementation steps
|
||||
</p>
|
||||
<p>
|
||||
<strong>Scope Down:</strong> Decreases task complexity by simplifying or removing unnecessary details
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CollapsibleSection>
|
||||
|
||||
@@ -227,3 +227,119 @@ export function useUpdateSubtask() {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Hook to scope up task complexity
|
||||
export function useScopeUpTask() {
|
||||
const { sendMessage } = useVSCodeContext();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
taskId,
|
||||
strength = 'regular',
|
||||
prompt,
|
||||
options = {}
|
||||
}: {
|
||||
taskId: string;
|
||||
strength?: 'light' | 'regular' | 'heavy';
|
||||
prompt?: string;
|
||||
options?: { research?: boolean };
|
||||
}) => {
|
||||
console.log('🔄 Scoping up task:', taskId, strength, prompt, options);
|
||||
|
||||
const response = await sendMessage({
|
||||
type: 'mcpRequest',
|
||||
tool: 'scope_up_task',
|
||||
params: {
|
||||
id: taskId,
|
||||
strength,
|
||||
prompt,
|
||||
research: options.research || false
|
||||
}
|
||||
});
|
||||
|
||||
console.log('📥 Scope up task response:', response);
|
||||
|
||||
// Check for error in response
|
||||
if (response && typeof response === 'object' && 'error' in response) {
|
||||
throw new Error(response.error || 'Failed to scope up task');
|
||||
}
|
||||
|
||||
return response;
|
||||
},
|
||||
onSuccess: async (data, variables) => {
|
||||
console.log(
|
||||
'✅ Task scope up successful, invalidating all task queries'
|
||||
);
|
||||
console.log('Task ID:', variables.taskId);
|
||||
|
||||
// Invalidate ALL task-related queries
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: taskKeys.all
|
||||
});
|
||||
|
||||
console.log(
|
||||
'🔄 All task queries invalidated for scoped up task:',
|
||||
variables.taskId
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Hook to scope down task complexity
|
||||
export function useScopeDownTask() {
|
||||
const { sendMessage } = useVSCodeContext();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
taskId,
|
||||
strength = 'regular',
|
||||
prompt,
|
||||
options = {}
|
||||
}: {
|
||||
taskId: string;
|
||||
strength?: 'light' | 'regular' | 'heavy';
|
||||
prompt?: string;
|
||||
options?: { research?: boolean };
|
||||
}) => {
|
||||
console.log('🔄 Scoping down task:', taskId, strength, prompt, options);
|
||||
|
||||
const response = await sendMessage({
|
||||
type: 'mcpRequest',
|
||||
tool: 'scope_down_task',
|
||||
params: {
|
||||
id: taskId,
|
||||
strength,
|
||||
prompt,
|
||||
research: options.research || false
|
||||
}
|
||||
});
|
||||
|
||||
console.log('📥 Scope down task response:', response);
|
||||
|
||||
// Check for error in response
|
||||
if (response && typeof response === 'object' && 'error' in response) {
|
||||
throw new Error(response.error || 'Failed to scope down task');
|
||||
}
|
||||
|
||||
return response;
|
||||
},
|
||||
onSuccess: async (data, variables) => {
|
||||
console.log(
|
||||
'✅ Task scope down successful, invalidating all task queries'
|
||||
);
|
||||
console.log('Task ID:', variables.taskId);
|
||||
|
||||
// Invalidate ALL task-related queries
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: taskKeys.all
|
||||
});
|
||||
|
||||
console.log(
|
||||
'🔄 All task queries invalidated for scoped down task:',
|
||||
variables.taskId
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user