feat: add react-query for state handling, it was becoming complex
This commit is contained in:
@@ -269,7 +269,8 @@
|
|||||||
"postcss": "8.5.6",
|
"postcss": "8.5.6",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
"tailwindcss": "4.1.11",
|
"tailwindcss": "4.1.11",
|
||||||
"typescript": "^5.8.3"
|
"typescript": "^5.8.3",
|
||||||
|
"@tanstack/react-query": "^5.83.0"
|
||||||
},
|
},
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"overrides": {
|
"overrides": {
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ import { Label } from '@/components/ui/label';
|
|||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
import { CollapsibleSection } from '@/components/ui/CollapsibleSection';
|
import { CollapsibleSection } from '@/components/ui/CollapsibleSection';
|
||||||
import { Wand2, Loader2, PlusCircle } from 'lucide-react';
|
import { Wand2, Loader2, PlusCircle } from 'lucide-react';
|
||||||
|
import {
|
||||||
|
useUpdateTask,
|
||||||
|
useUpdateSubtask
|
||||||
|
} from '../../webview/hooks/useTaskQueries';
|
||||||
import type { TaskMasterTask } from '../../webview/types';
|
import type { TaskMasterTask } from '../../webview/types';
|
||||||
|
|
||||||
interface AIActionsSectionProps {
|
interface AIActionsSectionProps {
|
||||||
@@ -27,42 +31,33 @@ export const AIActionsSection: React.FC<AIActionsSectionProps> = ({
|
|||||||
onAppendingChange
|
onAppendingChange
|
||||||
}) => {
|
}) => {
|
||||||
const [prompt, setPrompt] = useState('');
|
const [prompt, setPrompt] = useState('');
|
||||||
const [isRegenerating, setIsRegenerating] = useState(false);
|
const updateTask = useUpdateTask();
|
||||||
const [isAppending, setIsAppending] = useState(false);
|
const updateSubtask = useUpdateSubtask();
|
||||||
|
|
||||||
const handleRegenerate = async () => {
|
const handleRegenerate = async () => {
|
||||||
if (!currentTask || !prompt.trim()) {
|
if (!currentTask || !prompt.trim()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsRegenerating(true);
|
|
||||||
try {
|
try {
|
||||||
if (isSubtask && parentTask) {
|
if (isSubtask && parentTask) {
|
||||||
await sendMessage({
|
await updateSubtask.mutateAsync({
|
||||||
type: 'updateSubtask',
|
taskId: `${parentTask.id}.${currentTask.id}`,
|
||||||
data: {
|
prompt: prompt,
|
||||||
taskId: `${parentTask.id}.${currentTask.id}`,
|
options: { research: false }
|
||||||
prompt: prompt,
|
|
||||||
options: { research: false }
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await sendMessage({
|
await updateTask.mutateAsync({
|
||||||
type: 'updateTask',
|
taskId: currentTask.id,
|
||||||
data: {
|
updates: { description: prompt },
|
||||||
taskId: currentTask.id,
|
options: { append: false, research: false }
|
||||||
updates: { description: prompt },
|
|
||||||
options: { append: false, research: false }
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setPrompt('');
|
||||||
refreshComplexityAfterAI();
|
refreshComplexityAfterAI();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ TaskDetailsView: Failed to regenerate task:', error);
|
console.error('❌ TaskDetailsView: Failed to regenerate task:', error);
|
||||||
} finally {
|
|
||||||
setIsRegenerating(false);
|
|
||||||
setPrompt('');
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -71,37 +66,32 @@ export const AIActionsSection: React.FC<AIActionsSectionProps> = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsAppending(true);
|
|
||||||
try {
|
try {
|
||||||
if (isSubtask && parentTask) {
|
if (isSubtask && parentTask) {
|
||||||
await sendMessage({
|
await updateSubtask.mutateAsync({
|
||||||
type: 'updateSubtask',
|
taskId: `${parentTask.id}.${currentTask.id}`,
|
||||||
data: {
|
prompt: prompt,
|
||||||
taskId: `${parentTask.id}.${currentTask.id}`,
|
options: { research: false }
|
||||||
prompt: prompt,
|
|
||||||
options: { research: false }
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
await sendMessage({
|
await updateTask.mutateAsync({
|
||||||
type: 'updateTask',
|
taskId: currentTask.id,
|
||||||
data: {
|
updates: { description: prompt },
|
||||||
taskId: currentTask.id,
|
options: { append: true, research: false }
|
||||||
updates: { description: prompt },
|
|
||||||
options: { append: true, research: false }
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setPrompt('');
|
||||||
refreshComplexityAfterAI();
|
refreshComplexityAfterAI();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ TaskDetailsView: Failed to append to task:', error);
|
console.error('❌ TaskDetailsView: Failed to append to task:', error);
|
||||||
} finally {
|
|
||||||
setIsAppending(false);
|
|
||||||
setPrompt('');
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Track loading states
|
||||||
|
const isRegenerating = updateTask.isPending || updateSubtask.isPending;
|
||||||
|
const isAppending = updateTask.isPending || updateSubtask.isPending;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CollapsibleSection
|
<CollapsibleSection
|
||||||
title="AI Actions"
|
title="AI Actions"
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useEffect, useState, useCallback } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
import { useTaskDetails as useTaskDetailsQuery } from '../../webview/hooks/useTaskQueries';
|
||||||
import type { TaskMasterTask } from '../../webview/types';
|
import type { TaskMasterTask } from '../../webview/types';
|
||||||
|
|
||||||
interface TaskFileData {
|
interface TaskFileData {
|
||||||
@@ -17,162 +18,96 @@ export const useTaskDetails = ({
|
|||||||
sendMessage,
|
sendMessage,
|
||||||
tasks
|
tasks
|
||||||
}: UseTaskDetailsProps) => {
|
}: UseTaskDetailsProps) => {
|
||||||
const [taskFileData, setTaskFileData] = useState<TaskFileData>({});
|
|
||||||
const [taskFileDataError, setTaskFileDataError] = useState<string | null>(
|
|
||||||
null
|
|
||||||
);
|
|
||||||
const [complexity, setComplexity] = useState<any>(null);
|
|
||||||
const [currentTask, setCurrentTask] = useState<TaskMasterTask | null>(null);
|
|
||||||
const [parentTask, setParentTask] = useState<TaskMasterTask | null>(null);
|
|
||||||
|
|
||||||
// Determine if this is a subtask
|
|
||||||
const isSubtask = taskId.includes('.');
|
|
||||||
|
|
||||||
// Parse task ID to determine if it's a subtask (e.g., "13.2")
|
// Parse task ID to determine if it's a subtask (e.g., "13.2")
|
||||||
const parseTaskId = (id: string) => {
|
const { isSubtask, parentId, subtaskIndex, taskIdForFetch } = useMemo(() => {
|
||||||
const parts = id.split('.');
|
const parts = taskId.split('.');
|
||||||
if (parts.length === 2) {
|
if (parts.length === 2) {
|
||||||
return {
|
return {
|
||||||
isSubtask: true,
|
isSubtask: true,
|
||||||
parentId: parts[0],
|
parentId: parts[0],
|
||||||
subtaskIndex: parseInt(parts[1]) - 1 // Convert to 0-based index
|
subtaskIndex: parseInt(parts[1]) - 1, // Convert to 0-based index
|
||||||
|
taskIdForFetch: parts[0] // Always fetch parent task for subtasks
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
isSubtask: false,
|
isSubtask: false,
|
||||||
parentId: id,
|
parentId: taskId,
|
||||||
subtaskIndex: -1
|
subtaskIndex: -1,
|
||||||
|
taskIdForFetch: taskId
|
||||||
};
|
};
|
||||||
};
|
}, [taskId]);
|
||||||
|
|
||||||
// Find the current task
|
// Use React Query to fetch full task details
|
||||||
useEffect(() => {
|
const { data: fullTaskData, error: taskDetailsError } =
|
||||||
const { isSubtask: isSub, parentId, subtaskIndex } = parseTaskId(taskId);
|
useTaskDetailsQuery(taskIdForFetch);
|
||||||
|
|
||||||
if (isSub) {
|
// Find current task from local state for immediate display
|
||||||
|
const { currentTask, parentTask } = useMemo(() => {
|
||||||
|
if (isSubtask) {
|
||||||
const parent = tasks.find((t) => t.id === parentId);
|
const parent = tasks.find((t) => t.id === parentId);
|
||||||
if (parent && parent.subtasks && parent.subtasks[subtaskIndex]) {
|
if (parent && parent.subtasks && parent.subtasks[subtaskIndex]) {
|
||||||
const subtask = parent.subtasks[subtaskIndex];
|
const subtask = parent.subtasks[subtaskIndex];
|
||||||
setCurrentTask(subtask);
|
return { currentTask: subtask, parentTask: parent };
|
||||||
setParentTask(parent);
|
|
||||||
} else {
|
|
||||||
setCurrentTask(null);
|
|
||||||
setParentTask(null);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const task = tasks.find((t) => t.id === taskId);
|
const task = tasks.find((t) => t.id === taskId);
|
||||||
if (task) {
|
if (task) {
|
||||||
setCurrentTask(task);
|
return { currentTask: task, parentTask: null };
|
||||||
setParentTask(null);
|
|
||||||
} else {
|
|
||||||
setCurrentTask(null);
|
|
||||||
setParentTask(null);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [taskId, tasks]);
|
return { currentTask: null, parentTask: null };
|
||||||
|
}, [taskId, tasks, isSubtask, parentId, subtaskIndex]);
|
||||||
|
|
||||||
// Fetch full task details including details and testStrategy
|
// Merge full task data from React Query with local state
|
||||||
useEffect(() => {
|
const mergedCurrentTask = useMemo(() => {
|
||||||
const fetchTaskDetails = async () => {
|
if (!currentTask || !fullTaskData) return currentTask;
|
||||||
if (!currentTask) return;
|
|
||||||
|
|
||||||
try {
|
if (isSubtask && fullTaskData.subtasks) {
|
||||||
// Use the parent task ID for MCP call since get_task returns parent with subtasks
|
// Find the specific subtask in the full data
|
||||||
const taskIdToFetch =
|
const subtaskData = fullTaskData.subtasks.find(
|
||||||
isSubtask && parentTask ? parentTask.id : currentTask.id;
|
(st: any) =>
|
||||||
|
st.id === currentTask.id || st.id === parseInt(currentTask.id as any)
|
||||||
const result = await sendMessage({
|
);
|
||||||
type: 'mcpRequest',
|
if (subtaskData) {
|
||||||
tool: 'get_task',
|
return { ...currentTask, ...subtaskData };
|
||||||
params: {
|
|
||||||
id: taskIdToFetch
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Parse the MCP response - it comes as content[0].text JSON string
|
|
||||||
let fullTaskData = null;
|
|
||||||
if (result?.data?.content?.[0]?.text) {
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(result.data.content[0].text);
|
|
||||||
fullTaskData = parsed.data;
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to parse MCP response:', e);
|
|
||||||
}
|
|
||||||
} else if (result?.data?.data) {
|
|
||||||
// Fallback if response structure is different
|
|
||||||
fullTaskData = result.data.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fullTaskData) {
|
|
||||||
if (isSubtask && fullTaskData.subtasks) {
|
|
||||||
// Find the specific subtask
|
|
||||||
const subtaskData = fullTaskData.subtasks.find(
|
|
||||||
(st: any) =>
|
|
||||||
st.id === currentTask.id ||
|
|
||||||
st.id === parseInt(currentTask.id as any)
|
|
||||||
);
|
|
||||||
if (subtaskData) {
|
|
||||||
setTaskFileData({
|
|
||||||
details: subtaskData.details || '',
|
|
||||||
testStrategy: subtaskData.testStrategy || ''
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Use the main task data
|
|
||||||
setTaskFileData({
|
|
||||||
details: fullTaskData.details || '',
|
|
||||||
testStrategy: fullTaskData.testStrategy || ''
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Failed to fetch task details:', error);
|
|
||||||
setTaskFileDataError('Failed to load task details');
|
|
||||||
}
|
}
|
||||||
|
} 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]);
|
||||||
|
|
||||||
fetchTaskDetails();
|
// Get complexity score
|
||||||
}, [currentTask, isSubtask, parentTask, sendMessage]);
|
const complexity = useMemo(() => {
|
||||||
|
if (mergedCurrentTask?.complexityScore !== undefined) {
|
||||||
// Fetch complexity score
|
return { score: mergedCurrentTask.complexityScore };
|
||||||
const fetchComplexity = useCallback(async () => {
|
|
||||||
if (!currentTask) return;
|
|
||||||
|
|
||||||
// First check if the task already has a complexity score
|
|
||||||
if (currentTask.complexityScore !== undefined) {
|
|
||||||
setComplexity({ score: currentTask.complexityScore });
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
|
}, [mergedCurrentTask]);
|
||||||
|
|
||||||
try {
|
// Function to refresh data after AI operations
|
||||||
const result = await sendMessage({
|
|
||||||
type: 'getComplexity',
|
|
||||||
data: { taskId: currentTask.id }
|
|
||||||
});
|
|
||||||
if (result) {
|
|
||||||
setComplexity(result);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ TaskDetailsView: Failed to fetch complexity:', error);
|
|
||||||
}
|
|
||||||
}, [currentTask, sendMessage]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
fetchComplexity();
|
|
||||||
}, [fetchComplexity]);
|
|
||||||
|
|
||||||
const refreshComplexityAfterAI = () => {
|
const refreshComplexityAfterAI = () => {
|
||||||
setTimeout(() => {
|
// React Query will automatically refetch when mutations invalidate the query
|
||||||
fetchComplexity();
|
// No need for manual refresh
|
||||||
}, 2000);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
currentTask,
|
currentTask: mergedCurrentTask,
|
||||||
parentTask,
|
parentTask,
|
||||||
isSubtask,
|
isSubtask,
|
||||||
taskFileData,
|
taskFileData,
|
||||||
taskFileDataError,
|
taskFileDataError: taskDetailsError ? 'Failed to load task details' : null,
|
||||||
complexity,
|
complexity,
|
||||||
refreshComplexityAfterAI
|
refreshComplexityAfterAI
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -141,6 +141,7 @@ export type KanbanProviderProps = {
|
|||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
onDragEnd: (event: DragEndEvent) => void;
|
onDragEnd: (event: DragEndEvent) => void;
|
||||||
onDragStart?: (event: DragEndEvent) => void;
|
onDragStart?: (event: DragEndEvent) => void;
|
||||||
|
onDragCancel?: () => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
dragOverlay?: ReactNode;
|
dragOverlay?: ReactNode;
|
||||||
};
|
};
|
||||||
@@ -149,6 +150,7 @@ export const KanbanProvider = ({
|
|||||||
children,
|
children,
|
||||||
onDragEnd,
|
onDragEnd,
|
||||||
onDragStart,
|
onDragStart,
|
||||||
|
onDragCancel,
|
||||||
className,
|
className,
|
||||||
dragOverlay
|
dragOverlay
|
||||||
}: KanbanProviderProps) => {
|
}: KanbanProviderProps) => {
|
||||||
@@ -170,6 +172,7 @@ export const KanbanProvider = ({
|
|||||||
collisionDetection={rectIntersection}
|
collisionDetection={rectIntersection}
|
||||||
onDragEnd={onDragEnd}
|
onDragEnd={onDragEnd}
|
||||||
onDragStart={onDragStart}
|
onDragStart={onDragStart}
|
||||||
|
onDragCancel={onDragCancel}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|||||||
@@ -174,9 +174,66 @@ export class WebviewManager {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'updateTask':
|
case 'updateTask':
|
||||||
// Handle task content updates
|
// Handle task content updates with MCP
|
||||||
await this.repository.updateContent(data.taskId, data.updates);
|
if (this.mcpClient) {
|
||||||
response = { success: true };
|
try {
|
||||||
|
const { taskId, updates, options = {} } = data;
|
||||||
|
|
||||||
|
// Use the update_task MCP tool
|
||||||
|
const result = await this.mcpClient.callTool('update_task', {
|
||||||
|
id: taskId,
|
||||||
|
prompt: updates.description || '',
|
||||||
|
append: options.append || false,
|
||||||
|
research: options.research || false,
|
||||||
|
projectRoot: vscode.workspace.workspaceFolders?.[0]?.uri.fsPath
|
||||||
|
});
|
||||||
|
|
||||||
|
// Refresh tasks after update
|
||||||
|
await this.repository.refresh();
|
||||||
|
const updatedTasks = await this.repository.getAll();
|
||||||
|
this.broadcast('tasksUpdated', {
|
||||||
|
tasks: updatedTasks,
|
||||||
|
source: 'task-update'
|
||||||
|
});
|
||||||
|
response = { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Failed to update task via MCP:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error('MCP client not initialized');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'updateSubtask':
|
||||||
|
// Handle subtask content updates with MCP
|
||||||
|
if (this.mcpClient) {
|
||||||
|
try {
|
||||||
|
const { taskId, prompt, options = {} } = data;
|
||||||
|
|
||||||
|
// Use the update_subtask MCP tool
|
||||||
|
const result = await this.mcpClient.callTool('update_subtask', {
|
||||||
|
id: taskId,
|
||||||
|
prompt: prompt,
|
||||||
|
research: options.research || false,
|
||||||
|
projectRoot: vscode.workspace.workspaceFolders?.[0]?.uri.fsPath
|
||||||
|
});
|
||||||
|
|
||||||
|
// Refresh tasks after update
|
||||||
|
await this.repository.refresh();
|
||||||
|
const updatedTasks = await this.repository.getAll();
|
||||||
|
this.broadcast('tasksUpdated', {
|
||||||
|
tasks: updatedTasks,
|
||||||
|
source: 'task-update'
|
||||||
|
});
|
||||||
|
response = { success: true };
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Failed to update subtask via MCP:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error('MCP client not initialized');
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'getComplexity':
|
case 'getComplexity':
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
import React, { useReducer, useState, useEffect, useRef } from 'react';
|
import React, { useReducer, useState, useEffect, useRef } from 'react';
|
||||||
import { VSCodeContext } from './contexts/VSCodeContext';
|
import { VSCodeContext } from './contexts/VSCodeContext';
|
||||||
|
import { QueryProvider } from './providers/QueryProvider';
|
||||||
import { TaskMasterKanban } from './components/TaskMasterKanban';
|
import { TaskMasterKanban } from './components/TaskMasterKanban';
|
||||||
import TaskDetailsView from '@/components/TaskDetailsView';
|
import TaskDetailsView from '@/components/TaskDetailsView';
|
||||||
import { ConfigView } from '@/components/ConfigView';
|
import { ConfigView } from '@/components/ConfigView';
|
||||||
@@ -46,21 +47,7 @@ export const App: React.FC = () => {
|
|||||||
// Notify extension that webview is ready
|
// Notify extension that webview is ready
|
||||||
vscode.postMessage({ type: 'ready' });
|
vscode.postMessage({ type: 'ready' });
|
||||||
|
|
||||||
// Request initial tasks data
|
// React Query will handle task fetching, so we only need to load tags data
|
||||||
sendMessage({ type: 'getTasks' })
|
|
||||||
.then((tasksData) => {
|
|
||||||
console.log('📋 Initial tasks loaded:', tasksData);
|
|
||||||
dispatch({ type: 'SET_TASKS', payload: tasksData });
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error('❌ Failed to load initial tasks:', error);
|
|
||||||
dispatch({
|
|
||||||
type: 'SET_ERROR',
|
|
||||||
payload: `Failed to load tasks: ${error.message}`
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Request tags data
|
|
||||||
sendMessage({ type: 'getTags' })
|
sendMessage({ type: 'getTags' })
|
||||||
.then((tagsData) => {
|
.then((tagsData) => {
|
||||||
if (tagsData?.tags && tagsData?.currentTag) {
|
if (tagsData?.tags && tagsData?.currentTag) {
|
||||||
@@ -93,58 +80,64 @@ export const App: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<VSCodeContext.Provider value={contextValue}>
|
<QueryProvider>
|
||||||
<ErrorBoundary
|
<VSCodeContext.Provider value={contextValue}>
|
||||||
onError={(error) => {
|
<ErrorBoundary
|
||||||
// Handle React errors and show appropriate toast
|
onError={(error) => {
|
||||||
dispatch({
|
// Handle React errors and show appropriate toast
|
||||||
type: 'ADD_TOAST',
|
dispatch({
|
||||||
payload: createToast(
|
type: 'ADD_TOAST',
|
||||||
'error',
|
payload: createToast(
|
||||||
'Component Error',
|
'error',
|
||||||
`A React component crashed: ${error.message}`,
|
'Component Error',
|
||||||
10000
|
`A React component crashed: ${error.message}`,
|
||||||
)
|
10000
|
||||||
});
|
)
|
||||||
}}
|
});
|
||||||
>
|
}}
|
||||||
{/* Conditional rendering for different views */}
|
>
|
||||||
{(() => {
|
{/* Conditional rendering for different views */}
|
||||||
console.log(
|
{(() => {
|
||||||
'🎯 App render - currentView:',
|
console.log(
|
||||||
state.currentView,
|
'🎯 App render - currentView:',
|
||||||
'selectedTaskId:',
|
state.currentView,
|
||||||
state.selectedTaskId
|
'selectedTaskId:',
|
||||||
);
|
state.selectedTaskId
|
||||||
|
|
||||||
if (state.currentView === 'config') {
|
|
||||||
return (
|
|
||||||
<ConfigView
|
|
||||||
sendMessage={sendMessage}
|
|
||||||
onNavigateBack={() => dispatch({ type: 'NAVIGATE_TO_KANBAN' })}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
if (state.currentView === 'task-details' && state.selectedTaskId) {
|
if (state.currentView === 'config') {
|
||||||
return (
|
return (
|
||||||
<TaskDetailsView
|
<ConfigView
|
||||||
taskId={state.selectedTaskId}
|
sendMessage={sendMessage}
|
||||||
onNavigateBack={() => dispatch({ type: 'NAVIGATE_TO_KANBAN' })}
|
onNavigateBack={() =>
|
||||||
onNavigateToTask={(taskId: string) =>
|
dispatch({ type: 'NAVIGATE_TO_KANBAN' })
|
||||||
dispatch({ type: 'NAVIGATE_TO_TASK', payload: taskId })
|
}
|
||||||
}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return <TaskMasterKanban />;
|
if (state.currentView === 'task-details' && state.selectedTaskId) {
|
||||||
})()}
|
return (
|
||||||
<ToastContainer
|
<TaskDetailsView
|
||||||
notifications={state.toastNotifications}
|
taskId={state.selectedTaskId}
|
||||||
onDismiss={(id) => dispatch({ type: 'REMOVE_TOAST', payload: id })}
|
onNavigateBack={() =>
|
||||||
/>
|
dispatch({ type: 'NAVIGATE_TO_KANBAN' })
|
||||||
</ErrorBoundary>
|
}
|
||||||
</VSCodeContext.Provider>
|
onNavigateToTask={(taskId: string) =>
|
||||||
|
dispatch({ type: 'NAVIGATE_TO_TASK', payload: taskId })
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <TaskMasterKanban />;
|
||||||
|
})()}
|
||||||
|
<ToastContainer
|
||||||
|
notifications={state.toastNotifications}
|
||||||
|
onDismiss={(id) => dispatch({ type: 'REMOVE_TOAST', payload: id })}
|
||||||
|
/>
|
||||||
|
</ErrorBoundary>
|
||||||
|
</VSCodeContext.Provider>
|
||||||
|
</QueryProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -26,12 +26,9 @@ export const TaskCard: React.FC<TaskCardProps> = ({
|
|||||||
return (
|
return (
|
||||||
<KanbanCard
|
<KanbanCard
|
||||||
id={task.id}
|
id={task.id}
|
||||||
feature={{
|
name={task.title}
|
||||||
id: task.id,
|
index={0} // Index is not used in our implementation
|
||||||
title: task.title,
|
parent={task.status}
|
||||||
column: task.status
|
|
||||||
}}
|
|
||||||
dragging={dragging}
|
|
||||||
className="cursor-pointer p-3 transition-shadow hover:shadow-md bg-vscode-editor-background border-vscode-border group"
|
className="cursor-pointer p-3 transition-shadow hover:shadow-md bg-vscode-editor-background border-vscode-border group"
|
||||||
onClick={handleCardClick}
|
onClick={handleCardClick}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
* Main Kanban Board Component
|
* Main Kanban Board Component
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React, { useState, useCallback } from 'react';
|
import React, { useState, useCallback, useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
type DragEndEvent,
|
type DragEndEvent,
|
||||||
KanbanBoard,
|
KanbanBoard,
|
||||||
@@ -16,15 +16,19 @@ import { PollingStatus } from './PollingStatus';
|
|||||||
import { TagDropdown } from './TagDropdown';
|
import { TagDropdown } from './TagDropdown';
|
||||||
import { EmptyState } from './EmptyState';
|
import { EmptyState } from './EmptyState';
|
||||||
import { useVSCodeContext } from '../contexts/VSCodeContext';
|
import { useVSCodeContext } from '../contexts/VSCodeContext';
|
||||||
|
import {
|
||||||
|
useTasks,
|
||||||
|
useUpdateTaskStatus,
|
||||||
|
useUpdateTask
|
||||||
|
} from '../hooks/useTaskQueries';
|
||||||
import { kanbanStatuses, HEADER_HEIGHT } from '../constants';
|
import { kanbanStatuses, HEADER_HEIGHT } from '../constants';
|
||||||
import type { TaskMasterTask, TaskUpdates } from '../types';
|
import type { TaskMasterTask, TaskUpdates } from '../types';
|
||||||
|
|
||||||
export const TaskMasterKanban: React.FC = () => {
|
export const TaskMasterKanban: React.FC = () => {
|
||||||
const { state, dispatch, sendMessage, availableHeight } = useVSCodeContext();
|
const { state, dispatch, sendMessage, availableHeight } = useVSCodeContext();
|
||||||
const {
|
const {
|
||||||
tasks,
|
loading: legacyLoading,
|
||||||
loading,
|
error: legacyError,
|
||||||
error,
|
|
||||||
editingTask,
|
editingTask,
|
||||||
polling,
|
polling,
|
||||||
currentTag,
|
currentTag,
|
||||||
@@ -32,6 +36,23 @@ export const TaskMasterKanban: React.FC = () => {
|
|||||||
} = state;
|
} = state;
|
||||||
const [activeTask, setActiveTask] = useState<TaskMasterTask | null>(null);
|
const [activeTask, setActiveTask] = useState<TaskMasterTask | null>(null);
|
||||||
|
|
||||||
|
// Use React Query to fetch tasks
|
||||||
|
const {
|
||||||
|
data: serverTasks = [],
|
||||||
|
isLoading,
|
||||||
|
error
|
||||||
|
} = useTasks({ tag: currentTag });
|
||||||
|
const updateTaskStatus = useUpdateTaskStatus();
|
||||||
|
const updateTask = useUpdateTask();
|
||||||
|
|
||||||
|
// Temporary state only for active drag operations
|
||||||
|
const [tempReorderedTasks, setTempReorderedTasks] = useState<
|
||||||
|
TaskMasterTask[] | null
|
||||||
|
>(null);
|
||||||
|
|
||||||
|
// Use temp tasks only if actively set, otherwise use server tasks
|
||||||
|
const tasks = tempReorderedTasks ?? serverTasks;
|
||||||
|
|
||||||
// Calculate header height for proper kanban board sizing
|
// Calculate header height for proper kanban board sizing
|
||||||
const kanbanHeight = availableHeight - HEADER_HEIGHT;
|
const kanbanHeight = availableHeight - HEADER_HEIGHT;
|
||||||
|
|
||||||
@@ -60,21 +81,11 @@ export const TaskMasterKanban: React.FC = () => {
|
|||||||
const handleUpdateTask = async (taskId: string, updates: TaskUpdates) => {
|
const handleUpdateTask = async (taskId: string, updates: TaskUpdates) => {
|
||||||
console.log(`🔄 Updating task ${taskId} content:`, updates);
|
console.log(`🔄 Updating task ${taskId} content:`, updates);
|
||||||
|
|
||||||
// Optimistic update
|
|
||||||
dispatch({
|
|
||||||
type: 'UPDATE_TASK_CONTENT',
|
|
||||||
payload: { taskId, updates }
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Send update to extension
|
await updateTask.mutateAsync({
|
||||||
await sendMessage({
|
taskId,
|
||||||
type: 'updateTask',
|
updates,
|
||||||
data: {
|
options: { append: false, research: false }
|
||||||
taskId,
|
|
||||||
updates,
|
|
||||||
options: { append: false, research: false }
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(`✅ Task ${taskId} content updated successfully`);
|
console.log(`✅ Task ${taskId} content updated successfully`);
|
||||||
@@ -86,26 +97,6 @@ export const TaskMasterKanban: React.FC = () => {
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`❌ Failed to update task ${taskId}:`, error);
|
console.error(`❌ Failed to update task ${taskId}:`, error);
|
||||||
|
|
||||||
// Revert the optimistic update on error
|
|
||||||
const originalTask = editingTask?.editData;
|
|
||||||
if (originalTask) {
|
|
||||||
dispatch({
|
|
||||||
type: 'UPDATE_TASK_CONTENT',
|
|
||||||
payload: {
|
|
||||||
taskId,
|
|
||||||
updates: {
|
|
||||||
title: originalTask.title,
|
|
||||||
description: originalTask.description,
|
|
||||||
details: originalTask.details,
|
|
||||||
priority: originalTask.priority,
|
|
||||||
testStrategy: originalTask.testStrategy,
|
|
||||||
dependencies: originalTask.dependencies
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'SET_ERROR',
|
type: 'SET_ERROR',
|
||||||
payload: `Failed to update task: ${error}`
|
payload: `Failed to update task: ${error}`
|
||||||
@@ -115,57 +106,71 @@ export const TaskMasterKanban: React.FC = () => {
|
|||||||
|
|
||||||
// Handle drag start
|
// Handle drag start
|
||||||
const handleDragStart = useCallback(
|
const handleDragStart = useCallback(
|
||||||
(taskId: string) => {
|
(event: DragEndEvent) => {
|
||||||
|
const taskId = event.active.id as string;
|
||||||
const task = tasks.find((t) => t.id === taskId);
|
const task = tasks.find((t) => t.id === taskId);
|
||||||
if (task) {
|
if (task) {
|
||||||
setActiveTask(task);
|
setActiveTask(task);
|
||||||
dispatch({ type: 'SET_USER_INTERACTING', payload: true });
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[tasks, dispatch]
|
[tasks]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Handle drag cancel
|
||||||
|
const handleDragCancel = useCallback(() => {
|
||||||
|
setActiveTask(null);
|
||||||
|
// Clear any temporary state
|
||||||
|
setTempReorderedTasks(null);
|
||||||
|
}, []);
|
||||||
|
|
||||||
// Handle drag end
|
// Handle drag end
|
||||||
const handleDragEnd = useCallback(
|
const handleDragEnd = useCallback(
|
||||||
async (event: DragEndEvent) => {
|
async (event: DragEndEvent) => {
|
||||||
dispatch({ type: 'SET_USER_INTERACTING', payload: false });
|
const { active, over } = event;
|
||||||
|
|
||||||
|
// Reset active task
|
||||||
setActiveTask(null);
|
setActiveTask(null);
|
||||||
|
|
||||||
const { active, over } = event;
|
if (!over || active.id === over.id) {
|
||||||
if (!over || active.id === over.id) return;
|
// Clear any temp state if drag was cancelled
|
||||||
|
setTempReorderedTasks(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const taskId = active.id as string;
|
const taskId = active.id as string;
|
||||||
const newStatus = over.id as TaskMasterTask['status'];
|
const newStatus = over.id as TaskMasterTask['status'];
|
||||||
|
|
||||||
// Find the task
|
// Find the task
|
||||||
const task = tasks.find((t) => t.id === taskId);
|
const task = tasks.find((t) => t.id === taskId);
|
||||||
if (!task || task.status === newStatus) return;
|
if (!task || task.status === newStatus) {
|
||||||
|
// Clear temp state if no change needed
|
||||||
|
setTempReorderedTasks(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Optimistic update
|
// Create the optimistically reordered tasks
|
||||||
dispatch({
|
const reorderedTasks = tasks.map((t) =>
|
||||||
type: 'UPDATE_TASK_STATUS',
|
t.id === taskId ? { ...t, status: newStatus } : t
|
||||||
payload: { taskId, newStatus }
|
);
|
||||||
});
|
|
||||||
|
// Set temporary state to show immediate visual feedback
|
||||||
|
setTempReorderedTasks(reorderedTasks);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Send update to extension
|
// Update on server - React Query will handle optimistic updates
|
||||||
await sendMessage({
|
await updateTaskStatus.mutateAsync({ taskId, newStatus });
|
||||||
type: 'updateTaskStatus',
|
// Clear temp state after mutation starts successfully
|
||||||
data: { taskId, newStatus }
|
setTempReorderedTasks(null);
|
||||||
});
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Revert on error
|
// On error, clear temp state - React Query will revert optimistic update
|
||||||
dispatch({
|
setTempReorderedTasks(null);
|
||||||
type: 'UPDATE_TASK_STATUS',
|
|
||||||
payload: { taskId, newStatus: task.status }
|
|
||||||
});
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: 'SET_ERROR',
|
type: 'SET_ERROR',
|
||||||
payload: `Failed to update task status: ${error}`
|
payload: `Failed to update task status: ${error}`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[tasks, sendMessage, dispatch]
|
[tasks, updateTaskStatus, dispatch]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Handle retry connection
|
// Handle retry connection
|
||||||
@@ -178,22 +183,22 @@ export const TaskMasterKanban: React.FC = () => {
|
|||||||
async (tagName: string) => {
|
async (tagName: string) => {
|
||||||
console.log('Switching to tag:', tagName);
|
console.log('Switching to tag:', tagName);
|
||||||
await sendMessage({ type: 'switchTag', data: { tagName } });
|
await sendMessage({ type: 'switchTag', data: { tagName } });
|
||||||
// After switching tags, fetch the new tasks for that specific tag
|
dispatch({
|
||||||
const tasksData = await sendMessage({
|
type: 'SET_TAG_DATA',
|
||||||
type: 'getTasks',
|
payload: { currentTag: tagName, availableTags }
|
||||||
data: { tag: tagName }
|
|
||||||
});
|
});
|
||||||
console.log('Received new tasks for tag', tagName, ':', {
|
|
||||||
tasksData,
|
|
||||||
isArray: Array.isArray(tasksData),
|
|
||||||
count: Array.isArray(tasksData) ? tasksData.length : 'not an array'
|
|
||||||
});
|
|
||||||
dispatch({ type: 'SET_TASKS', payload: tasksData });
|
|
||||||
console.log('Dispatched SET_TASKS');
|
|
||||||
},
|
},
|
||||||
[sendMessage, dispatch]
|
[sendMessage, dispatch, availableTags]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Use React Query loading state
|
||||||
|
const loading = isLoading || legacyLoading;
|
||||||
|
const displayError = error
|
||||||
|
? error instanceof Error
|
||||||
|
? error.message
|
||||||
|
: String(error)
|
||||||
|
: legacyError;
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -208,10 +213,10 @@ export const TaskMasterKanban: React.FC = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (error) {
|
if (displayError) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-red-500/10 border border-red-500/30 rounded-lg p-4 m-4">
|
<div className="bg-red-500/10 border border-red-500/30 rounded-lg p-4 m-4">
|
||||||
<p className="text-red-400 text-sm">Error: {error}</p>
|
<p className="text-red-400 text-sm">Error: {displayError}</p>
|
||||||
<button
|
<button
|
||||||
onClick={() => dispatch({ type: 'CLEAR_ERROR' })}
|
onClick={() => dispatch({ type: 'CLEAR_ERROR' })}
|
||||||
className="mt-2 text-sm text-red-400 hover:text-red-300 underline"
|
className="mt-2 text-sm text-red-400 hover:text-red-300 underline"
|
||||||
@@ -228,7 +233,7 @@ export const TaskMasterKanban: React.FC = () => {
|
|||||||
<div className="flex-shrink-0 p-4 bg-vscode-sidebar-background border-b border-vscode-border">
|
<div className="flex-shrink-0 p-4 bg-vscode-sidebar-background border-b border-vscode-border">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h1 className="text-lg font-semibold text-vscode-foreground">
|
<h1 className="text-lg font-semibold text-vscode-foreground">
|
||||||
Task Master Kanban
|
TaskMaster Kanban
|
||||||
</h1>
|
</h1>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<TagDropdown
|
<TagDropdown
|
||||||
@@ -250,7 +255,7 @@ export const TaskMasterKanban: React.FC = () => {
|
|||||||
<button
|
<button
|
||||||
onClick={() => dispatch({ type: 'NAVIGATE_TO_CONFIG' })}
|
onClick={() => dispatch({ type: 'NAVIGATE_TO_CONFIG' })}
|
||||||
className="p-1.5 rounded hover:bg-vscode-button-hoverBackground transition-colors"
|
className="p-1.5 rounded hover:bg-vscode-button-hoverBackground transition-colors"
|
||||||
title="Task Master Configuration"
|
title="TaskMaster Configuration"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
className="w-4 h-4 text-vscode-foreground/70"
|
className="w-4 h-4 text-vscode-foreground/70"
|
||||||
@@ -286,6 +291,7 @@ export const TaskMasterKanban: React.FC = () => {
|
|||||||
<KanbanProvider
|
<KanbanProvider
|
||||||
onDragStart={handleDragStart}
|
onDragStart={handleDragStart}
|
||||||
onDragEnd={handleDragEnd}
|
onDragEnd={handleDragEnd}
|
||||||
|
onDragCancel={handleDragCancel}
|
||||||
className="kanban-container w-full h-full overflow-x-auto overflow-y-hidden"
|
className="kanban-container w-full h-full overflow-x-auto overflow-y-hidden"
|
||||||
dragOverlay={
|
dragOverlay={
|
||||||
activeTask ? <TaskCard task={activeTask} dragging /> : null
|
activeTask ? <TaskCard task={activeTask} dragging /> : null
|
||||||
@@ -300,7 +306,6 @@ export const TaskMasterKanban: React.FC = () => {
|
|||||||
<KanbanBoard
|
<KanbanBoard
|
||||||
key={status.id}
|
key={status.id}
|
||||||
id={status.id}
|
id={status.id}
|
||||||
title={status.name}
|
|
||||||
className={`
|
className={`
|
||||||
w-80 flex flex-col
|
w-80 flex flex-col
|
||||||
border border-vscode-border/30
|
border border-vscode-border/30
|
||||||
@@ -309,7 +314,7 @@ export const TaskMasterKanban: React.FC = () => {
|
|||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<KanbanHeader
|
<KanbanHeader
|
||||||
name={`${status.name} (${statusTasks.length})`}
|
name={`${status.title} (${statusTasks.length})`}
|
||||||
color={status.color}
|
color={status.color}
|
||||||
className="px-3 py-3 text-sm font-medium flex-shrink-0 border-b border-vscode-border/30"
|
className="px-3 py-3 text-sm font-medium flex-shrink-0 border-b border-vscode-border/30"
|
||||||
/>
|
/>
|
||||||
@@ -325,7 +330,7 @@ export const TaskMasterKanban: React.FC = () => {
|
|||||||
maxHeight: `${kanbanHeight - 80}px`
|
maxHeight: `${kanbanHeight - 80}px`
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<KanbanCards column={status.id}>
|
<KanbanCards>
|
||||||
{statusTasks.map((task) => (
|
{statusTasks.map((task) => (
|
||||||
<TaskCard
|
<TaskCard
|
||||||
key={task.id}
|
key={task.id}
|
||||||
|
|||||||
@@ -4,13 +4,38 @@
|
|||||||
|
|
||||||
import type { Status } from '@/components/ui/shadcn-io/kanban';
|
import type { Status } from '@/components/ui/shadcn-io/kanban';
|
||||||
|
|
||||||
export const kanbanStatuses: Status[] = [
|
export const kanbanStatuses = [
|
||||||
{ id: 'pending', name: 'Pending', color: 'yellow' },
|
{
|
||||||
{ id: 'in-progress', name: 'In Progress', color: 'blue' },
|
id: 'pending',
|
||||||
{ id: 'review', name: 'Review', color: 'purple' },
|
title: 'Pending',
|
||||||
{ id: 'done', name: 'Done', color: 'green' },
|
color: 'yellow',
|
||||||
{ id: 'deferred', name: 'Deferred', color: 'gray' }
|
className: 'text-yellow-600 border-yellow-600/20'
|
||||||
];
|
},
|
||||||
|
{
|
||||||
|
id: 'in-progress',
|
||||||
|
title: 'In Progress',
|
||||||
|
color: 'blue',
|
||||||
|
className: 'text-blue-600 border-blue-600/20'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'review',
|
||||||
|
title: 'Review',
|
||||||
|
color: 'purple',
|
||||||
|
className: 'text-purple-600 border-purple-600/20'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'done',
|
||||||
|
title: 'Done',
|
||||||
|
color: 'green',
|
||||||
|
className: 'text-green-600 border-green-600/20'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'deferred',
|
||||||
|
title: 'Deferred',
|
||||||
|
color: 'gray',
|
||||||
|
className: 'text-gray-600 border-gray-600/20'
|
||||||
|
}
|
||||||
|
] as const;
|
||||||
|
|
||||||
export const CACHE_DURATION = 30000; // 30 seconds
|
export const CACHE_DURATION = 30000; // 30 seconds
|
||||||
export const REQUEST_TIMEOUT = 30000; // 30 seconds
|
export const REQUEST_TIMEOUT = 30000; // 30 seconds
|
||||||
|
|||||||
189
apps/extension/src/webview/hooks/useTaskQueries.ts
Normal file
189
apps/extension/src/webview/hooks/useTaskQueries.ts
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
import { useVSCodeContext } from '../contexts/VSCodeContext';
|
||||||
|
import type { TaskMasterTask, TaskUpdates } from '../types';
|
||||||
|
|
||||||
|
// Query keys factory
|
||||||
|
export const taskKeys = {
|
||||||
|
all: ['tasks'] as const,
|
||||||
|
lists: () => [...taskKeys.all, 'list'] as const,
|
||||||
|
list: (filters: { tag?: string; status?: string }) =>
|
||||||
|
[...taskKeys.lists(), filters] as const,
|
||||||
|
details: () => [...taskKeys.all, 'detail'] as const,
|
||||||
|
detail: (id: string) => [...taskKeys.details(), id] as const
|
||||||
|
};
|
||||||
|
|
||||||
|
// Hook to fetch all tasks
|
||||||
|
export function useTasks(options?: { tag?: string; status?: string }) {
|
||||||
|
const { sendMessage } = useVSCodeContext();
|
||||||
|
|
||||||
|
return useQuery({
|
||||||
|
queryKey: taskKeys.list(options || {}),
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await sendMessage({
|
||||||
|
type: 'getTasks',
|
||||||
|
data: {
|
||||||
|
tag: options?.tag,
|
||||||
|
withSubtasks: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return response as TaskMasterTask[];
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hook to fetch a single task with full details
|
||||||
|
export function useTaskDetails(taskId: string) {
|
||||||
|
const { sendMessage } = useVSCodeContext();
|
||||||
|
|
||||||
|
return useQuery({
|
||||||
|
queryKey: taskKeys.detail(taskId),
|
||||||
|
queryFn: async () => {
|
||||||
|
const response = await sendMessage({
|
||||||
|
type: 'mcpRequest',
|
||||||
|
tool: 'get_task',
|
||||||
|
params: {
|
||||||
|
id: taskId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Parse the MCP response
|
||||||
|
let fullTaskData = null;
|
||||||
|
if (response?.data?.content?.[0]?.text) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(response.data.content[0].text);
|
||||||
|
fullTaskData = parsed.data;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to parse MCP response:', e);
|
||||||
|
}
|
||||||
|
} else if (response?.data?.data) {
|
||||||
|
fullTaskData = response.data.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fullTaskData as TaskMasterTask;
|
||||||
|
},
|
||||||
|
enabled: !!taskId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hook to update task status
|
||||||
|
export function useUpdateTaskStatus() {
|
||||||
|
const { sendMessage } = useVSCodeContext();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async ({
|
||||||
|
taskId,
|
||||||
|
newStatus
|
||||||
|
}: {
|
||||||
|
taskId: string;
|
||||||
|
newStatus: TaskMasterTask['status'];
|
||||||
|
}) => {
|
||||||
|
const response = await sendMessage({
|
||||||
|
type: 'updateTaskStatus',
|
||||||
|
data: { taskId, newStatus }
|
||||||
|
});
|
||||||
|
return { taskId, newStatus, response };
|
||||||
|
},
|
||||||
|
// Optimistic update to prevent snap-back
|
||||||
|
onMutate: async ({ taskId, newStatus }) => {
|
||||||
|
// Cancel any outgoing refetches
|
||||||
|
await queryClient.cancelQueries({ queryKey: taskKeys.all });
|
||||||
|
|
||||||
|
// Snapshot the previous value
|
||||||
|
const previousTasks = queryClient.getQueriesData({
|
||||||
|
queryKey: taskKeys.all
|
||||||
|
});
|
||||||
|
|
||||||
|
// Optimistically update all task queries
|
||||||
|
queryClient.setQueriesData({ queryKey: taskKeys.all }, (old: any) => {
|
||||||
|
if (!old) return old;
|
||||||
|
|
||||||
|
// Handle both array and object responses
|
||||||
|
if (Array.isArray(old)) {
|
||||||
|
return old.map((task: TaskMasterTask) =>
|
||||||
|
task.id === taskId ? { ...task, status: newStatus } : task
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return old;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Return a context object with the snapshot
|
||||||
|
return { previousTasks };
|
||||||
|
},
|
||||||
|
// If the mutation fails, roll back to the previous value
|
||||||
|
onError: (err, variables, context) => {
|
||||||
|
if (context?.previousTasks) {
|
||||||
|
context.previousTasks.forEach(([queryKey, data]) => {
|
||||||
|
queryClient.setQueryData(queryKey, data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Always refetch after error or success to ensure consistency
|
||||||
|
onSettled: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: taskKeys.all });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hook to update task content
|
||||||
|
export function useUpdateTask() {
|
||||||
|
const { sendMessage } = useVSCodeContext();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async ({
|
||||||
|
taskId,
|
||||||
|
updates,
|
||||||
|
options = {}
|
||||||
|
}: {
|
||||||
|
taskId: string;
|
||||||
|
updates: TaskUpdates | { description: string };
|
||||||
|
options?: { append?: boolean; research?: boolean };
|
||||||
|
}) => {
|
||||||
|
await sendMessage({
|
||||||
|
type: 'updateTask',
|
||||||
|
data: { taskId, updates, options }
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSuccess: (_, variables) => {
|
||||||
|
// Invalidate the specific task and all lists
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: taskKeys.detail(variables.taskId)
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({ queryKey: taskKeys.lists() });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hook to update subtask
|
||||||
|
export function useUpdateSubtask() {
|
||||||
|
const { sendMessage } = useVSCodeContext();
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: async ({
|
||||||
|
taskId,
|
||||||
|
prompt,
|
||||||
|
options = {}
|
||||||
|
}: {
|
||||||
|
taskId: string;
|
||||||
|
prompt: string;
|
||||||
|
options?: { research?: boolean };
|
||||||
|
}) => {
|
||||||
|
await sendMessage({
|
||||||
|
type: 'updateSubtask',
|
||||||
|
data: { taskId, prompt, options }
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onSuccess: (_, variables) => {
|
||||||
|
// Extract parent task ID from subtask ID (e.g., "1.2" -> "1")
|
||||||
|
const parentTaskId = variables.taskId.split('.')[0];
|
||||||
|
// Invalidate the parent task details and all lists
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: taskKeys.detail(parentTaskId)
|
||||||
|
});
|
||||||
|
queryClient.invalidateQueries({ queryKey: taskKeys.lists() });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
34
apps/extension/src/webview/providers/QueryProvider.tsx
Normal file
34
apps/extension/src/webview/providers/QueryProvider.tsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
// Create a stable query client
|
||||||
|
const queryClient = new QueryClient({
|
||||||
|
defaultOptions: {
|
||||||
|
queries: {
|
||||||
|
// Don't refetch on window focus by default
|
||||||
|
refetchOnWindowFocus: false,
|
||||||
|
// Keep data fresh for 30 seconds
|
||||||
|
staleTime: 30 * 1000,
|
||||||
|
// Cache data for 5 minutes
|
||||||
|
gcTime: 5 * 60 * 1000,
|
||||||
|
// Retry failed requests 3 times
|
||||||
|
retry: 3,
|
||||||
|
// Retry delay exponentially backs off
|
||||||
|
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000)
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
// Don't retry mutations by default
|
||||||
|
retry: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
interface QueryProviderProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const QueryProvider: React.FC<QueryProviderProps> = ({ children }) => {
|
||||||
|
return (
|
||||||
|
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||||
|
);
|
||||||
|
};
|
||||||
202
package-lock.json
generated
202
package-lock.json
generated
@@ -86,10 +86,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"apps/extension": {
|
"apps/extension": {
|
||||||
"name": "taskr",
|
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@tanstack/react-query": "^5.83.0"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "^2.1.2",
|
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/modifiers": "^9.0.0",
|
"@dnd-kit/modifiers": "^9.0.0",
|
||||||
"@modelcontextprotocol/sdk": "1.13.3",
|
"@modelcontextprotocol/sdk": "1.13.3",
|
||||||
@@ -126,67 +127,6 @@
|
|||||||
"vscode": "^1.93.0"
|
"vscode": "^1.93.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"apps/extension/node_modules/@biomejs/biome": {
|
|
||||||
"version": "2.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.1.2.tgz",
|
|
||||||
"integrity": "sha512-yq8ZZuKuBVDgAS76LWCfFKHSYIAgqkxVB3mGVVpOe2vSkUTs7xG46zXZeNPRNVjiJuw0SZ3+J2rXiYx0RUpfGg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT OR Apache-2.0",
|
|
||||||
"bin": {
|
|
||||||
"biome": "bin/biome"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14.21.3"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/biome"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@biomejs/cli-darwin-arm64": "2.1.2",
|
|
||||||
"@biomejs/cli-darwin-x64": "2.1.2",
|
|
||||||
"@biomejs/cli-linux-arm64": "2.1.2",
|
|
||||||
"@biomejs/cli-linux-arm64-musl": "2.1.2",
|
|
||||||
"@biomejs/cli-linux-x64": "2.1.2",
|
|
||||||
"@biomejs/cli-linux-x64-musl": "2.1.2",
|
|
||||||
"@biomejs/cli-win32-arm64": "2.1.2",
|
|
||||||
"@biomejs/cli-win32-x64": "2.1.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"apps/extension/node_modules/@biomejs/cli-darwin-arm64": {
|
|
||||||
"version": "2.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.1.2.tgz",
|
|
||||||
"integrity": "sha512-leFAks64PEIjc7MY/cLjE8u5OcfBKkcDB0szxsWUB4aDfemBep1WVKt0qrEyqZBOW8LPHzrFMyDl3FhuuA0E7g==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT OR Apache-2.0",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14.21.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"apps/extension/node_modules/@biomejs/cli-linux-x64": {
|
|
||||||
"version": "2.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.1.2.tgz",
|
|
||||||
"integrity": "sha512-Km/UYeVowygTjpX6sGBzlizjakLoMQkxWbruVZSNE6osuSI63i4uCeIL+6q2AJlD3dxoiBJX70dn1enjQnQqwA==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT OR Apache-2.0",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14.21.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"apps/extension/node_modules/@dnd-kit/core": {
|
"apps/extension/node_modules/@dnd-kit/core": {
|
||||||
"version": "6.3.1",
|
"version": "6.3.1",
|
||||||
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
|
||||||
@@ -2986,57 +2926,6 @@
|
|||||||
"node": ">=14.21.3"
|
"node": ">=14.21.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@biomejs/cli-darwin-x64": {
|
|
||||||
"version": "2.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.1.2.tgz",
|
|
||||||
"integrity": "sha512-Nmmv7wRX5Nj7lGmz0FjnWdflJg4zii8Ivruas6PBKzw5SJX/q+Zh2RfnO+bBnuKLXpj8kiI2x2X12otpH6a32A==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT OR Apache-2.0",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14.21.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@biomejs/cli-linux-arm64": {
|
|
||||||
"version": "2.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.1.2.tgz",
|
|
||||||
"integrity": "sha512-NWNy2Diocav61HZiv2enTQykbPP/KrA/baS7JsLSojC7Xxh2nl9IczuvE5UID7+ksRy2e7yH7klm/WkA72G1dw==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT OR Apache-2.0",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14.21.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@biomejs/cli-linux-arm64-musl": {
|
|
||||||
"version": "2.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.1.2.tgz",
|
|
||||||
"integrity": "sha512-qgHvafhjH7Oca114FdOScmIKf1DlXT1LqbOrrbR30kQDLFPEOpBG0uzx6MhmsrmhGiCFCr2obDamu+czk+X0HQ==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT OR Apache-2.0",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14.21.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@biomejs/cli-linux-x64": {
|
"node_modules/@biomejs/cli-linux-x64": {
|
||||||
"version": "1.9.4",
|
"version": "1.9.4",
|
||||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-1.9.4.tgz",
|
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-1.9.4.tgz",
|
||||||
@@ -3053,57 +2942,6 @@
|
|||||||
"node": ">=14.21.3"
|
"node": ">=14.21.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@biomejs/cli-linux-x64-musl": {
|
|
||||||
"version": "2.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.1.2.tgz",
|
|
||||||
"integrity": "sha512-xlB3mU14ZUa3wzLtXfmk2IMOGL+S0aHFhSix/nssWS/2XlD27q+S6f0dlQ8WOCbYoXcuz8BCM7rCn2lxdTrlQA==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT OR Apache-2.0",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14.21.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@biomejs/cli-win32-arm64": {
|
|
||||||
"version": "2.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.1.2.tgz",
|
|
||||||
"integrity": "sha512-G8KWZli5ASOXA3yUQgx+M4pZRv3ND16h77UsdunUL17uYpcL/UC7RkWTdkfvMQvogVsAuz5JUcBDjgZHXxlKoA==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT OR Apache-2.0",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14.21.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@biomejs/cli-win32-x64": {
|
|
||||||
"version": "2.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.1.2.tgz",
|
|
||||||
"integrity": "sha512-9zajnk59PMpjBkty3bK2IrjUsUHvqe9HWwyAWQBjGLE7MIBjbX2vwv1XPEhmO2RRuGoTkVx3WCanHrjAytICLA==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT OR Apache-2.0",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14.21.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@changesets/apply-release-plan": {
|
"node_modules/@changesets/apply-release-plan": {
|
||||||
"version": "7.0.12",
|
"version": "7.0.12",
|
||||||
"resolved": "https://registry.npmjs.org/@changesets/apply-release-plan/-/apply-release-plan-7.0.12.tgz",
|
"resolved": "https://registry.npmjs.org/@changesets/apply-release-plan/-/apply-release-plan-7.0.12.tgz",
|
||||||
@@ -7103,6 +6941,32 @@
|
|||||||
"tailwindcss": "4.1.11"
|
"tailwindcss": "4.1.11"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tanstack/query-core": {
|
||||||
|
"version": "5.83.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.83.0.tgz",
|
||||||
|
"integrity": "sha512-0M8dA+amXUkyz5cVUm/B+zSk3xkQAcuXuz5/Q/LveT4ots2rBpPTZOzd7yJa2Utsf8D2Upl5KyjhHRY+9lB/XA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tanstack/react-query": {
|
||||||
|
"version": "5.83.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.83.0.tgz",
|
||||||
|
"integrity": "sha512-/XGYhZ3foc5H0VM2jLSD/NyBRIOK4q9kfeml4+0x2DlL6xVuAcVEW+hTlTapAmejObg0i3eNqhkr2dT+eciwoQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@tanstack/query-core": "5.83.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/tannerlinsley"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^18 || ^19"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@tokenizer/inflate": {
|
"node_modules/@tokenizer/inflate": {
|
||||||
"version": "0.2.7",
|
"version": "0.2.7",
|
||||||
"resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.7.tgz",
|
||||||
@@ -10866,6 +10730,10 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/extension": {
|
||||||
|
"resolved": "apps/extension",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
"node_modules/external-editor": {
|
"node_modules/external-editor": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz",
|
||||||
@@ -19331,10 +19199,6 @@
|
|||||||
"resolved": "",
|
"resolved": "",
|
||||||
"link": true
|
"link": true
|
||||||
},
|
},
|
||||||
"node_modules/taskr": {
|
|
||||||
"resolved": "apps/extension",
|
|
||||||
"link": true
|
|
||||||
},
|
|
||||||
"node_modules/term-size": {
|
"node_modules/term-size": {
|
||||||
"version": "2.2.1",
|
"version": "2.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.1.tgz",
|
||||||
|
|||||||
Reference in New Issue
Block a user