(null);
useEffect(() => {
setIsExpanded(data.isExpanded);
}, [data.isExpanded]);
+ useEffect(() => {
+ const runningTask = data.agent?.tasks?.find(
+ (task) =>
+ task.status === "running" && task.toolkits && task.toolkits.length > 0
+ );
+
+ if (runningTask && runningTask.id !== lastAutoExpandedTaskIdRef.current) {
+ if (!isExpanded) {
+ setIsExpanded(true);
+ data.onExpandChange(id, true);
+ setSelectedTask(runningTask);
+ }
+ lastAutoExpandedTaskIdRef.current = runningTask.id;
+ }
+ }, [data.agent?.tasks, id, data.onExpandChange, isExpanded]);
+
// manually control node size
useEffect(() => {
if (data.isEditMode) {
@@ -403,7 +420,7 @@ export function Node({ id, data }: NodeProps) {
{/* {JSON.stringify(data.agent)} */}
{agentToolkits[
diff --git a/src/components/update/index.tsx b/src/components/update/index.tsx
index 0d0537c13..7f93e810f 100644
--- a/src/components/update/index.tsx
+++ b/src/components/update/index.tsx
@@ -9,22 +9,8 @@ const Update = () => {
const [isDownloading, setIsDownloading] = useState(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 && !shouldSuppressError(result.error.message)) {
- toast.error(t("update.update-check-failed"), {
- description: result.error.message,
- });
- }
+ const checkUpdate = () => {
+ window.ipcRenderer.invoke("check-update");
};
const onUpdateCanAvailable = useCallback(
@@ -50,10 +36,6 @@ const Update = () => {
const onUpdateError = useCallback(
(_event: Electron.IpcRendererEvent, err: ErrorType) => {
- if (shouldSuppressError(err.message)) {
- console.warn("[update] suppressed updater error:", err.message);
- return;
- }
toast.error(t("update.update-error"), {
description: err.message,
});
diff --git a/src/lib/llm.ts b/src/lib/llm.ts
index fac1cb2a5..7d6739939 100644
--- a/src/lib/llm.ts
+++ b/src/lib/llm.ts
@@ -73,6 +73,15 @@ export const INIT_PROVODERS: Provider[] = [
is_valid: false,
model_type: ""
},
+ {
+ id: 'ModelArk',
+ name: 'ModelArk',
+ apiKey: '',
+ apiHost: 'https://ark.ap-southeast.bytepluses.com/api/v3',
+ description: "ModelArk model configuration.",
+ is_valid: false,
+ model_type: ""
+ },
{
id: 'aws-bedrock',
name: 'AWS Bedrock',
diff --git a/src/store/chatStore.ts b/src/store/chatStore.ts
index 2b475693f..4f00e8e7e 100644
--- a/src/store/chatStore.ts
+++ b/src/store/chatStore.ts
@@ -159,6 +159,8 @@ const resolveProcessTaskIdForToolkitEvent = (
// Throttle streaming decompose text updates to prevent excessive re-renders
const streamingDecomposeTextBuffer: Record = {};
const streamingDecomposeTextTimers: Record> = {};
+// TTFT (Time to First Token) tracking for task decomposition
+const ttftTracking: Record = {};
const chatStore = (initial?: Partial) => createStore()(
(set, get) => ({
@@ -757,6 +759,11 @@ const chatStore = (initial?: Partial) => createStore()(
//Enable it for the rest of current SSE session
skipFirstConfirm = false;
+
+ // Record confirmed time for TTFT tracking
+ const ttftTaskId = getCurrentTaskId();
+ ttftTracking[ttftTaskId] = { confirmedAt: performance.now(), firstTokenLogged: false };
+ console.log(`[TTFT] Task ${ttftTaskId} confirmed at ${new Date().toISOString()}, starting TTFT measurement`);
return
}
@@ -796,6 +803,13 @@ const chatStore = (initial?: Partial) => createStore()(
const text = content;
const currentId = getCurrentTaskId();
+ // Log TTFT (Time to First Token) on first decompose_text event
+ if (ttftTracking[currentId] && !ttftTracking[currentId].firstTokenLogged) {
+ ttftTracking[currentId].firstTokenLogged = true;
+ const ttft = performance.now() - ttftTracking[currentId].confirmedAt;
+ console.log(`%c[TTFT] 🚀 Time to First Token: ${ttft.toFixed(2)}ms - First streaming token received for task ${currentId}`, 'color: #4CAF50; font-weight: bold');
+ }
+
// Get current buffer or task state
const currentContent = streamingDecomposeTextBuffer[currentId] ||
getCurrentChatStore().tasks[currentId]?.streamingDecomposeText || "";
@@ -829,6 +843,8 @@ const chatStore = (initial?: Partial) => createStore()(
if (agentMessages.step === "to_sub_tasks") {
// Clear streaming decompose text when task splitting is done
clearStreamingDecomposeText(currentTaskId);
+ // Clean up TTFT tracking
+ delete ttftTracking[currentTaskId];
// Check if this is a multi-turn scenario after task completion
const isMultiTurnAfterCompletion = tasks[currentTaskId].status === 'finished';
diff --git a/test/mocks/environmentMocks.ts b/test/mocks/environmentMocks.ts
index 92229db0d..51782df53 100644
--- a/test/mocks/environmentMocks.ts
+++ b/test/mocks/environmentMocks.ts
@@ -429,11 +429,19 @@ export function createProcessUtilsMock() {
getVenvsBaseDir: vi.fn(),
cleanupOldVenvs: vi.fn(),
isBinaryExists: vi.fn(),
+ getUvEnv: vi.fn(),
mockState: {} as MockEnvironmentState,
setup: (mockState: MockEnvironmentState) => {
utilsMock.mockState = mockState
+ utilsMock.getUvEnv.mockReturnValue({
+ UV_PYTHON_INSTALL_DIR: `${mockState.system.homedir}/.eigent/cache/uv_python`,
+ UV_TOOL_DIR: `${mockState.system.homedir}/.eigent/cache/uv_tool`,
+ UV_PROJECT_ENVIRONMENT: `${mockState.system.homedir}/.eigent/venvs/backend-mock`,
+ UV_HTTP_TIMEOUT: '300',
+ })
+
utilsMock.getResourcePath.mockReturnValue(
`${mockState.app.appPath}/resources`
)