diff --git a/src/components/ChatBox/ProjectSection.tsx b/src/components/ChatBox/ProjectSection.tsx index 3bd3c5f2d..a35290646 100644 --- a/src/components/ChatBox/ProjectSection.tsx +++ b/src/components/ChatBox/ProjectSection.tsx @@ -23,7 +23,39 @@ export const ProjectSection = React.forwardRef { - const chatState = chatStore.getState(); + // Subscribe to store changes with throttling to prevent excessive re-renders + const [chatState, setChatState] = React.useState(() => chatStore.getState()); + + React.useEffect(() => { + let timeoutId: NodeJS.Timeout | null = null; + let latestState: any = null; + + const unsubscribe = chatStore.subscribe((state) => { + latestState = state; + + // Throttle updates to max once per 100ms + if (!timeoutId) { + timeoutId = setTimeout(() => { + if (latestState) { + setChatState(latestState); + } + timeoutId = null; + }, 100); + } + }); + + return () => { + unsubscribe(); + if (timeoutId) { + clearTimeout(timeoutId); + // Apply final state on cleanup + if (latestState) { + setChatState(latestState); + } + } + }; + }, [chatStore]); + const activeTaskId = chatState.activeTaskId; if (!activeTaskId || !chatState.tasks[activeTaskId]) { @@ -33,8 +65,17 @@ export const ProjectSection = React.forwardRef { + // Only re-compute when message count or last message changes + const lastMsg = messages[messages.length - 1]; + return `${messages.length}-${lastMsg?.id || ''}-${lastMsg?.content?.length || 0}`; + }, [messages.length, messages[messages.length - 1]?.id, messages[messages.length - 1]?.content?.length]); + + // Memoize grouping to prevent re-creating objects on every render + const queryGroups = React.useMemo(() => { + return groupMessagesByQuery(messages); + }, [messagesKey, messages]); return ( = ({ const chatState = chatStore.getState(); const activeTaskId = chatState.activeTaskId; + // Subscribe to streaming decompose text separately for efficient updates + const streamingDecomposeText = useSyncExternalStore( + (callback) => chatStore.subscribe(callback), + () => { + const state = chatStore.getState(); + const taskId = state.activeTaskId; + if (!taskId || !state.tasks[taskId]) return ''; + return state.tasks[taskId].streamingDecomposeText || ''; + } + ); + // Show task if this query group has a task message OR if it's the most recent user query during splitting // During splitting phase (no to_sub_tasks yet), show task for the most recent query only // Exclude human-reply scenarios (when user is replying to an activeAsk) - const isHumanReply = queryGroup.userMessage && + const isHumanReply = queryGroup.userMessage && activeTaskId && chatState.tasks[activeTaskId] && - (chatState.tasks[activeTaskId].activeAsk || - // Check if this user message follows an 'ask' message in the message sequence - (() => { - const messages = chatState.tasks[activeTaskId].messages; - const userMessageIndex = messages.findIndex((m: any) => m.id === queryGroup.userMessage.id); - if (userMessageIndex > 0) { - // Check the previous message - if it's an agent message with step 'ask', this is a human-reply - const prevMessage = messages[userMessageIndex - 1]; - return prevMessage?.role === 'agent' && prevMessage?.step === 'ask'; - } - return false; - })()); + (chatState.tasks[activeTaskId].activeAsk || + // Check if this user message follows an 'ask' message in the message sequence + (() => { + const messages = chatState.tasks[activeTaskId].messages; + const userMessageIndex = messages.findIndex((m: any) => m.id === queryGroup.userMessage.id); + if (userMessageIndex > 0) { + // Check the previous message - if it's an agent message with step 'ask', this is a human-reply + const prevMessage = messages[userMessageIndex - 1]; + return prevMessage?.role === 'agent' && prevMessage?.step === 'ask'; + } + return false; + })()); const isLastUserQuery = !queryGroup.taskMessage && !isHumanReply && @@ -68,8 +79,10 @@ export const UserQueryGroup: React.FC = ({ // Only show the fallback task box for the newest query while the agent is still splitting work. // Simple Q&A sessions set hasWaitComfirm to true, so we should not render an empty task box there. + // Also, do not show fallback task if we are currently decomposing (streaming text). + const isDecomposing = streamingDecomposeText.length > 0; const shouldShowFallbackTask = - isLastUserQuery && activeTaskId && !chatState.tasks[activeTaskId].hasWaitComfirm; + isLastUserQuery && activeTaskId && !chatState.tasks[activeTaskId].hasWaitComfirm && !isDecomposing; const task = (queryGroup.taskMessage || shouldShowFallbackTask) && activeTaskId @@ -114,7 +127,7 @@ export const UserQueryGroup: React.FC = ({ sentinel.style.height = '1px'; sentinel.style.pointerEvents = 'none'; sentinel.style.zIndex = '-1'; - + // Insert sentinel before the sticky element taskBoxRef.current.parentNode?.insertBefore(sentinel, taskBoxRef.current); @@ -144,9 +157,9 @@ export const UserQueryGroup: React.FC = ({ const anyToSubTasksMessage = task?.messages.find((m: any) => m.step === "to_sub_tasks"); const isSkeletonPhase = task && ( (task.status !== 'finished' && - !anyToSubTasksMessage && - !task.hasWaitComfirm && - task.messages.length > 0) || + !anyToSubTasksMessage && + !task.hasWaitComfirm && + task.messages.length > 0) || (task.isTakeControl && !anyToSubTasksMessage) ); @@ -156,7 +169,7 @@ export const UserQueryGroup: React.FC = ({ data-query-id={queryGroup.queryId} initial={{ opacity: 0, y: 10 }} animate={{ opacity: 1, y: 0 }} - transition={{ + transition={{ duration: 0.3, delay: index * 0.1 // Stagger animation for multiple groups }} @@ -191,16 +204,16 @@ export const UserQueryGroup: React.FC = ({ > -
= ({ {/* Other Messages */} {queryGroup.otherMessages.map((message) => { - if (message.content.length > 0) { - if (message.step === "end") { - return ( - - {}} - /> - {/* File List */} - {message.fileList && ( -
- {message.fileList.map((file: any, fileIndex: number) => ( - { - chatState.setSelectedFile(activeTaskId as string, file); - chatState.setActiveWorkSpace(activeTaskId as string, "documentWorkSpace"); - }} - className="flex items-center gap-2 bg-message-fill-default rounded-sm px-2 py-1 w-[140px] cursor-pointer hover:bg-message-fill-hover transition-colors" - > -
-
- {file.name.split(".")[0]} -
-
- {file.type} -
-
-
- ))} -
- )} -
- ); - } else if (message.content === "skip") { - return ( - + if (message.content.length > 0) { + if (message.step === "end") { + return ( + {}} - /> - - ); - } else { - return ( - - {}} - attaches={message.attaches} + onTyping={() => { }} /> - - ); - } - } else if (message.step === "end" && message.content === "") { - return ( - + {/* File List */} {message.fileList && ( -
+
{message.fileList.map((file: any, fileIndex: number) => ( = ({ chatState.setSelectedFile(activeTaskId as string, file); chatState.setActiveWorkSpace(activeTaskId as string, "documentWorkSpace"); }} - className="flex items-center gap-2 bg-message-fill-default rounded-2xl px-2 py-1 w-[120px] cursor-pointer hover:bg-message-fill-hover transition-colors" + className="flex items-center gap-2 bg-message-fill-default rounded-sm px-2 py-1 w-[140px] cursor-pointer hover:bg-message-fill-hover transition-colors" > -
-
+
{file.name.split(".")[0]}
@@ -362,30 +297,122 @@ export const UserQueryGroup: React.FC = ({ )} ); + } else if (message.content === "skip") { + return ( + + { }} + /> + + ); + } else { + return ( + + { }} + attaches={message.attaches} + /> + + ); } + } else if (message.step === "end" && message.content === "") { + return ( + + {message.fileList && ( +
+ {message.fileList.map((file: any, fileIndex: number) => ( + { + chatState.setSelectedFile(activeTaskId as string, file); + chatState.setActiveWorkSpace(activeTaskId as string, "documentWorkSpace"); + }} + className="flex items-center gap-2 bg-message-fill-default rounded-2xl px-2 py-1 w-[120px] cursor-pointer hover:bg-message-fill-hover transition-colors" + > + +
+
+ {file.name.split(".")[0]} +
+
+ {file.type} +
+
+
+ ))} +
+ )} +
+ ); + } - // Notice Card - if ( - message.step === "notice_card" && - !task?.isTakeControl && - task?.cotList && task.cotList.length > 0 - ) { - return ; - } + // Notice Card + if ( + message.step === "notice_card" && + !task?.isTakeControl && + task?.cotList && task.cotList.length > 0 + ) { + return ; + } - return null; - })} + return null; + })} - {/* Skeleton for loading state */} - {isSkeletonPhase && ( - - - - )} + {/* Streaming Decompose Text - rendered separately to avoid flickering */} + {isLastUserQuery && streamingDecomposeText && ( +
+
+ Splitting Tasks +
+
+
+              {streamingDecomposeText}
+            
+
+
+ )} + + {/* Skeleton for loading state */} + {isSkeletonPhase && ( + + + + )} ); }; diff --git a/src/store/chatStore.ts b/src/store/chatStore.ts index 20882b8f2..ee36c6b43 100644 --- a/src/store/chatStore.ts +++ b/src/store/chatStore.ts @@ -42,6 +42,8 @@ interface Task { isTakeControl: boolean; isTaskEdit: boolean; isContextExceeded?: boolean; + // Streaming decompose text - stored separately to avoid frequent re-renders + streamingDecomposeText: string; } export interface ChatStore { @@ -56,9 +58,10 @@ export interface ChatStore { setActiveTaskId: (taskId: string) => void; replay: (taskId: string, question: string, time: number) => Promise; startTask: (taskId: string, type?: string, shareToken?: string, delayTime?: number, messageContent?: string, messageAttaches?: File[]) => Promise; - handleConfirmTask: (project_id:string, taskId: string, type?: string) => void; + handleConfirmTask: (project_id: string, taskId: string, type?: string) => void; addMessages: (taskId: string, messages: Message) => void; setMessages: (taskId: string, messages: Message[]) => void; + updateMessage: (taskId: string, messageId: string, message: Message) => void; removeMessage: (taskId: string, messageId: string) => void; setAttaches: (taskId: string, attaches: File[]) => void; setSummaryTask: (taskId: string, summaryTask: string) => void; @@ -102,6 +105,8 @@ export interface ChatStore { clearTasks: () => void, setIsContextExceeded: (taskId: string, isContextExceeded: boolean) => void; setNextTaskId: (taskId: string | null) => void; + setStreamingDecomposeText: (taskId: string, text: string) => void; + clearStreamingDecomposeText: (taskId: string) => void; } export type VanillaChatStore = { @@ -118,6 +123,10 @@ const autoConfirmTimers: Record> = {}; // Track active SSE connections for proper cleanup const activeSSEControllers: Record = {}; +// Throttle streaming decompose text updates to prevent excessive re-renders +const streamingDecomposeTextBuffer: Record = {}; +const streamingDecomposeTextTimers: Record> = {}; + const chatStore = (initial?: Partial) => createStore()( (set, get) => ({ activeTaskId: null, @@ -162,6 +171,7 @@ const chatStore = (initial?: Partial) => createStore()( snapshotsTemp: [], isTakeControl: false, isTaskEdit: false, + streamingDecomposeText: '', }, } })) @@ -212,6 +222,27 @@ const chatStore = (initial?: Partial) => createStore()( }) }) }, + updateMessage(taskId: string, messageId: string, message: Message) { + set((state) => { + const task = state.tasks[taskId]; + if (!task) return state; + const messages = task.messages.map((m) => { + if (m.id === messageId) { + return message; + } + return m; + }); + return { + tasks: { + ...state.tasks, + [taskId]: { + ...task, + messages, + }, + }, + }; + }); + }, stopTask(taskId: string) { // Abort the SSE connection for this task try { @@ -305,7 +336,7 @@ const chatStore = (initial?: Partial) => createStore()( /** * Replay creates its own chatStore for each task with replayProject */ - if(project_id && type !== "replay") { + if (project_id && type !== "replay") { console.log("Creating a new Chat Instance for current project on end") const newChatResult = projectStore.appendInitChatStore(project_id); @@ -313,7 +344,7 @@ const chatStore = (initial?: Partial) => createStore()( newTaskId = newChatResult.taskId; targetChatStore = newChatResult.chatStore; targetChatStore.getState().setIsPending(newTaskId, true); - + //From handleSend if message is given // Add the message to the new chatStore if provided if (messageContent) { @@ -329,11 +360,11 @@ const chatStore = (initial?: Partial) => createStore()( } const base_Url = import.meta.env.DEV ? import.meta.env.VITE_PROXY_URL : import.meta.env.VITE_BASE_URL - const api = type == 'share' ? - `${base_Url}/api/chat/share/playback/${shareToken}?delay_time=${delayTime}` - : type == 'replay' ? - `${base_Url}/api/chat/steps/playback/${newTaskId}?delay_time=${delayTime}` - : `${baseURL}/chat` + const api = type == 'share' ? + `${base_Url}/api/chat/share/playback/${shareToken}?delay_time=${delayTime}` + : type == 'replay' ? + `${base_Url}/api/chat/steps/playback/${newTaskId}?delay_time=${delayTime}` + : `${baseURL}/chat` const { tasks } = get() let historyId: string | null = projectStore.getHistoryId(project_id); @@ -387,9 +418,9 @@ const chatStore = (initial?: Partial) => createStore()( apiModel = { api_key: res.value, model_type: cloud_model_type, - model_platform: cloud_model_type.includes('gpt') ? 'openai' : - cloud_model_type.includes('claude') ? 'anthropic' : - cloud_model_type.includes('gemini') ? 'gemini' : 'openai-compatible-model', + model_platform: cloud_model_type.includes('gpt') ? 'openai' : + cloud_model_type.includes('claude') ? 'anthropic' : + cloud_model_type.includes('gemini') ? 'gemini' : 'openai-compatible-model', api_url: res.api_url, extra_params: {} } @@ -451,7 +482,7 @@ const chatStore = (initial?: Partial) => createStore()( } catch (error) { console.log('get-env-path error', error) } - + // create history if (!type) { const authStore = getAuthStore(); @@ -478,11 +509,11 @@ const chatStore = (initial?: Partial) => createStore()( * TODO(history): Remove historyId handling to support per projectId * instead in history api */ - if(project_id && historyId) projectStore.setHistoryId(project_id, historyId); + if (project_id && historyId) projectStore.setHistoryId(project_id, historyId); }) } const browser_port = await window.ipcRenderer.invoke('get-browser-port'); - + // Lock the chatStore reference at the start of SSE session to prevent focus changes // during active message processing let lockedChatStore = targetChatStore; @@ -577,8 +608,8 @@ const chatStore = (initial?: Partial) => createStore()( // - Task switching: confirmed, new_task_state, end // - Multi-turn simple answer: wait_confirm const isTaskSwitchingEvent = agentMessages.step === "confirmed" || - agentMessages.step === "new_task_state" || - agentMessages.step === "end"; + agentMessages.step === "new_task_state" || + agentMessages.step === "end"; const isMultiTurnSimpleAnswer = agentMessages.step === "wait_confirm"; @@ -612,78 +643,78 @@ const chatStore = (initial?: Partial) => createStore()( */ let currentTaskId = getCurrentTaskId(); const previousChatStore = getCurrentChatStore() - if(agentMessages.step === "confirmed") { + if (agentMessages.step === "confirmed") { const { question } = agentMessages.data; const shouldCreateNewChat = project_id && (question || messageContent); - + //All except first confirmed event to reuse the existing chatStore - if(shouldCreateNewChat && !skipFirstConfirm) { - /** - * For Tasks where appended to existing project by - * reusing same projectId. Need to create new chatStore - * as it has been skipped earlier in startTask. - */ - const nextTaskId = previousChatStore.nextTaskId || undefined; - const newChatResult = projectStore.appendInitChatStore(project_id || projectStore.activeProjectId!, nextTaskId); - - if (newChatResult) { - const { taskId: newTaskId, chatStore: newChatStore } = newChatResult; - - // Update references for both scenarios - updateLockedReferences(newChatStore, newTaskId); - newChatStore.getState().setIsPending(newTaskId, false); - - if(type === "replay") { - newChatStore.getState().setDelayTime(newTaskId, delayTime as number); - newChatStore.getState().setType(newTaskId, "replay"); - } + if (shouldCreateNewChat && !skipFirstConfirm) { + /** + * For Tasks where appended to existing project by + * reusing same projectId. Need to create new chatStore + * as it has been skipped earlier in startTask. + */ + const nextTaskId = previousChatStore.nextTaskId || undefined; + const newChatResult = projectStore.appendInitChatStore(project_id || projectStore.activeProjectId!, nextTaskId); - const lastMessage = previousChatStore.tasks[currentTaskId]?.messages.at(-1); - if(lastMessage?.role === "user" && lastMessage?.id) { - previousChatStore.removeMessage(currentTaskId, lastMessage.id); - } - - //Trick: by the time the question is retrieved from event, - //the last message from previous chatStore is at display - newChatStore.getState().addMessages(newTaskId, { - id: generateUniqueId(), - role: "user", - content: question || messageContent as string, - //TODO: The attaches that reach here (when Improve API is called) doesn't reach the backend - attaches: [...(previousChatStore.tasks[currentTaskId]?.attaches || []), ...(messageAttaches || [])], - }); - console.log("[NEW CHATSTORE] Created for ", project_id); + if (newChatResult) { + const { taskId: newTaskId, chatStore: newChatStore } = newChatResult; - //Create a new history point - if (!type) { - const authStore = getAuthStore(); + // Update references for both scenarios + updateLockedReferences(newChatStore, newTaskId); + newChatStore.getState().setIsPending(newTaskId, false); - const obj = { - "project_id": project_id, - "task_id": newTaskId, - "user_id": authStore.user_id, - "question": question || messageContent || (targetChatStore.getState().tasks[newTaskId]?.messages[0]?.content ?? ''), - "language": systemLanguage, - "model_platform": apiModel.model_platform, - "model_type": apiModel.model_type, - "api_url": modelType === 'cloud' ? "cloud" : apiModel.api_url, - "max_retries": 3, - "file_save_path": "string", - "installed_mcp": "string", - "status": 1, - "tokens": 0 - } - await proxyFetchPost(`/api/chat/history`, obj).then(res => { - historyId = res.id; - - /**Save history id for replay reuse purposes. - * TODO(history): Remove historyId handling to support per projectId - * instead in history api - */ - if(project_id && historyId) projectStore.setHistoryId(project_id, historyId); - }) - } + if (type === "replay") { + newChatStore.getState().setDelayTime(newTaskId, delayTime as number); + newChatStore.getState().setType(newTaskId, "replay"); } + + const lastMessage = previousChatStore.tasks[currentTaskId]?.messages.at(-1); + if (lastMessage?.role === "user" && lastMessage?.id) { + previousChatStore.removeMessage(currentTaskId, lastMessage.id); + } + + //Trick: by the time the question is retrieved from event, + //the last message from previous chatStore is at display + newChatStore.getState().addMessages(newTaskId, { + id: generateUniqueId(), + role: "user", + content: question || messageContent as string, + //TODO: The attaches that reach here (when Improve API is called) doesn't reach the backend + attaches: [...(previousChatStore.tasks[currentTaskId]?.attaches || []), ...(messageAttaches || [])], + }); + console.log("[NEW CHATSTORE] Created for ", project_id); + + //Create a new history point + if (!type) { + const authStore = getAuthStore(); + + const obj = { + "project_id": project_id, + "task_id": newTaskId, + "user_id": authStore.user_id, + "question": question || messageContent || (targetChatStore.getState().tasks[newTaskId]?.messages[0]?.content ?? ''), + "language": systemLanguage, + "model_platform": apiModel.model_platform, + "model_type": apiModel.model_type, + "api_url": modelType === 'cloud' ? "cloud" : apiModel.api_url, + "max_retries": 3, + "file_save_path": "string", + "installed_mcp": "string", + "status": 1, + "tokens": 0 + } + await proxyFetchPost(`/api/chat/history`, obj).then(res => { + historyId = res.id; + + /**Save history id for replay reuse purposes. + * TODO(history): Remove historyId handling to support per projectId + * instead in history api + */ + if (project_id && historyId) projectStore.setHistoryId(project_id, historyId); + }) + } + } } else { //NOTE: Triggered only with first "confirmed" in the project //Handle Original cases - with old chatStore @@ -696,18 +727,18 @@ const chatStore = (initial?: Partial) => createStore()( return } - const { - setNuwFileNum, - setCotList, - getTokens, - setUpdateCount, - addTokens, - setStatus, - addWebViewUrl, - setIsPending, - addMessages, - setHasWaitComfirm, - setSummaryTask, + const { + setNuwFileNum, + setCotList, + getTokens, + setUpdateCount, + addTokens, + setStatus, + addWebViewUrl, + setIsPending, + addMessages, + setHasWaitComfirm, + setSummaryTask, setTaskAssigning, setTaskInfo, setTaskRunning, @@ -721,11 +752,50 @@ const chatStore = (initial?: Partial) => createStore()( setElapsed, setActiveTaskId, setIsContextExceeded, - setIsTaskEdit} = getCurrentChatStore() + setIsTaskEdit } = getCurrentChatStore() currentTaskId = getCurrentTaskId(); // if (tasks[currentTaskId].status === 'finished') return + if (agentMessages.step === "decompose_text") { + const { content } = agentMessages.data; + const text = content; + const currentId = getCurrentTaskId(); + + // Get current buffer or task state + const currentContent = streamingDecomposeTextBuffer[currentId] || + getCurrentChatStore().tasks[currentId]?.streamingDecomposeText || ""; + const newContent = text || ""; + let updatedContent = newContent; + + if (newContent.startsWith(currentContent)) { + // Accumulated format: new content contains old content -> Replace + updatedContent = newContent; + } else { + // Delta format: new content is a chunk -> Append + updatedContent = currentContent + newContent; + } + + // Store in buffer immediately + streamingDecomposeTextBuffer[currentId] = updatedContent; + + // Throttle store updates to every 50ms for smoother streaming display + if (!streamingDecomposeTextTimers[currentId]) { + streamingDecomposeTextTimers[currentId] = setTimeout(() => { + const bufferedText = streamingDecomposeTextBuffer[currentId]; + if (bufferedText !== undefined) { + getCurrentChatStore().setStreamingDecomposeText(currentId, bufferedText); + } + delete streamingDecomposeTextTimers[currentId]; + }, 16); + } + return; + } + if (agentMessages.step === "to_sub_tasks") { + // Clear streaming decompose text when task splitting is done + const currentStore = getCurrentChatStore(); + const currentId = getCurrentTaskId(); + currentStore.clearStreamingDecomposeText(currentId); // Check if this is a multi-turn scenario after task completion const isMultiTurnAfterCompletion = tasks[currentTaskId].status === 'finished'; @@ -863,18 +933,18 @@ const chatStore = (initial?: Partial) => createStore()( return; } if (agentMessages.step === "wait_confirm") { - const {content, question} = agentMessages.data; + const { content, question } = agentMessages.data; setHasWaitComfirm(currentTaskId, true) setIsPending(currentTaskId, false) const currentChatStore = getCurrentChatStore(); //Make sure to add user Message on replay and avoid duplication of first msg - if(question && !(currentChatStore.tasks[currentTaskId].messages.length === 1)) { + if (question && !(currentChatStore.tasks[currentTaskId].messages.length === 1)) { //Replace the optimistic update if existent. const lastMessage = currentChatStore.tasks[currentTaskId]?.messages.at(-1); - if(lastMessage?.role === "user" && lastMessage.id && lastMessage.content === question) { + if (lastMessage?.role === "user" && lastMessage.id && lastMessage.content === question) { currentChatStore.removeMessage(currentTaskId, lastMessage.id) - } + } addMessages(currentTaskId, { id: generateUniqueId(), role: "user", @@ -1075,7 +1145,7 @@ const chatStore = (initial?: Partial) => createStore()( if (target) { const { agentIndex, taskIndex } = target const agentName = taskAssigning.find((agent: Agent) => agent.agent_id === assignee_id)?.name - if(agentName!==taskAssigning[agentIndex].name){ + if (agentName !== taskAssigning[agentIndex].name) { taskAssigning[agentIndex].tasks[taskIndex].reAssignTo = agentName } } @@ -1126,7 +1196,7 @@ const chatStore = (initial?: Partial) => createStore()( // Only update or add to taskRunning, never duplicate if (taskRunningIndex === -1) { // Task not in taskRunning, add it - if(task){ + if (task) { task.status = taskState === "waiting" ? "waiting" : "running"; } taskRunning!.push( @@ -1322,27 +1392,27 @@ const chatStore = (initial?: Partial) => createStore()( } - if (agentMessages.step === "context_too_long") { - console.error('Context too long:', agentMessages.data) - const currentLength = agentMessages.data.current_length || 0; - const maxLength = agentMessages.data.max_length || 100000; - - // Show toast notification - toast.dismiss(); - toast.error( - `⚠️ Context Limit Exceeded\n\nThe conversation history is too long (${currentLength.toLocaleString()} / ${maxLength.toLocaleString()} characters).\n\nPlease create a new project to continue your work.`, - { - duration: Infinity, - closeButton: true, - } - ); + if (agentMessages.step === "context_too_long") { + console.error('Context too long:', agentMessages.data) + const currentLength = agentMessages.data.current_length || 0; + const maxLength = agentMessages.data.max_length || 100000; - // Set flag to block input and set status to pause - setIsContextExceeded(currentTaskId, true); - setStatus(currentTaskId, "pause"); - uploadLog(currentTaskId, type); - return - } + // Show toast notification + toast.dismiss(); + toast.error( + `⚠️ Context Limit Exceeded\n\nThe conversation history is too long (${currentLength.toLocaleString()} / ${maxLength.toLocaleString()} characters).\n\nPlease create a new project to continue your work.`, + { + duration: Infinity, + closeButton: true, + } + ); + + // Set flag to block input and set status to pause + setIsContextExceeded(currentTaskId, true); + setStatus(currentTaskId, "pause"); + uploadLog(currentTaskId, type); + return + } if (agentMessages.step === "error") { try { @@ -1355,8 +1425,8 @@ const chatStore = (initial?: Partial) => createStore()( // Safely extract error message with fallback chain const errorMessage = agentMessages.data?.message || - (typeof agentMessages.data === 'string' ? agentMessages.data : null) || - 'An error occurred while processing your request'; + (typeof agentMessages.data === 'string' ? agentMessages.data : null) || + 'An error occurred while processing your request'; // Mark all incomplete tasks as failed let taskRunning = [...tasks[currentTaskId].taskRunning]; @@ -1438,10 +1508,10 @@ const chatStore = (initial?: Partial) => createStore()( const taskIdToRemove = agentMessages.data.task_id as string; const projectStore = useProjectStore.getState(); //Remove the task from the queue on error - if(project_id) { + if (project_id) { const project = projectStore.getProjectById(project_id); if (project && project.queuedMessages) { - const messageToRemove = project.queuedMessages.find(msg => + const messageToRemove = project.queuedMessages.find(msg => msg.task_id === taskIdToRemove || msg.content.includes(taskIdToRemove) ); if (messageToRemove) { @@ -1467,7 +1537,7 @@ const chatStore = (initial?: Partial) => createStore()( // Find and remove the message with matching task ID const project = projectStore.getProjectById(project_id); if (project && project.queuedMessages) { - const messageToRemove = project.queuedMessages.find(msg => + const messageToRemove = project.queuedMessages.find(msg => msg.task_id === taskIdToRemove || msg.content.includes(taskIdToRemove) ); if (messageToRemove) { @@ -1773,7 +1843,7 @@ const chatStore = (initial?: Partial) => createStore()( const { create, setHasMessages, addMessages, startTask, setActiveTaskId, handleConfirmTask } = get(); //get project id const project_id = useProjectStore.getState().activeProjectId - if(!project_id) { + if (!project_id) { console.error("Can't replay task because no project id provided") return; } @@ -1996,7 +2066,7 @@ const chatStore = (initial?: Partial) => createStore()( }, })) }, - handleConfirmTask: async (project_id:string, taskId: string, type?: string) => { + handleConfirmTask: async (project_id: string, taskId: string, type?: string) => { const { tasks, setMessages, setActiveWorkSpace, setStatus, setTaskTime, setTaskInfo, setTaskRunning, setIsTaskEdit } = get(); if (!taskId) return; @@ -2022,7 +2092,7 @@ const chatStore = (initial?: Partial) => createStore()( task: taskInfo, }); await fetchPost(`/task/${project_id}/start`, {}); - + setActiveWorkSpace(taskId, 'workflow') setStatus(taskId, 'running') } @@ -2435,6 +2505,43 @@ const chatStore = (initial?: Partial) => createStore()( ...state, nextTaskId: taskId, })) + }, + setStreamingDecomposeText: (taskId, text) => { + set((state) => { + if (!state.tasks[taskId]) return state; + return { + ...state, + tasks: { + ...state.tasks, + [taskId]: { + ...state.tasks[taskId], + streamingDecomposeText: text, + }, + }, + }; + }); + }, + clearStreamingDecomposeText: (taskId) => { + // Clear buffer and any pending timer + delete streamingDecomposeTextBuffer[taskId]; + if (streamingDecomposeTextTimers[taskId]) { + clearTimeout(streamingDecomposeTextTimers[taskId]); + delete streamingDecomposeTextTimers[taskId]; + } + + set((state) => { + if (!state.tasks[taskId]) return state; + return { + ...state, + tasks: { + ...state.tasks, + [taskId]: { + ...state.tasks[taskId], + streamingDecomposeText: '', + }, + }, + }; + }); } }) ); diff --git a/src/types/chatbox.d.ts b/src/types/chatbox.d.ts index 8f392fa18..d494feb58 100644 --- a/src/types/chatbox.d.ts +++ b/src/types/chatbox.d.ts @@ -38,7 +38,7 @@ declare global { toolkitStatus?: AgentStatus; }[]; failure_count?: number; - reAssignTo?:string; + reAssignTo?: string; } interface File { @@ -66,7 +66,7 @@ declare global { log: AgentMessage[]; img?: string[]; activeWebviewIds?: ActiveWebView[]; - tools?:string[]; + tools?: string[]; workerInfo?: { name: string; description: string; @@ -94,13 +94,13 @@ declare global { task_id?: string; summary?: string; agent_name?: string; - attaches?:File[] + attaches?: File[] } interface AgentMessage { step: string; data: { - project_id?:string; + project_id?: string; failure_count?: number; tokens?: number; sub_tasks?: TaskInfo[]; @@ -125,7 +125,8 @@ declare global { tools?: string[]; //Context Length current_length?: number; - max_length?: number + max_length?: number; + text?: string; }; status?: 'running' | 'filled' | 'completed'; }