mirror of
https://github.com/coleam00/Archon.git
synced 2026-01-03 13:19:05 -05:00
Fixed the socket optimistic updates. And the MCP for update task.
This commit is contained in:
@@ -511,10 +511,10 @@ export const TasksTab = ({
|
|||||||
debouncedPersistBatchReorder(tasksToUpdate);
|
debouncedPersistBatchReorder(tasksToUpdate);
|
||||||
}, [tasks, updateTasks, debouncedPersistBatchReorder]);
|
}, [tasks, updateTasks, debouncedPersistBatchReorder]);
|
||||||
|
|
||||||
// Task move function (for board view)
|
// Task move function (for board view) with optimistic UI update
|
||||||
const moveTask = async (taskId: string, newStatus: Task['status']) => {
|
const moveTask = async (taskId: string, newStatus: Task['status']) => {
|
||||||
console.log(`[TasksTab] Attempting to move task ${taskId} to new status: ${newStatus}`);
|
console.log(`[TasksTab] Attempting to move task ${taskId} to new status: ${newStatus}`);
|
||||||
try {
|
|
||||||
const movingTask = tasks.find(task => task.id === taskId);
|
const movingTask = tasks.find(task => task.id === taskId);
|
||||||
if (!movingTask) {
|
if (!movingTask) {
|
||||||
console.warn(`[TasksTab] Task ${taskId} not found for move operation.`);
|
console.warn(`[TasksTab] Task ${taskId} not found for move operation.`);
|
||||||
@@ -523,21 +523,38 @@ export const TasksTab = ({
|
|||||||
|
|
||||||
const oldStatus = movingTask.status;
|
const oldStatus = movingTask.status;
|
||||||
const newOrder = getNextOrderForStatus(newStatus);
|
const newOrder = getNextOrderForStatus(newStatus);
|
||||||
|
const updatedTask = { ...movingTask, status: newStatus, task_order: newOrder };
|
||||||
|
|
||||||
console.log(`[TasksTab] Moving task ${movingTask.title} from ${oldStatus} to ${newStatus} with order ${newOrder}`);
|
console.log(`[TasksTab] Moving task ${movingTask.title} from ${oldStatus} to ${newStatus} with order ${newOrder}`);
|
||||||
|
|
||||||
// Update the task with new status and order
|
// OPTIMISTIC UPDATE: Update UI immediately
|
||||||
|
setTasks(prev => {
|
||||||
|
const updated = prev.map(task => task.id === taskId ? updatedTask : task);
|
||||||
|
setTimeout(() => onTasksChange(updated), 0);
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
console.log(`[TasksTab] Optimistically updated UI for task ${taskId}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Then update the backend
|
||||||
await projectService.updateTask(taskId, {
|
await projectService.updateTask(taskId, {
|
||||||
status: mapUIStatusToDBStatus(newStatus),
|
status: mapUIStatusToDBStatus(newStatus),
|
||||||
task_order: newOrder
|
task_order: newOrder
|
||||||
});
|
});
|
||||||
console.log(`[TasksTab] Successfully updated task ${taskId} status in backend.`);
|
console.log(`[TasksTab] Successfully updated task ${taskId} status in backend.`);
|
||||||
|
|
||||||
// Don't update local state immediately - let socket handle it
|
// Socket will confirm the update, but UI is already updated
|
||||||
console.log(`[TasksTab] Waiting for socket update for task ${taskId}.`);
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[TasksTab] Failed to move task ${taskId}:`, error);
|
console.error(`[TasksTab] Failed to move task ${taskId}, rolling back:`, error);
|
||||||
|
|
||||||
|
// ROLLBACK on error - restore original task
|
||||||
|
setTasks(prev => {
|
||||||
|
const updated = prev.map(task => task.id === taskId ? movingTask : task);
|
||||||
|
setTimeout(() => onTasksChange(updated), 0);
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
|
||||||
alert(`Failed to move task: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
alert(`Failed to move task: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -557,15 +574,50 @@ export const TasksTab = ({
|
|||||||
if (!taskToDelete) return;
|
if (!taskToDelete) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Delete (actually archives) the task - backend will emit socket event
|
// Add to recently deleted cache to prevent race conditions
|
||||||
await projectService.deleteTask(taskToDelete.id);
|
setRecentlyDeletedIds(prev => new Set(prev).add(taskToDelete.id));
|
||||||
console.log(`[TasksTab] Task ${taskToDelete.id} archival sent to backend`);
|
|
||||||
|
|
||||||
// Don't update local state - let socket handle it
|
// OPTIMISTIC UPDATE: Remove task from UI immediately
|
||||||
|
setTasks(prev => {
|
||||||
|
const updated = prev.filter(t => t.id !== taskToDelete.id);
|
||||||
|
setTimeout(() => onTasksChange(updated), 0);
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
console.log(`[TasksTab] Optimistically removed task ${taskToDelete.id} from UI`);
|
||||||
|
|
||||||
|
// Then delete from backend
|
||||||
|
await projectService.deleteTask(taskToDelete.id);
|
||||||
|
console.log(`[TasksTab] Task ${taskToDelete.id} deletion confirmed by backend`);
|
||||||
|
|
||||||
|
// Clear from recently deleted cache after a delay (to catch any lingering socket events)
|
||||||
|
setTimeout(() => {
|
||||||
|
setRecentlyDeletedIds(prev => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
newSet.delete(taskToDelete.id);
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
}, 3000); // 3 second window to ignore stale socket events
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to archive task:', error);
|
console.error('Failed to delete task:', error);
|
||||||
// Note: The toast notification for deletion is now handled by TaskBoardView and TaskTableView
|
|
||||||
|
// Remove from recently deleted cache on error
|
||||||
|
setRecentlyDeletedIds(prev => {
|
||||||
|
const newSet = new Set(prev);
|
||||||
|
newSet.delete(taskToDelete.id);
|
||||||
|
return newSet;
|
||||||
|
});
|
||||||
|
|
||||||
|
// ROLLBACK on error - restore the task
|
||||||
|
setTasks(prev => {
|
||||||
|
const updated = [...prev, taskToDelete].sort((a, b) => a.task_order - b.task_order);
|
||||||
|
setTimeout(() => onTasksChange(updated), 0);
|
||||||
|
return updated;
|
||||||
|
});
|
||||||
|
console.log(`[TasksTab] Rolled back task deletion for ${taskToDelete.id}`);
|
||||||
|
|
||||||
|
// Re-throw to let the calling component handle the error display
|
||||||
|
throw error;
|
||||||
} finally {
|
} finally {
|
||||||
setTaskToDelete(null);
|
setTaskToDelete(null);
|
||||||
setShowDeleteConfirm(false);
|
setShowDeleteConfirm(false);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
* approach that avoids conflicts and connection issues.
|
* approach that avoids conflicts and connection issues.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useEffect, useRef, useCallback } from 'react';
|
import { useEffect, useRef, useCallback, useState } from 'react';
|
||||||
import { taskSocketService, TaskSocketEvents } from '../services/taskSocketService';
|
import { taskSocketService, TaskSocketEvents } from '../services/taskSocketService';
|
||||||
import { WebSocketState } from '../services/socketIOService';
|
import { WebSocketState } from '../services/socketIOService';
|
||||||
|
|
||||||
@@ -37,6 +37,10 @@ export function useTaskSocket(options: UseTaskSocketOptions) {
|
|||||||
const currentProjectIdRef = useRef<string | null>(null);
|
const currentProjectIdRef = useRef<string | null>(null);
|
||||||
const isInitializedRef = useRef<boolean>(false);
|
const isInitializedRef = useRef<boolean>(false);
|
||||||
|
|
||||||
|
// Add reactive state for connection status
|
||||||
|
const [isConnected, setIsConnected] = useState<boolean>(false);
|
||||||
|
const [connectionState, setConnectionState] = useState<WebSocketState>(WebSocketState.DISCONNECTED);
|
||||||
|
|
||||||
// Memoized handlers to prevent unnecessary re-registrations
|
// Memoized handlers to prevent unnecessary re-registrations
|
||||||
const memoizedHandlers = useCallback((): Partial<TaskSocketEvents> => {
|
const memoizedHandlers = useCallback((): Partial<TaskSocketEvents> => {
|
||||||
return {
|
return {
|
||||||
@@ -58,6 +62,44 @@ export function useTaskSocket(options: UseTaskSocketOptions) {
|
|||||||
onConnectionStateChange
|
onConnectionStateChange
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Subscribe to connection state changes
|
||||||
|
useEffect(() => {
|
||||||
|
const checkConnection = () => {
|
||||||
|
const connected = taskSocketService.isConnected();
|
||||||
|
const state = taskSocketService.getConnectionState();
|
||||||
|
setIsConnected(connected);
|
||||||
|
setConnectionState(state);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check initial state
|
||||||
|
checkConnection();
|
||||||
|
|
||||||
|
// Poll for connection state changes (since the service doesn't expose event emitters)
|
||||||
|
const interval = setInterval(checkConnection, 500);
|
||||||
|
|
||||||
|
// Also trigger when connection state handler is called
|
||||||
|
const wrappedOnConnectionStateChange = onConnectionStateChange ? (state: WebSocketState) => {
|
||||||
|
setConnectionState(state);
|
||||||
|
setIsConnected(state === WebSocketState.CONNECTED);
|
||||||
|
onConnectionStateChange(state);
|
||||||
|
} : (state: WebSocketState) => {
|
||||||
|
setConnectionState(state);
|
||||||
|
setIsConnected(state === WebSocketState.CONNECTED);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Update the handler
|
||||||
|
if (componentIdRef.current && taskSocketService) {
|
||||||
|
taskSocketService.registerHandlers(componentIdRef.current, {
|
||||||
|
...memoizedHandlers(),
|
||||||
|
onConnectionStateChange: wrappedOnConnectionStateChange
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
clearInterval(interval);
|
||||||
|
};
|
||||||
|
}, [onConnectionStateChange, memoizedHandlers]);
|
||||||
|
|
||||||
// Initialize connection once and register handlers
|
// Initialize connection once and register handlers
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!projectId || isInitializedRef.current) return;
|
if (!projectId || isInitializedRef.current) return;
|
||||||
@@ -65,6 +107,7 @@ export function useTaskSocket(options: UseTaskSocketOptions) {
|
|||||||
const initializeConnection = async () => {
|
const initializeConnection = async () => {
|
||||||
try {
|
try {
|
||||||
console.log(`[USE_TASK_SOCKET] Initializing connection for project: ${projectId}`);
|
console.log(`[USE_TASK_SOCKET] Initializing connection for project: ${projectId}`);
|
||||||
|
setConnectionState(WebSocketState.CONNECTING);
|
||||||
|
|
||||||
// Register handlers first
|
// Register handlers first
|
||||||
taskSocketService.registerHandlers(componentIdRef.current, memoizedHandlers());
|
taskSocketService.registerHandlers(componentIdRef.current, memoizedHandlers());
|
||||||
@@ -76,8 +119,14 @@ export function useTaskSocket(options: UseTaskSocketOptions) {
|
|||||||
isInitializedRef.current = true;
|
isInitializedRef.current = true;
|
||||||
console.log(`[USE_TASK_SOCKET] Successfully initialized for project: ${projectId}`);
|
console.log(`[USE_TASK_SOCKET] Successfully initialized for project: ${projectId}`);
|
||||||
|
|
||||||
|
// Update connection state after successful connection
|
||||||
|
setIsConnected(taskSocketService.isConnected());
|
||||||
|
setConnectionState(taskSocketService.getConnectionState());
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[USE_TASK_SOCKET] Failed to initialize for project ${projectId}:`, error);
|
console.error(`[USE_TASK_SOCKET] Failed to initialize for project ${projectId}:`, error);
|
||||||
|
setConnectionState(WebSocketState.DISCONNECTED);
|
||||||
|
setIsConnected(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -103,6 +152,8 @@ export function useTaskSocket(options: UseTaskSocketOptions) {
|
|||||||
|
|
||||||
const switchProject = async () => {
|
const switchProject = async () => {
|
||||||
try {
|
try {
|
||||||
|
setConnectionState(WebSocketState.CONNECTING);
|
||||||
|
|
||||||
// Update handlers for new project
|
// Update handlers for new project
|
||||||
taskSocketService.registerHandlers(componentIdRef.current, memoizedHandlers());
|
taskSocketService.registerHandlers(componentIdRef.current, memoizedHandlers());
|
||||||
|
|
||||||
@@ -112,8 +163,14 @@ export function useTaskSocket(options: UseTaskSocketOptions) {
|
|||||||
currentProjectIdRef.current = projectId;
|
currentProjectIdRef.current = projectId;
|
||||||
console.log(`[USE_TASK_SOCKET] Successfully switched to project: ${projectId}`);
|
console.log(`[USE_TASK_SOCKET] Successfully switched to project: ${projectId}`);
|
||||||
|
|
||||||
|
// Update connection state
|
||||||
|
setIsConnected(taskSocketService.isConnected());
|
||||||
|
setConnectionState(taskSocketService.getConnectionState());
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`[USE_TASK_SOCKET] Failed to switch to project ${projectId}:`, error);
|
console.error(`[USE_TASK_SOCKET] Failed to switch to project ${projectId}:`, error);
|
||||||
|
setConnectionState(WebSocketState.DISCONNECTED);
|
||||||
|
setIsConnected(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -132,10 +189,10 @@ export function useTaskSocket(options: UseTaskSocketOptions) {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Return utility functions
|
// Return reactive state and utility functions
|
||||||
return {
|
return {
|
||||||
isConnected: taskSocketService.isConnected(),
|
isConnected, // Now reactive!
|
||||||
connectionState: taskSocketService.getConnectionState(),
|
connectionState, // Now reactive!
|
||||||
reconnect: taskSocketService.reconnect.bind(taskSocketService),
|
reconnect: taskSocketService.reconnect.bind(taskSocketService),
|
||||||
getCurrentProjectId: taskSocketService.getCurrentProjectId.bind(taskSocketService)
|
getCurrentProjectId: taskSocketService.getCurrentProjectId.bind(taskSocketService)
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -678,28 +678,15 @@ export function createWebSocketService(config?: WebSocketConfig): WebSocketServi
|
|||||||
return new WebSocketService(config);
|
return new WebSocketService(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create SEPARATE WebSocket instances for different features
|
// Create a SINGLE shared WebSocket instance to prevent multiple connections
|
||||||
// This prevents a failure in one feature (like a long crawl) from breaking the entire site
|
// This fixes the socket disconnection issue when switching tabs
|
||||||
export const knowledgeSocketIO = new WebSocketService({
|
const sharedSocketInstance = new WebSocketService();
|
||||||
maxReconnectAttempts: 10, // More attempts for crawls that can take a long time
|
|
||||||
reconnectInterval: 2000,
|
|
||||||
heartbeatInterval: 30000,
|
|
||||||
enableAutoReconnect: true
|
|
||||||
});
|
|
||||||
|
|
||||||
export const taskUpdateSocketIO = new WebSocketService({
|
// Export the SAME instance with different names for backward compatibility
|
||||||
maxReconnectAttempts: 5,
|
// This ensures only ONE Socket.IO connection is created and shared across all features
|
||||||
reconnectInterval: 1000,
|
export const knowledgeSocketIO = sharedSocketInstance;
|
||||||
heartbeatInterval: 30000,
|
export const taskUpdateSocketIO = sharedSocketInstance;
|
||||||
enableAutoReconnect: true
|
export const projectListSocketIO = sharedSocketInstance;
|
||||||
});
|
|
||||||
|
|
||||||
export const projectListSocketIO = new WebSocketService({
|
// Export as default for new code
|
||||||
maxReconnectAttempts: 5,
|
export default sharedSocketInstance;
|
||||||
reconnectInterval: 1000,
|
|
||||||
heartbeatInterval: 30000,
|
|
||||||
enableAutoReconnect: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Export knowledgeSocketIO as default for backward compatibility
|
|
||||||
export default knowledgeSocketIO;
|
|
||||||
@@ -13,7 +13,8 @@
|
|||||||
* - Proper session identification
|
* - Proper session identification
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { WebSocketService, WebSocketState } from './socketIOService';
|
import { WebSocketState } from './socketIOService';
|
||||||
|
import sharedSocketInstance from './socketIOService';
|
||||||
|
|
||||||
export interface Task {
|
export interface Task {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -38,7 +39,7 @@ export interface TaskSocketEvents {
|
|||||||
|
|
||||||
class TaskSocketService {
|
class TaskSocketService {
|
||||||
private static instance: TaskSocketService | null = null;
|
private static instance: TaskSocketService | null = null;
|
||||||
private socketService: WebSocketService;
|
private socketService: typeof sharedSocketInstance;
|
||||||
private currentProjectId: string | null = null;
|
private currentProjectId: string | null = null;
|
||||||
private eventHandlers: Map<string, TaskSocketEvents> = new Map();
|
private eventHandlers: Map<string, TaskSocketEvents> = new Map();
|
||||||
private connectionPromise: Promise<void> | null = null;
|
private connectionPromise: Promise<void> | null = null;
|
||||||
@@ -47,13 +48,11 @@ class TaskSocketService {
|
|||||||
private connectionCooldown = 1000; // 1 second cooldown between connection attempts
|
private connectionCooldown = 1000; // 1 second cooldown between connection attempts
|
||||||
|
|
||||||
private constructor() {
|
private constructor() {
|
||||||
this.socketService = new WebSocketService({
|
// Use the shared socket instance instead of creating a new one
|
||||||
maxReconnectAttempts: 5,
|
this.socketService = sharedSocketInstance;
|
||||||
reconnectInterval: 1000,
|
|
||||||
heartbeatInterval: 30000,
|
// Enable operation tracking for echo suppression
|
||||||
enableAutoReconnect: true,
|
this.socketService.enableOperationTracking();
|
||||||
enableHeartbeat: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Set up global event handlers
|
// Set up global event handlers
|
||||||
this.setupGlobalHandlers();
|
this.setupGlobalHandlers();
|
||||||
@@ -191,7 +190,7 @@ class TaskSocketService {
|
|||||||
const joinSuccess = this.socketService.send({
|
const joinSuccess = this.socketService.send({
|
||||||
type: 'join_project',
|
type: 'join_project',
|
||||||
project_id: projectId
|
project_id: projectId
|
||||||
});
|
}, true); // Enable operation tracking
|
||||||
|
|
||||||
if (!joinSuccess) {
|
if (!joinSuccess) {
|
||||||
throw new Error('Failed to send join_project message');
|
throw new Error('Failed to send join_project message');
|
||||||
@@ -214,7 +213,7 @@ class TaskSocketService {
|
|||||||
this.socketService.send({
|
this.socketService.send({
|
||||||
type: 'leave_project',
|
type: 'leave_project',
|
||||||
project_id: this.currentProjectId
|
project_id: this.currentProjectId
|
||||||
});
|
}, true); // Enable operation tracking
|
||||||
|
|
||||||
this.currentProjectId = null;
|
this.currentProjectId = null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ Mirrors the functionality of the original manage_task tool but with individual t
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Dict, List, Optional, TypedDict
|
from typing import Any, Dict, List, Optional
|
||||||
from urllib.parse import urljoin
|
from urllib.parse import urljoin
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
@@ -20,19 +20,6 @@ from src.server.config.service_discovery import get_api_url
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class TaskUpdateFields(TypedDict, total=False):
|
|
||||||
"""Valid fields that can be updated on a task."""
|
|
||||||
|
|
||||||
title: str
|
|
||||||
description: str
|
|
||||||
status: str # "todo" | "doing" | "review" | "done"
|
|
||||||
assignee: str # "User" | "Archon" | "AI IDE Agent" | "prp-executor" | "prp-validator"
|
|
||||||
task_order: int # 0-100, higher = more priority
|
|
||||||
feature: Optional[str]
|
|
||||||
sources: Optional[List[Dict[str, str]]]
|
|
||||||
code_examples: Optional[List[Dict[str, str]]]
|
|
||||||
|
|
||||||
|
|
||||||
def register_task_tools(mcp: FastMCP):
|
def register_task_tools(mcp: FastMCP):
|
||||||
"""Register individual task management tools with the MCP server."""
|
"""Register individual task management tools with the MCP server."""
|
||||||
|
|
||||||
@@ -315,26 +302,66 @@ def register_task_tools(mcp: FastMCP):
|
|||||||
async def update_task(
|
async def update_task(
|
||||||
ctx: Context,
|
ctx: Context,
|
||||||
task_id: str,
|
task_id: str,
|
||||||
update_fields: TaskUpdateFields,
|
title: Optional[str] = None,
|
||||||
|
description: Optional[str] = None,
|
||||||
|
status: Optional[str] = None,
|
||||||
|
assignee: Optional[str] = None,
|
||||||
|
task_order: Optional[int] = None,
|
||||||
|
feature: Optional[str] = None,
|
||||||
|
sources: Optional[List[Dict[str, str]]] = None,
|
||||||
|
code_examples: Optional[List[Dict[str, str]]] = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Update a task's properties.
|
Update a task's properties.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
task_id: UUID of the task to update
|
task_id: UUID of the task to update
|
||||||
update_fields: Dict of fields to update (e.g., {"status": "doing", "assignee": "AI IDE Agent"})
|
title: New task title (optional)
|
||||||
|
description: New task description (optional)
|
||||||
|
status: New status - "todo" | "doing" | "review" | "done" (optional)
|
||||||
|
assignee: New assignee (optional)
|
||||||
|
task_order: New priority order (optional)
|
||||||
|
feature: New feature label (optional)
|
||||||
|
sources: New source references (optional)
|
||||||
|
code_examples: New code examples (optional)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
JSON with updated task details
|
JSON with updated task details
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
update_task(task_id="uuid", update_fields={"status": "doing"})
|
update_task(task_id="uuid", status="doing")
|
||||||
update_task(task_id="uuid", update_fields={"title": "New Title", "description": "Updated description"})
|
update_task(task_id="uuid", title="New Title", description="Updated description")
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
api_url = get_api_url()
|
api_url = get_api_url()
|
||||||
timeout = get_default_timeout()
|
timeout = get_default_timeout()
|
||||||
|
|
||||||
|
# Build update_fields dict from provided parameters
|
||||||
|
update_fields = {}
|
||||||
|
if title is not None:
|
||||||
|
update_fields["title"] = title
|
||||||
|
if description is not None:
|
||||||
|
update_fields["description"] = description
|
||||||
|
if status is not None:
|
||||||
|
update_fields["status"] = status
|
||||||
|
if assignee is not None:
|
||||||
|
update_fields["assignee"] = assignee
|
||||||
|
if task_order is not None:
|
||||||
|
update_fields["task_order"] = task_order
|
||||||
|
if feature is not None:
|
||||||
|
update_fields["feature"] = feature
|
||||||
|
if sources is not None:
|
||||||
|
update_fields["sources"] = sources
|
||||||
|
if code_examples is not None:
|
||||||
|
update_fields["code_examples"] = code_examples
|
||||||
|
|
||||||
|
if not update_fields:
|
||||||
|
return MCPErrorFormatter.format_error(
|
||||||
|
error_type="validation_error",
|
||||||
|
message="No fields provided to update",
|
||||||
|
suggestion="Provide at least one field to update",
|
||||||
|
)
|
||||||
|
|
||||||
async with httpx.AsyncClient(timeout=timeout) as client:
|
async with httpx.AsyncClient(timeout=timeout) as client:
|
||||||
response = await client.put(
|
response = await client.put(
|
||||||
urljoin(api_url, f"/api/tasks/{task_id}"), json=update_fields
|
urljoin(api_url, f"/api/tasks/{task_id}"), json=update_fields
|
||||||
|
|||||||
Reference in New Issue
Block a user