mirror of
https://github.com/eigent-ai/eigent.git
synced 2026-05-31 13:19:15 +00:00
Merge remote-tracking branch 'origin/main' into fix-grouped-proj-runtime_a7m
This commit is contained in:
commit
cd6176cb46
10 changed files with 224 additions and 41 deletions
|
|
@ -5,7 +5,7 @@ description = "Add your description here"
|
|||
readme = "README.md"
|
||||
requires-python = "==3.10.16"
|
||||
dependencies = [
|
||||
"camel-ai[eigent]==0.2.80a0",
|
||||
"camel-ai[eigent]==0.2.80a3",
|
||||
"fastapi>=0.115.12",
|
||||
"fastapi-babel>=1.0.0",
|
||||
"uvicorn[standard]>=0.34.2",
|
||||
|
|
|
|||
8
backend/uv.lock
generated
8
backend/uv.lock
generated
|
|
@ -249,7 +249,7 @@ dev = [
|
|||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "aiofiles", specifier = ">=24.1.0" },
|
||||
{ name = "camel-ai", extras = ["eigent"], specifier = "==0.2.80a0" },
|
||||
{ name = "camel-ai", extras = ["eigent"], specifier = "==0.2.80a3" },
|
||||
{ name = "debugpy", specifier = ">=1.8.17" },
|
||||
{ name = "fastapi", specifier = ">=0.115.12" },
|
||||
{ name = "fastapi-babel", specifier = ">=1.0.0" },
|
||||
|
|
@ -333,7 +333,7 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "camel-ai"
|
||||
version = "0.2.80a0"
|
||||
version = "0.2.80a3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "astor" },
|
||||
|
|
@ -349,9 +349,9 @@ dependencies = [
|
|||
{ name = "tiktoken" },
|
||||
{ name = "websockets" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8d/b8/0af67d136edb1d2e598986c9cfaa0e616e2fd337765a40dac60809860698/camel_ai-0.2.80a0.tar.gz", hash = "sha256:a7425ecfebc7d5713e058ae5c04d0b108aa934d14fc2bc48786cd9afa84c9a53", size = 1004526, upload-time = "2025-11-19T08:25:44.579Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1c/72/691a6126e062b5c5a24b7c5a116f690be1b50127a359c7d53d13435d27ef/camel_ai-0.2.80a3.tar.gz", hash = "sha256:edda7cb0466a63c4d8f92ae4ea2d11ee6946b69146336176346db3227de747d9", size = 1013184, upload-time = "2025-11-20T18:52:25.016Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/2e/4e0d0d55c13abf7e6ce9699d8639516e879b966bc6b843be1c1704567f9b/camel_ai-0.2.80a0-py3-none-any.whl", hash = "sha256:eae23435fefe8813c8de3f250a449e435593ead651c93f530fa4ce2377109095", size = 1465918, upload-time = "2025-11-19T08:25:42.331Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/2c/a1203a2fa8e432deb4e6a7740a4dff532b79e0129bbf2e60626fca8f4271/camel_ai-0.2.80a3-py3-none-any.whl", hash = "sha256:2f398e648edae57bf23372a9cc812c82bf64f007178cdaf7ba113f2fde6761a6", size = 1473469, upload-time = "2025-11-20T18:52:22.853Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
|
|
|
|||
|
|
@ -1116,7 +1116,7 @@ const startBackendAfterInstall = async () => {
|
|||
|
||||
// ==================== installation lock ====================
|
||||
let isInstallationInProgress = false;
|
||||
let installationLock = Promise.resolve();
|
||||
let installationLock: Promise<PromiseReturnType> = Promise.resolve({ message: "No installation needed", success: true });
|
||||
|
||||
// ==================== window create ====================
|
||||
async function createWindow() {
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ export function update(win: Electron.BrowserWindow) {
|
|||
if (!app.isPackaged) {
|
||||
console.log('[DEV] setFeedURL:', feed)
|
||||
// In development, check for updates but don't fail if it errors
|
||||
autoUpdater.checkForUpdates().catch(err => {
|
||||
autoUpdater.checkForUpdates().catch((err: Error) => {
|
||||
console.log('[DEV] Update check failed (expected in dev environment):', err.message)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -162,11 +162,16 @@ 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)
|
||||
// 2. Task is finished (complex task completed)
|
||||
// 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
|
||||
// 3. Has any messages but pending (ongoing conversation)
|
||||
const shouldContinueConversation = hasWaitComfirm || isFinished || (hasMessages && chatStore.tasks[_taskId as string].status === "pending");
|
||||
const shouldContinueConversation = (hasWaitComfirm && !wasTaskStopped) || (isFinished && !wasTaskStopped) || (hasMessages && chatStore.tasks[_taskId as string].status === "pending");
|
||||
|
||||
if (shouldContinueConversation) {
|
||||
// Check if this is the very first message and task hasn't started
|
||||
|
|
@ -416,28 +421,38 @@ export default function ChatBox(): JSX.Element {
|
|||
const handleSkip = async () => {
|
||||
const taskId = chatStore.activeTaskId as string;
|
||||
setIsPauseResumeLoading(true);
|
||||
|
||||
|
||||
try {
|
||||
// Skip the current task
|
||||
// First, try to notify backend to skip the task
|
||||
await fetchPost(`/chat/${projectStore.activeProjectId}/skip-task`, {
|
||||
project_id: projectStore.activeProjectId
|
||||
});
|
||||
|
||||
// Update task status to finished
|
||||
chatStore.setStatus(taskId, 'finished');
|
||||
// Only stop local task if backend call succeeds
|
||||
chatStore.stopTask(taskId);
|
||||
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);
|
||||
toast.error("Failed to skip task", {
|
||||
closeButton: true,
|
||||
});
|
||||
|
||||
// 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,
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
setIsPauseResumeLoading(false);
|
||||
}
|
||||
|
|
@ -898,4 +913,4 @@ export default function ChatBox(): JSX.Element {
|
|||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ import { TooltipSimple } from "@/components/ui/tooltip";
|
|||
import { CircleAlert, Settings2 } from "lucide-react";
|
||||
import { fetchGet, fetchPost } from "@/api/http";
|
||||
|
||||
import React, { useState, useCallback } from "react";
|
||||
import React, { useState, useCallback, useMemo } from "react";
|
||||
import ellipseIcon from "@/assets/mcp/Ellipse-25.svg";
|
||||
import { MCPEnvDialog } from "@/pages/Setting/components/MCPEnvDialog";
|
||||
import { OAuth } from "@/lib/oauth";
|
||||
|
|
@ -239,6 +239,7 @@ export default function IntegrationList({
|
|||
);
|
||||
|
||||
const COMING_SOON_ITEMS = [
|
||||
"Slack",
|
||||
"X(Twitter)",
|
||||
"WhatsApp",
|
||||
"LinkedIn",
|
||||
|
|
@ -246,6 +247,12 @@ export default function IntegrationList({
|
|||
"Github",
|
||||
];
|
||||
|
||||
const sortedItems = useMemo(() => {
|
||||
const available = items.filter((item) => !COMING_SOON_ITEMS.includes(item.name));
|
||||
const comingSoon = items.filter((item) => COMING_SOON_ITEMS.includes(item.name));
|
||||
return [...available, ...comingSoon];
|
||||
}, [items]);
|
||||
|
||||
// Determine container and item styles based on variant
|
||||
const containerClassName = isSelectMode
|
||||
? "space-y-3"
|
||||
|
|
@ -267,7 +274,7 @@ export default function IntegrationList({
|
|||
onConnect={onConnect}
|
||||
activeMcp={activeMcp}
|
||||
></MCPEnvDialog>
|
||||
{items.map((item) => {
|
||||
{sortedItems.map((item) => {
|
||||
const isInstalled = !!installed[item.key];
|
||||
const isComingSoon = COMING_SOON_ITEMS.includes(item.name);
|
||||
|
||||
|
|
|
|||
|
|
@ -8,9 +8,19 @@ const Update = () => {
|
|||
const [downloadProgress, setDownloadProgress] = useState<number>(0);
|
||||
const [isDownloading, setIsDownloading] = useState<boolean>(false);
|
||||
const { t } = useTranslation();
|
||||
|
||||
// Some updater errors (e.g. GitHub 503 / missing release) are noisy and not actionable for users.
|
||||
const shouldSuppressError = (message?: string) => {
|
||||
if (!message) return false;
|
||||
const lower = message.toLowerCase();
|
||||
return (
|
||||
lower.includes("unable to find latest version on github")
|
||||
);
|
||||
};
|
||||
|
||||
const checkUpdate = async () => {
|
||||
const result = await window.ipcRenderer.invoke("check-update");
|
||||
if (result?.error) {
|
||||
if (result?.error && !shouldSuppressError(result.error.message)) {
|
||||
toast.error(t("update.update-check-failed"), {
|
||||
description: result.error.message,
|
||||
});
|
||||
|
|
@ -40,11 +50,15 @@ const Update = () => {
|
|||
|
||||
const onUpdateError = useCallback(
|
||||
(_event: Electron.IpcRendererEvent, err: ErrorType) => {
|
||||
toast.error(t("update.update-error"), {
|
||||
if (shouldSuppressError(err.message)) {
|
||||
console.warn("[update] suppressed updater error:", err.message);
|
||||
return;
|
||||
}
|
||||
toast.error(t("update.update-error"), {
|
||||
description: err.message,
|
||||
});
|
||||
},
|
||||
[]
|
||||
[t]
|
||||
);
|
||||
|
||||
const onDownloadProgress = useCallback(
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ 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>;
|
||||
|
|
@ -114,6 +115,9 @@ 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,
|
||||
|
|
@ -189,6 +193,16 @@ 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 ({
|
||||
|
|
@ -198,6 +212,58 @@ 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') {
|
||||
|
|
@ -209,7 +275,7 @@ const chatStore = (initial?: Partial<ChatStore>) => createStore<ChatStore>()(
|
|||
const { addMessages } = get();
|
||||
addMessages(taskId, {
|
||||
id: generateUniqueId(),
|
||||
role: 'system',
|
||||
role: 'agent',
|
||||
content: '❌ Backend service is not ready. Please wait a moment and try again, or restart the application if the problem persists.',
|
||||
});
|
||||
return;
|
||||
|
|
@ -422,26 +488,42 @@ 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,
|
||||
|
|
@ -486,6 +568,32 @@ 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",
|
||||
|
|
@ -1654,12 +1762,31 @@ 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("server closed");
|
||||
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);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
|
@ -2278,6 +2405,22 @@ 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)
|
||||
|
|
@ -2340,4 +2483,4 @@ const filterMessage = (message: AgentMessage) => {
|
|||
|
||||
export const useChatStore = chatStore;
|
||||
|
||||
export const getToolStore = () => chatStore().getState();
|
||||
export const getToolStore = () => chatStore().getState();
|
||||
Loading…
Add table
Add a link
Reference in a new issue