feat(extension): complete VS Code extension with kanban board interface (#997)

---------
Co-authored-by: DavidMaliglowka <13022280+DavidMaliglowka@users.noreply.github.com>
Co-authored-by: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
DavidMaliglowka
2025-08-01 07:04:22 -05:00
committed by GitHub
parent 60c03c548d
commit 64302dc191
101 changed files with 20608 additions and 181 deletions

View File

@@ -0,0 +1,207 @@
import type React from 'react';
import { useState } from 'react';
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 {
useUpdateTask,
useUpdateSubtask
} from '../../webview/hooks/useTaskQueries';
import type { TaskMasterTask } from '../../webview/types';
interface AIActionsSectionProps {
currentTask: TaskMasterTask;
isSubtask: boolean;
parentTask?: TaskMasterTask | null;
sendMessage: (message: any) => Promise<any>;
refreshComplexityAfterAI: () => void;
onRegeneratingChange?: (isRegenerating: boolean) => void;
onAppendingChange?: (isAppending: boolean) => void;
}
export const AIActionsSection: React.FC<AIActionsSectionProps> = ({
currentTask,
isSubtask,
parentTask,
sendMessage,
refreshComplexityAfterAI,
onRegeneratingChange,
onAppendingChange
}) => {
const [prompt, setPrompt] = useState('');
const [lastAction, setLastAction] = useState<'regenerate' | 'append' | null>(
null
);
const updateTask = useUpdateTask();
const updateSubtask = useUpdateSubtask();
const handleRegenerate = async () => {
if (!currentTask || !prompt.trim()) {
return;
}
setLastAction('regenerate');
onRegeneratingChange?.(true);
try {
if (isSubtask && parentTask) {
await updateSubtask.mutateAsync({
taskId: `${parentTask.id}.${currentTask.id}`,
prompt: prompt,
options: { research: false }
});
} else {
await updateTask.mutateAsync({
taskId: currentTask.id,
updates: { description: prompt },
options: { append: false, research: false }
});
}
setPrompt('');
refreshComplexityAfterAI();
} catch (error) {
console.error('❌ TaskDetailsView: Failed to regenerate task:', error);
} finally {
setLastAction(null);
onRegeneratingChange?.(false);
}
};
const handleAppend = async () => {
if (!currentTask || !prompt.trim()) {
return;
}
setLastAction('append');
onAppendingChange?.(true);
try {
if (isSubtask && parentTask) {
await updateSubtask.mutateAsync({
taskId: `${parentTask.id}.${currentTask.id}`,
prompt: prompt,
options: { research: false }
});
} else {
await updateTask.mutateAsync({
taskId: currentTask.id,
updates: { description: prompt },
options: { append: true, research: false }
});
}
setPrompt('');
refreshComplexityAfterAI();
} catch (error) {
console.error('❌ TaskDetailsView: Failed to append to task:', error);
} finally {
setLastAction(null);
onAppendingChange?.(false);
}
};
// Track loading states based on the last action
const isLoading = updateTask.isPending || updateSubtask.isPending;
const isRegenerating = isLoading && lastAction === 'regenerate';
const isAppending = isLoading && lastAction === 'append';
return (
<CollapsibleSection
title="AI Actions"
icon={Wand2}
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"
>
{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() || 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 className="text-xs text-vscode-foreground/60 space-y-1">
{isSubtask ? (
<p>
<strong>Add Notes:</strong> Appends timestamped implementation
notes, progress updates, or findings to this subtask's details
</p>
) : (
<>
<p>
<strong>Regenerate:</strong> Completely rewrites the task
description and subtasks based on your prompt
</p>
<p>
<strong>Append:</strong> Adds new content to the existing task
implementation details based on your prompt
</p>
</>
)}
</div>
</div>
</CollapsibleSection>
);
};

View File

@@ -0,0 +1,204 @@
import type React from 'react';
import { CollapsibleSection } from '@/components/ui/CollapsibleSection';
interface MarkdownRendererProps {
content: string;
className?: string;
}
const MarkdownRenderer: React.FC<MarkdownRendererProps> = ({
content,
className = ''
}) => {
const parseMarkdown = (text: string) => {
const parts = [];
const lines = text.split('\n');
let currentBlock = [];
let inCodeBlock = false;
let codeLanguage = '';
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
if (line.startsWith('```')) {
if (inCodeBlock) {
if (currentBlock.length > 0) {
parts.push({
type: 'code',
content: currentBlock.join('\n'),
language: codeLanguage
});
currentBlock = [];
}
inCodeBlock = false;
codeLanguage = '';
} else {
if (currentBlock.length > 0) {
parts.push({
type: 'text',
content: currentBlock.join('\n')
});
currentBlock = [];
}
inCodeBlock = true;
codeLanguage = line.substring(3).trim();
}
} else {
currentBlock.push(line);
}
}
if (currentBlock.length > 0) {
parts.push({
type: inCodeBlock ? 'code' : 'text',
content: currentBlock.join('\n'),
language: codeLanguage
});
}
return parts;
};
const parts = parseMarkdown(content);
return (
<div className={className}>
{parts.map((part, index) => {
if (part.type === 'code') {
return (
<pre
key={index}
className="bg-vscode-editor-background rounded-md p-4 overflow-x-auto mb-4 border border-vscode-editor-lineHighlightBorder"
>
<code className="text-sm text-vscode-editor-foreground font-mono">
{part.content}
</code>
</pre>
);
}
return (
<div key={index} className="whitespace-pre-wrap mb-4 last:mb-0">
{part.content.split('\n').map((line, lineIndex) => {
const bulletMatch = line.match(/^(\s*)([-*])\s(.+)$/);
if (bulletMatch) {
const indent = bulletMatch[1].length;
return (
<div
key={lineIndex}
className="flex gap-2 mb-1"
style={{ paddingLeft: `${indent * 16}px` }}
>
<span className="text-vscode-foreground/60"></span>
<span className="flex-1">{bulletMatch[3]}</span>
</div>
);
}
const numberedMatch = line.match(/^(\s*)(\d+\.)\s(.+)$/);
if (numberedMatch) {
const indent = numberedMatch[1].length;
return (
<div
key={lineIndex}
className="flex gap-2 mb-1"
style={{ paddingLeft: `${indent * 16}px` }}
>
<span className="text-vscode-foreground/60 font-mono">
{numberedMatch[2]}
</span>
<span className="flex-1">{numberedMatch[3]}</span>
</div>
);
}
const headingMatch = line.match(/^(#{1,6})\s(.+)$/);
if (headingMatch) {
const level = headingMatch[1].length;
const headingLevel = Math.min(level + 2, 6);
const headingClassName =
'font-semibold text-vscode-foreground mb-2 mt-4 first:mt-0';
switch (headingLevel) {
case 3:
return (
<h3 key={lineIndex} className={headingClassName}>
{headingMatch[2]}
</h3>
);
case 4:
return (
<h4 key={lineIndex} className={headingClassName}>
{headingMatch[2]}
</h4>
);
case 5:
return (
<h5 key={lineIndex} className={headingClassName}>
{headingMatch[2]}
</h5>
);
case 6:
return (
<h6 key={lineIndex} className={headingClassName}>
{headingMatch[2]}
</h6>
);
default:
return (
<h3 key={lineIndex} className={headingClassName}>
{headingMatch[2]}
</h3>
);
}
}
if (line.trim() === '') {
return <div key={lineIndex} className="h-2" />;
}
return (
<div key={lineIndex} className="mb-2 last:mb-0">
{line}
</div>
);
})}
</div>
);
})}
</div>
);
};
interface DetailsSectionProps {
title: string;
content?: string;
error?: string | null;
emptyMessage?: string;
defaultExpanded?: boolean;
}
export const DetailsSection: React.FC<DetailsSectionProps> = ({
title,
content,
error,
emptyMessage = 'No details available',
defaultExpanded = false
}) => {
return (
<CollapsibleSection title={title} defaultExpanded={defaultExpanded}>
<div className={title.toLowerCase().replace(/\s+/g, '-') + '-content'}>
{error ? (
<div className="text-sm text-red-400 py-2">
Error loading {title.toLowerCase()}: {error}
</div>
) : content !== undefined && content !== '' ? (
<MarkdownRenderer content={content} />
) : (
<div className="text-sm text-vscode-foreground/50 py-2">
{emptyMessage}
</div>
)}
</div>
</CollapsibleSection>
);
};

View File

@@ -0,0 +1,47 @@
import type React from 'react';
import type { TaskMasterTask } from '../../webview/types';
// Custom Priority Badge Component with theme-adaptive styling
export const PriorityBadge: React.FC<{
priority: TaskMasterTask['priority'];
}> = ({ priority }) => {
const getPriorityColors = (priority: string) => {
switch (priority) {
case 'high':
return {
backgroundColor: 'rgba(239, 68, 68, 0.2)', // red-500 with opacity
color: '#dc2626', // red-600 - works in both themes
borderColor: 'rgba(239, 68, 68, 0.4)'
};
case 'medium':
return {
backgroundColor: 'rgba(245, 158, 11, 0.2)', // amber-500 with opacity
color: '#d97706', // amber-600 - works in both themes
borderColor: 'rgba(245, 158, 11, 0.4)'
};
case 'low':
return {
backgroundColor: 'rgba(34, 197, 94, 0.2)', // green-500 with opacity
color: '#16a34a', // green-600 - works in both themes
borderColor: 'rgba(34, 197, 94, 0.4)'
};
default:
return {
backgroundColor: 'rgba(156, 163, 175, 0.2)',
color: 'var(--vscode-foreground)',
borderColor: 'rgba(156, 163, 175, 0.4)'
};
}
};
const colors = getPriorityColors(priority || '');
return (
<span
className="inline-flex items-center px-2 py-1 text-xs font-medium rounded-md border"
style={colors}
>
{priority || 'None'}
</span>
);
};

View File

@@ -0,0 +1,218 @@
import type React from 'react';
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Badge } from '@/components/ui/badge';
import { CollapsibleSection } from '@/components/ui/CollapsibleSection';
import { Plus, Loader2 } from 'lucide-react';
import type { TaskMasterTask } from '../../webview/types';
import { getStatusDotColor } from '../constants';
interface SubtasksSectionProps {
currentTask: TaskMasterTask;
isSubtask: boolean;
sendMessage: (message: any) => Promise<any>;
onNavigateToTask: (taskId: string) => void;
}
export const SubtasksSection: React.FC<SubtasksSectionProps> = ({
currentTask,
isSubtask,
sendMessage,
onNavigateToTask
}) => {
const [isAddingSubtask, setIsAddingSubtask] = useState(false);
const [newSubtaskTitle, setNewSubtaskTitle] = useState('');
const [newSubtaskDescription, setNewSubtaskDescription] = useState('');
const [isSubmittingSubtask, setIsSubmittingSubtask] = useState(false);
const handleAddSubtask = async () => {
if (!currentTask || !newSubtaskTitle.trim() || isSubtask) {
return;
}
setIsSubmittingSubtask(true);
try {
await sendMessage({
type: 'addSubtask',
data: {
parentTaskId: currentTask.id,
subtaskData: {
title: newSubtaskTitle.trim(),
description: newSubtaskDescription.trim() || undefined,
status: 'pending'
}
}
});
// Reset form and close
setNewSubtaskTitle('');
setNewSubtaskDescription('');
setIsAddingSubtask(false);
} catch (error) {
console.error('❌ TaskDetailsView: Failed to add subtask:', error);
} finally {
setIsSubmittingSubtask(false);
}
};
const handleCancelAddSubtask = () => {
setIsAddingSubtask(false);
setNewSubtaskTitle('');
setNewSubtaskDescription('');
};
if (
!((currentTask.subtasks && currentTask.subtasks.length > 0) || !isSubtask)
) {
return null;
}
const rightElement = (
<>
{currentTask.subtasks && currentTask.subtasks.length > 0 && (
<span className="text-sm text-vscode-foreground/50">
{currentTask.subtasks?.filter((st) => st.status === 'done').length}/
{currentTask.subtasks?.length}
</span>
)}
{!isSubtask && (
<Button
variant="ghost"
size="sm"
className="ml-auto p-1 h-6 w-6 hover:bg-vscode-button-hoverBackground"
onClick={() => setIsAddingSubtask(true)}
title="Add subtask"
>
<Plus className="w-4 h-4" />
</Button>
)}
</>
);
return (
<CollapsibleSection
title="Sub-issues"
defaultExpanded={true}
rightElement={rightElement}
>
<div className="space-y-3">
{/* Add Subtask Form */}
{isAddingSubtask && (
<div className="bg-widget-background rounded-lg p-4 border border-widget-border">
<h4 className="text-sm font-medium text-vscode-foreground mb-3">
Add New Subtask
</h4>
<div className="space-y-3">
<div>
<Label
htmlFor="subtask-title"
className="block text-sm text-vscode-foreground/80 mb-1"
>
Title*
</Label>
<input
id="subtask-title"
type="text"
placeholder="Enter subtask title..."
value={newSubtaskTitle}
onChange={(e) => setNewSubtaskTitle(e.target.value)}
className="w-full px-3 py-2 text-sm bg-vscode-input-background border border-vscode-input-border text-vscode-input-foreground placeholder-vscode-input-foreground/50 rounded focus:border-vscode-focusBorder focus:ring-1 focus:ring-vscode-focusBorder"
disabled={isSubmittingSubtask}
/>
</div>
<div>
<Label
htmlFor="subtask-description"
className="block text-sm text-vscode-foreground/80 mb-1"
>
Description (Optional)
</Label>
<Textarea
id="subtask-description"
placeholder="Enter subtask description..."
value={newSubtaskDescription}
onChange={(e) => setNewSubtaskDescription(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={isSubmittingSubtask}
/>
</div>
<div className="flex gap-3 pt-2">
<Button
onClick={handleAddSubtask}
disabled={!newSubtaskTitle.trim() || isSubmittingSubtask}
className="bg-primary text-primary-foreground hover:bg-primary/90"
>
{isSubmittingSubtask ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Adding...
</>
) : (
<>
<Plus className="w-4 h-4 mr-2" />
Add Subtask
</>
)}
</Button>
<Button
onClick={handleCancelAddSubtask}
variant="outline"
disabled={isSubmittingSubtask}
className="border-widget-border"
>
Cancel
</Button>
</div>
</div>
</div>
)}
{/* Subtasks List */}
{currentTask.subtasks && currentTask.subtasks.length > 0 && (
<div className="space-y-2">
{currentTask.subtasks.map((subtask, index) => {
const subtaskId = `${currentTask.id}.${index + 1}`;
return (
<div
key={subtask.id}
className="flex items-center gap-3 p-3 rounded-md border border-textSeparator-foreground hover:border-vscode-border/70 transition-colors cursor-pointer"
onClick={() => onNavigateToTask(subtaskId)}
>
<div
className="w-4 h-4 rounded-full flex items-center justify-center"
style={{
backgroundColor: getStatusDotColor(subtask.status)
}}
/>
<div className="flex-1 min-w-0">
<p className="text-sm text-vscode-foreground truncate">
{subtask.title}
</p>
{subtask.description && (
<p className="text-xs text-vscode-foreground/60 truncate mt-0.5">
{subtask.description}
</p>
)}
</div>
<div className="flex items-center gap-2 flex-shrink-0">
<Badge
variant="secondary"
className="text-xs bg-secondary/20 border-secondary/30 text-secondary-foreground px-2 py-0.5"
>
{subtask.status === 'pending' ? 'todo' : subtask.status}
</Badge>
</div>
</div>
);
})}
</div>
)}
</div>
</CollapsibleSection>
);
};

View File

@@ -0,0 +1,291 @@
import type React from 'react';
import { useState, useEffect } from 'react';
import { Button } from '@/components/ui/button';
import { Loader2 } from 'lucide-react';
import { PriorityBadge } from './PriorityBadge';
import type { TaskMasterTask } from '../../webview/types';
interface TaskMetadataSidebarProps {
currentTask: TaskMasterTask;
tasks: TaskMasterTask[];
complexity: any;
isSubtask: boolean;
sendMessage: (message: any) => Promise<any>;
onStatusChange: (status: TaskMasterTask['status']) => void;
onDependencyClick: (depId: string) => void;
isRegenerating?: boolean;
isAppending?: boolean;
}
export const TaskMetadataSidebar: React.FC<TaskMetadataSidebarProps> = ({
currentTask,
tasks,
complexity,
isSubtask,
sendMessage,
onStatusChange,
onDependencyClick,
isRegenerating = false,
isAppending = false
}) => {
const [isLoadingComplexity, setIsLoadingComplexity] = useState(false);
const [mcpComplexityScore, setMcpComplexityScore] = useState<
number | undefined
>(undefined);
// Get complexity score from task
const currentComplexityScore = complexity?.score;
// Display logic - use MCP score if available, otherwise use current score
const displayComplexityScore =
mcpComplexityScore !== undefined
? mcpComplexityScore
: currentComplexityScore;
// Fetch complexity from MCP when needed
const fetchComplexityFromMCP = async (force = false) => {
if (!currentTask || (!force && currentComplexityScore !== undefined)) {
return;
}
setIsLoadingComplexity(true);
try {
const complexityResult = await sendMessage({
type: 'mcpRequest',
tool: 'complexity_report',
params: {}
});
if (complexityResult?.data?.report?.complexityAnalysis) {
const taskComplexity =
complexityResult.data.report.complexityAnalysis.tasks?.find(
(t: any) => t.id === currentTask.id
);
if (taskComplexity) {
setMcpComplexityScore(taskComplexity.complexityScore);
}
}
} catch (error) {
console.error('Failed to fetch complexity from MCP:', error);
} finally {
setIsLoadingComplexity(false);
}
};
// Handle running complexity analysis for a task
const handleRunComplexityAnalysis = async () => {
if (!currentTask) {
return;
}
setIsLoadingComplexity(true);
try {
// Run complexity analysis on this specific task
await sendMessage({
type: 'mcpRequest',
tool: 'analyze_project_complexity',
params: {
ids: currentTask.id.toString(),
research: false
}
});
// After analysis, fetch the updated complexity report
setTimeout(() => {
fetchComplexityFromMCP(true);
}, 1000);
} catch (error) {
console.error('Failed to run complexity analysis:', error);
} finally {
setIsLoadingComplexity(false);
}
};
// Effect to handle complexity on task change
useEffect(() => {
if (currentTask?.id) {
setMcpComplexityScore(undefined);
if (currentComplexityScore === undefined) {
fetchComplexityFromMCP();
}
}
}, [currentTask?.id, currentComplexityScore]);
return (
<div className="md:col-span-1 border-l border-textSeparator-foreground">
<div className="p-6">
<div className="space-y-6">
<div>
<h3 className="text-sm font-medium text-vscode-foreground/70 mb-3">
Properties
</h3>
</div>
<div className="space-y-4">
{/* Status */}
<div className="flex items-center justify-between">
<span className="text-sm text-vscode-foreground/70">Status</span>
<select
value={currentTask.status}
onChange={(e) =>
onStatusChange(e.target.value as TaskMasterTask['status'])
}
className="border rounded-md px-3 py-1 text-sm font-medium focus:ring-1 focus:border-vscode-focusBorder focus:ring-vscode-focusBorder"
style={{
backgroundColor:
currentTask.status === 'pending'
? 'rgba(156, 163, 175, 0.2)'
: currentTask.status === 'in-progress'
? 'rgba(245, 158, 11, 0.2)'
: currentTask.status === 'review'
? 'rgba(59, 130, 246, 0.2)'
: currentTask.status === 'done'
? 'rgba(34, 197, 94, 0.2)'
: currentTask.status === 'deferred'
? 'rgba(239, 68, 68, 0.2)'
: 'var(--vscode-input-background)',
color:
currentTask.status === 'pending'
? 'var(--vscode-foreground)'
: currentTask.status === 'in-progress'
? '#d97706'
: currentTask.status === 'review'
? '#2563eb'
: currentTask.status === 'done'
? '#16a34a'
: currentTask.status === 'deferred'
? '#dc2626'
: 'var(--vscode-foreground)',
borderColor:
currentTask.status === 'pending'
? 'rgba(156, 163, 175, 0.4)'
: currentTask.status === 'in-progress'
? 'rgba(245, 158, 11, 0.4)'
: currentTask.status === 'review'
? 'rgba(59, 130, 246, 0.4)'
: currentTask.status === 'done'
? 'rgba(34, 197, 94, 0.4)'
: currentTask.status === 'deferred'
? 'rgba(239, 68, 68, 0.4)'
: 'var(--vscode-input-border)'
}}
>
<option value="pending">To do</option>
<option value="in-progress">In Progress</option>
<option value="review">Review</option>
<option value="done">Done</option>
<option value="deferred">Deferred</option>
</select>
</div>
{/* Priority */}
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Priority</span>
<PriorityBadge priority={currentTask.priority} />
</div>
{/* Complexity Score */}
<div className="space-y-2">
<label className="text-sm font-medium text-[var(--vscode-foreground)]">
Complexity Score
</label>
{isLoadingComplexity ? (
<div className="flex items-center gap-2">
<Loader2 className="w-4 h-4 animate-spin text-[var(--vscode-descriptionForeground)]" />
<span className="text-sm text-[var(--vscode-descriptionForeground)]">
Loading...
</span>
</div>
) : displayComplexityScore !== undefined ? (
<div className="flex items-center gap-2">
<span className="text-sm font-medium text-[var(--vscode-foreground)]">
{displayComplexityScore}/10
</span>
<div
className={`flex-1 rounded-full h-2 ${
displayComplexityScore >= 7
? 'bg-red-500/20'
: displayComplexityScore >= 4
? 'bg-yellow-500/20'
: 'bg-green-500/20'
}`}
>
<div
className={`h-2 rounded-full transition-all duration-300 ${
displayComplexityScore >= 7
? 'bg-red-500'
: displayComplexityScore >= 4
? 'bg-yellow-500'
: 'bg-green-500'
}`}
style={{
width: `${(displayComplexityScore || 0) * 10}%`
}}
/>
</div>
</div>
) : currentTask?.status === 'done' ||
currentTask?.status === 'deferred' ||
currentTask?.status === 'review' ? (
<div className="text-sm text-[var(--vscode-descriptionForeground)]">
N/A
</div>
) : (
<>
<div className="text-sm text-[var(--vscode-descriptionForeground)]">
No complexity score available
</div>
<div className="mt-3">
<Button
onClick={() => handleRunComplexityAnalysis()}
variant="outline"
size="sm"
className="text-xs"
disabled={isRegenerating || isAppending}
>
Run Complexity Analysis
</Button>
</div>
</>
)}
</div>
</div>
<div className="border-b border-textSeparator-foreground" />
{/* Dependencies */}
{currentTask.dependencies && currentTask.dependencies.length > 0 && (
<div>
<h4 className="text-sm font-medium text-vscode-foreground/70 mb-3">
Dependencies
</h4>
<div className="space-y-2">
{currentTask.dependencies.map((depId) => {
// Convert both to string for comparison since depId might be string or number
const depTask = tasks.find(
(t) => String(t.id) === String(depId)
);
const fullTitle = `Task ${depId}: ${depTask?.title || 'Unknown Task'}`;
const truncatedTitle =
fullTitle.length > 40
? fullTitle.substring(0, 37) + '...'
: fullTitle;
return (
<div
key={depId}
className="text-sm text-link cursor-pointer hover:text-link-hover"
onClick={() => onDependencyClick(depId)}
title={fullTitle}
>
{truncatedTitle}
</div>
);
})}
</div>
</div>
)}
{/* Divider after Dependencies */}
{currentTask.dependencies && currentTask.dependencies.length > 0 && (
<div className="border-b border-textSeparator-foreground" />
)}
</div>
</div>
</div>
);
};

View File

@@ -0,0 +1,116 @@
import { useMemo } from 'react';
import { useTaskDetails as useTaskDetailsQuery } from '../../webview/hooks/useTaskQueries';
import type { TaskMasterTask } from '../../webview/types';
interface TaskFileData {
details?: string;
testStrategy?: string;
}
interface UseTaskDetailsProps {
taskId: string;
sendMessage: (message: any) => Promise<any>;
tasks: TaskMasterTask[];
}
export const useTaskDetails = ({
taskId,
sendMessage,
tasks
}: UseTaskDetailsProps) => {
// Parse task ID to determine if it's a subtask (e.g., "13.2")
const { isSubtask, parentId, subtaskIndex, taskIdForFetch } = useMemo(() => {
// Ensure taskId is a string
const taskIdStr = String(taskId);
const parts = taskIdStr.split('.');
if (parts.length === 2) {
return {
isSubtask: true,
parentId: parts[0],
subtaskIndex: parseInt(parts[1]) - 1, // Convert to 0-based index
taskIdForFetch: parts[0] // Always fetch parent task for subtasks
};
}
return {
isSubtask: false,
parentId: taskIdStr,
subtaskIndex: -1,
taskIdForFetch: taskIdStr
};
}, [taskId]);
// Use React Query to fetch full task details
const { data: fullTaskData, error: taskDetailsError } =
useTaskDetailsQuery(taskIdForFetch);
// Find current task from local state for immediate display
const { currentTask, parentTask } = useMemo(() => {
if (isSubtask) {
const parent = tasks.find((t) => t.id === parentId);
if (parent && parent.subtasks && parent.subtasks[subtaskIndex]) {
const subtask = parent.subtasks[subtaskIndex];
return { currentTask: subtask, parentTask: parent };
}
} else {
const task = tasks.find((t) => t.id === String(taskId));
if (task) {
return { currentTask: task, parentTask: null };
}
}
return { currentTask: null, parentTask: null };
}, [taskId, tasks, isSubtask, parentId, subtaskIndex]);
// Merge full task data from React Query with local state
const mergedCurrentTask = useMemo(() => {
if (!currentTask || !fullTaskData) return currentTask;
if (isSubtask && fullTaskData.subtasks) {
// Find the specific subtask in the full data
const subtaskData = fullTaskData.subtasks.find(
(st: any) =>
st.id === currentTask.id || st.id === parseInt(currentTask.id as any)
);
if (subtaskData) {
return { ...currentTask, ...subtaskData };
}
} else if (!isSubtask) {
// Merge parent task data
return { ...currentTask, ...fullTaskData };
}
return currentTask;
}, [currentTask, fullTaskData, isSubtask]);
// Extract task file data
const taskFileData: TaskFileData = useMemo(() => {
if (!mergedCurrentTask) return {};
return {
details: mergedCurrentTask.details || '',
testStrategy: mergedCurrentTask.testStrategy || ''
};
}, [mergedCurrentTask]);
// Get complexity score
const complexity = useMemo(() => {
if (mergedCurrentTask?.complexityScore !== undefined) {
return { score: mergedCurrentTask.complexityScore };
}
return null;
}, [mergedCurrentTask]);
// Function to refresh data after AI operations
const refreshComplexityAfterAI = () => {
// React Query will automatically refetch when mutations invalidate the query
// No need for manual refresh
};
return {
currentTask: mergedCurrentTask,
parentTask,
isSubtask,
taskFileData,
taskFileDataError: taskDetailsError ? 'Failed to load task details' : null,
complexity,
refreshComplexityAfterAI
};
};