Merge branch 'hide_slack' of https://github.com/eigent-ai/eigent into hide_slack

This commit is contained in:
Sun Tao 2025-11-21 02:27:45 +08:00
commit 6241deff7f
5 changed files with 58 additions and 189 deletions

View file

@ -56,7 +56,7 @@ const ToolSelect = forwardRef<
const [integrations, setIntegrations] = useState<any[]>([]);
const fetchIntegrationsData = (keyword?: string) => {
proxyFetchGet("/api/config/info").then((res) => {
if (res && typeof res === "object") {
if (res && typeof res === "object" && !res.error) {
const baseURL = getProxyBaseURL();
const list = Object.entries(res)
@ -187,7 +187,13 @@ const ToolSelect = forwardRef<
};
});
setIntegrations(list);
} else {
console.error("Failed to fetch integrations:", res);
setIntegrations([]);
}
}).catch((error) => {
console.error("Error fetching integrations:", error);
setIntegrations([]);
});
};
@ -217,7 +223,16 @@ const ToolSelect = forwardRef<
page: 1,
size: 100,
}).then((res) => {
setAllMcpList(res.items);
// Add defensive check for API errors
if (res && res.items && Array.isArray(res.items)) {
setAllMcpList(res.items);
} else {
console.error("Failed to fetch MCPs:", res);
setAllMcpList([]);
}
}).catch((error) => {
console.error("Error fetching MCPs:", error);
setAllMcpList([]);
});
};
@ -228,7 +243,7 @@ const ToolSelect = forwardRef<
if (Array.isArray(res)) {
ids = res.map((item: any) => item.mcp_id);
dataList = res;
} else if (Array.isArray(res.items)) {
} else if (res && Array.isArray(res.items)) {
ids = res.items.map((item: any) => item.mcp_id);
dataList = res.items;
}
@ -236,14 +251,22 @@ const ToolSelect = forwardRef<
const customMcpList = dataList.filter((item: any) => item.mcp_id === 0);
setCustomMcpList(customMcpList);
}).catch((error) => {
console.error("Error fetching installed MCPs:", error);
setInstalledIds([]);
setCustomMcpList([]);
});
};
// only surface installed MCPs from the market list
useEffect(() => {
if (!installedIds.length) {
// Add defensive check and fix logic: should filter when installedIds has items
if (Array.isArray(allMcpList) && installedIds.length > 0) {
const filtered = allMcpList.filter((item) => installedIds.includes(item.id));
setMcpList(filtered);
} else if (Array.isArray(allMcpList)) {
// If no installed IDs, show empty list instead of all
setMcpList([]);
}
}, [allMcpList, installedIds]);

View file

@ -162,16 +162,11 @@ export default function ChatBox(): JSX.Element {
const isFinished = chatStore.tasks[_taskId as string].status === "finished";
const hasWaitComfirm = chatStore.tasks[_taskId as string]?.hasWaitComfirm;
// Check if this task was manually stopped (finished but without natural completion)
const wasTaskStopped = isFinished && !chatStore.tasks[_taskId as string].messages.some(
m => m.step === "end" // Natural completion has an "end" step message
);
// Continue conversation if:
// 1. Has wait confirm (simple query response) - but not if task was stopped
// 2. Task is naturally finished (complex task completed) - but not if task was stopped
// 1. Has wait confirm (simple query response)
// 2. Task is finished (complex task completed)
// 3. Has any messages but pending (ongoing conversation)
const shouldContinueConversation = (hasWaitComfirm && !wasTaskStopped) || (isFinished && !wasTaskStopped) || (hasMessages && chatStore.tasks[_taskId as string].status === "pending");
const shouldContinueConversation = hasWaitComfirm || isFinished || (hasMessages && chatStore.tasks[_taskId as string].status === "pending");
if (shouldContinueConversation) {
// Check if this is the very first message and task hasn't started
@ -421,38 +416,28 @@ export default function ChatBox(): JSX.Element {
const handleSkip = async () => {
const taskId = chatStore.activeTaskId as string;
setIsPauseResumeLoading(true);
try {
// First, try to notify backend to skip the task
// Skip the current task
await fetchPost(`/chat/${projectStore.activeProjectId}/skip-task`, {
project_id: projectStore.activeProjectId
});
// Only stop local task if backend call succeeds
chatStore.stopTask(taskId);
// Update task status to finished
chatStore.setStatus(taskId, 'finished');
chatStore.setIsPending(taskId, false);
// toast.success("Task skipped successfully", {
// closeButton: true,
// });
toast.success("Task stopped successfully", {
closeButton: true,
});
} catch (error) {
console.error("Failed to skip task:", error);
// If backend call failed, still try to stop local task as fallback
// but with different messaging to user
try {
chatStore.stopTask(taskId);
chatStore.setIsPending(taskId, false);
toast.warning("Task stopped locally, but backend notification failed. Backend task may continue running.", {
closeButton: true,
duration: 5000,
});
} catch (localError) {
console.error("Failed to stop task locally:", localError);
toast.error("Failed to stop task completely. Please refresh the page.", {
closeButton: true,
});
}
toast.error("Failed to skip task", {
closeButton: true,
});
} finally {
setIsPauseResumeLoading(false);
}

View file

@ -335,10 +335,12 @@ export default function ProjectGroup({
<span>{project.total_tokens ? project.total_tokens.toLocaleString() : "0"}</span>
</Tag>
<Tag variant="default" size="sm" className="min-w-10">
<Pin />
<span>{project.task_count}</span>
</Tag>
<TooltipSimple content={t("layout.tasks")}>
<Tag variant="default" size="sm" className="min-w-10">
<Pin />
<span>{project.task_count}</span>
</Tag>
</TooltipSimple>
</div>
{/* End: Status and menu */}
@ -401,4 +403,4 @@ export default function ProjectGroup({
/>
</div>
);
}
}

View file

@ -88,7 +88,9 @@ export default function TaskItem({
`}
>
<div className="flex items-center gap-2 flex-1 min-w-0">
<Pin className="w-4 h-4 text-icon-primary" />
<TooltipSimple content={t("layout.tasks")}>
<Pin className="w-4 h-4 text-icon-primary" />
</TooltipSimple>
<div className="flex flex-col gap-1 flex-1 min-w-0">
<TooltipSimple
@ -206,4 +208,4 @@ export default function TaskItem({
</div>
</div>
);
}
}

View file

@ -51,7 +51,6 @@ export interface ChatStore {
tasks: { [key: string]: Task };
create: (id?: string, type?: any) => string;
removeTask: (taskId: string) => void;
stopTask: (taskId: string) => void;
setStatus: (taskId: string, status: 'running' | 'finished' | 'pending' | 'pause') => void;
setActiveTaskId: (taskId: string) => void;
replay: (taskId: string, question: string, time: number) => Promise<void>;
@ -115,9 +114,6 @@ export type VanillaChatStore = {
// Track auto-confirm timers per task to avoid reusing stale timers across rounds
const autoConfirmTimers: Record<string, ReturnType<typeof setTimeout>> = {};
// Track active SSE connections for proper cleanup
const activeSSEControllers: Record<string, AbortController> = {};
const chatStore = (initial?: Partial<ChatStore>) => createStore<ChatStore>()(
(set, get) => ({
activeTaskId: null,
@ -193,16 +189,6 @@ const chatStore = (initial?: Partial<ChatStore>) => createStore<ChatStore>()(
console.warn('Error clearing auto-confirm timer in removeTask:', error);
}
// Clean up SSE connection if it exists
try {
if (activeSSEControllers[taskId]) {
activeSSEControllers[taskId].abort();
delete activeSSEControllers[taskId];
}
} catch (error) {
console.warn('Error aborting SSE connection in removeTask:', error);
}
set((state) => {
delete state.tasks[taskId];
return ({
@ -212,58 +198,6 @@ const chatStore = (initial?: Partial<ChatStore>) => createStore<ChatStore>()(
})
})
},
stopTask(taskId: string) {
// Abort the SSE connection for this task
try {
if (activeSSEControllers[taskId]) {
console.log(`Stopping SSE connection for task ${taskId}`);
activeSSEControllers[taskId].abort();
delete activeSSEControllers[taskId];
}
} catch (error) {
console.warn('Error aborting SSE connection in stopTask:', error);
// Even if abort fails, still clean up the reference
try {
delete activeSSEControllers[taskId];
} catch (cleanupError) {
console.warn('Error cleaning up SSE controller reference:', cleanupError);
}
}
// Clean up any pending auto-confirm timers
try {
if (autoConfirmTimers[taskId]) {
clearTimeout(autoConfirmTimers[taskId]);
delete autoConfirmTimers[taskId];
}
} catch (error) {
console.warn('Error clearing auto-confirm timer in stopTask:', error);
}
// Update task status to finished - ensure this happens even if cleanup fails
try {
set((state) => {
// Check if task exists before updating
if (!state.tasks[taskId]) {
console.warn(`Task ${taskId} not found when trying to stop it`);
return state;
}
return {
...state,
tasks: {
...state.tasks,
[taskId]: {
...state.tasks[taskId],
status: 'finished'
},
},
};
});
} catch (error) {
console.error('Error updating task status to finished in stopTask:', error);
}
},
startTask: async (taskId: string, type?: string, shareToken?: string, delayTime?: number, messageContent?: string, messageAttaches?: File[]) => {
// ✅ Wait for backend to be ready before starting task (except for replay/share)
if (!type || type === 'normal') {
@ -275,7 +209,7 @@ const chatStore = (initial?: Partial<ChatStore>) => createStore<ChatStore>()(
const { addMessages } = get();
addMessages(taskId, {
id: generateUniqueId(),
role: 'agent',
role: 'system',
content: '❌ Backend service is not ready. Please wait a moment and try again, or restart the application if the problem persists.',
});
return;
@ -487,42 +421,26 @@ const chatStore = (initial?: Partial<ChatStore>) => createStore<ChatStore>()(
// during active message processing
let lockedChatStore = targetChatStore;
let lockedTaskId = newTaskId;
// Create AbortController for this task's SSE connection
// First check if there's already an active SSE connection for this task
if (activeSSEControllers[newTaskId]) {
console.warn(`Task ${newTaskId} already has an active SSE connection, aborting old one`);
try {
activeSSEControllers[newTaskId].abort();
} catch (error) {
console.warn('Error aborting existing SSE connection:', error);
}
delete activeSSEControllers[newTaskId];
}
const abortController = new AbortController();
activeSSEControllers[newTaskId] = abortController;
// Getter functions that use the locked references instead of dynamic ones
const getCurrentChatStore = () => {
return lockedChatStore.getState();
};
// Get the locked task ID - this won't change during the SSE session
const getCurrentTaskId = () => {
return lockedTaskId;
};
// Function to update locked references (only for special cases like replay)
const updateLockedReferences = (newChatStore: VanillaChatStore, newTaskId: string) => {
lockedChatStore = newChatStore;
lockedTaskId = newTaskId;
};
fetchEventSource(api, {
method: !type ? "POST" : "GET",
openWhenHidden: true,
signal: abortController.signal, // Add abort signal for proper cleanup
headers: { "Content-Type": "application/json", "Authorization": type == 'replay' ? `Bearer ${token}` : undefined as unknown as string },
body: !type ? JSON.stringify({
project_id: project_id,
@ -567,32 +485,6 @@ const chatStore = (initial?: Partial<ChatStore>) => createStore<ChatStore>()(
return;
}
// Check if this task has been stopped before processing any message
// But allow messages that switch to new tasks (like confirmed events)
const lockedTaskId = getCurrentTaskId();
const currentTask = getCurrentChatStore().tasks[lockedTaskId];
// Only ignore messages if:
// 1. The task doesn't exist, OR
// 2. The task is finished AND it's not a task-switching event
const isTaskSwitchingEvent = agentMessages.step === "confirmed" ||
agentMessages.step === "new_task_state" ||
agentMessages.step === "end";
// More robust check - only ignore if task doesn't exist OR
// task is finished and it's not a legitimate flow-control event
if (!currentTask) {
console.log(`Task ${lockedTaskId} not found, ignoring SSE message for step: ${agentMessages.step}`);
return;
}
if (currentTask.status === 'finished' && !isTaskSwitchingEvent) {
// Only ignore non-essential messages for finished tasks
// Allow flow control messages through even for finished tasks
console.log(`Ignoring SSE message for finished task ${lockedTaskId}, step: ${agentMessages.step}`);
return;
}
console.log("agentMessages", agentMessages);
const agentNameMap = {
developer_agent: "Developer Agent",
@ -1737,31 +1629,12 @@ const chatStore = (initial?: Partial<ChatStore>) => createStore<ChatStore>()(
// For other errors, log and throw to stop retrying
console.error('[fetchEventSource] Fatal error, stopping connection:', err);
// Clean up AbortController on error with robust error handling
try {
if (activeSSEControllers[newTaskId]) {
delete activeSSEControllers[newTaskId];
console.log(`Cleaned up SSE controller for task ${newTaskId} after error`);
}
} catch (cleanupError) {
console.warn('Error cleaning up AbortController on SSE error:', cleanupError);
}
throw err;
},
// Server closes connection
onclose() {
console.log("SSE connection closed");
// Clean up AbortController when connection closes with robust error handling
try {
if (activeSSEControllers[newTaskId]) {
delete activeSSEControllers[newTaskId];
console.log(`Cleaned up SSE controller for task ${newTaskId} after connection close`);
}
} catch (cleanupError) {
console.warn('Error cleaning up AbortController on SSE close:', cleanupError);
}
console.log("server closed");
},
});
@ -2380,22 +2253,6 @@ const chatStore = (initial?: Partial<ChatStore>) => createStore<ChatStore>()(
console.error('Error during timer cleanup in clearTasks:', error);
}
// Clean up all active SSE connections
try {
Object.keys(activeSSEControllers).forEach(taskId => {
try {
if (activeSSEControllers[taskId]) {
activeSSEControllers[taskId].abort();
delete activeSSEControllers[taskId];
}
} catch (error) {
console.warn(`Error aborting SSE connection for task ${taskId}:`, error);
}
});
} catch (error) {
console.error('Error during SSE cleanup in clearTasks:', error);
}
window.ipcRenderer.invoke('restart-backend')
.then((res) => {
console.log('restart-backend', res)