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:
Eyal Toledano
2025-08-02 17:48:43 +03:00
committed by Ralph Khreish
parent 2b79822002
commit 451a55d44f
4 changed files with 335 additions and 66 deletions

View File

@@ -1,6 +1,6 @@
{
"meta": {
"generatedAt": "2025-08-02T09:50:02.895Z",
"generatedAt": "2025-08-02T14:28:59.851Z",
"tasksAnalyzed": 1,
"totalTasks": 93,
"analysisCount": 1,
@@ -10,12 +10,12 @@
},
"complexityAnalysis": [
{
"taskId": 104,
"taskTitle": "Implement 'scope-up' and 'scope-down' CLI Commands for Dynamic Task Complexity Adjustment",
"taskId": 24,
"taskTitle": "Implement AI-Powered Test Generation Command",
"complexityScore": 8,
"recommendedSubtasks": 6,
"expansionPrompt": "Break down the implementation of the 'scope-up' and 'scope-down' CLI commands into detailed subtasks, focusing on: 1) Command structure and argument parsing, 2) Task/subtask retrieval and context gathering, 3) AI prompt construction for different strength levels, 4) Response processing and task updating, 5) Error handling and user feedback, and 6) Integration with existing systems including MCP tool. For each subtask, include specific implementation details, dependencies, and test strategies.",
"reasoning": "This task has high complexity (8/10) due to several factors: 1) It requires integration with multiple existing systems (CLI, AI services, task management), 2) It involves sophisticated AI prompt engineering with context awareness, 3) It needs to handle various parameters and edge cases, 4) It must maintain data integrity across task modifications, and 5) It requires both CLI and MCP tool implementations. The task already has 5 subtasks that cover the main components, but could benefit from one additional subtask focused on testing and documentation."
"expansionPrompt": "Expand task 24 'Implement AI-Powered Test Generation Command' into 6 subtasks, focusing on: 1) Command structure implementation, 2) AI prompt engineering for test generation, 3) Test file generation and output, 4) Framework-specific template implementation, 5) MCP tool integration, and 6) Documentation and help system integration. Include detailed implementation steps, dependencies, and testing approaches for each subtask.",
"reasoning": "This task has high complexity due to several challenging aspects: 1) AI integration requiring sophisticated prompt engineering, 2) Test generation across multiple frameworks, 3) File system operations with proper error handling, 4) MCP tool integration, 5) Complex configuration requirements, and 6) Framework-specific template generation. The task already has 5 subtasks but could benefit from reorganization based on the updated implementation details in the info blocks, particularly around framework support and configuration."
}
]
}

View File

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

View File

@@ -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
);
}
});
}

2
package-lock.json generated
View File

@@ -84,7 +84,7 @@
}
},
"apps/extension": {
"version": "0.0.0",
"version": "0.22.3",
"devDependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",