fix: refactor chatStore from tasks dictionary to single task object (#627)

This commit is contained in:
gayanMatch 2026-01-16 19:35:39 -06:00
parent 89a38f815e
commit bf6cb685c5
25 changed files with 1059 additions and 1193 deletions

View file

@ -60,8 +60,8 @@ export function AddWorker({
return <div>Loading...</div>;
}
const activeProjectId = projectStore.activeProjectId;
const activeTaskId = chatStore.activeTaskId;
const tasks = chatStore.tasks;
const activeTaskId = chatStore.taskId;
const task = chatStore.task;
const [showEnvConfig, setShowEnvConfig] = useState(false);
const [activeMcp, setActiveMcp] = useState<McpItem | null>(null);
const [envValues, setEnvValues] = useState<{ [key: string]: EnvValue }>({});
@ -301,7 +301,7 @@ export function AddWorker({
});
setWorkerList(newWorkerList);
} else if (
activeTaskId && tasks[activeTaskId].messages.length === 0
activeTaskId && task && task.messages.length === 0
) {
const worker: Agent = {
tasks: [],

View file

@ -34,7 +34,7 @@ export function NoticeCard() {
}, 100);
}
}, [
chatStore.tasks[chatStore.activeTaskId as string].cotList.length,
chatStore.task?.cotList?.length,
isExpanded,
]);
@ -62,16 +62,16 @@ export function NoticeCard() {
isExpanded ? "overflow-y-auto" : "overflow-y-auto max-h-[200px]"
} transition-all duration-300 ease-in-out scrollbar-hide relative`}
style={{
maskImage: isExpanded
? 'none'
maskImage: isExpanded
? 'none'
: 'linear-gradient(to top, black 0%, black 40%, transparent 100%)',
WebkitMaskImage: isExpanded
? 'none'
WebkitMaskImage: isExpanded
? 'none'
: 'linear-gradient(to top, black 0%, black 40%, transparent 100%)'
}}
>
<div className="mt-sm flex flex-col px-2 gap-2">
{chatStore.tasks[chatStore.activeTaskId as string].cotList.map(
{(chatStore.task?.cotList || []).map(
(cot: string, index: number) => {
return (
<div

View file

@ -47,31 +47,31 @@ export const ProjectChatContainer: React.FC<ProjectChatContainerProps> = ({
useEffect(() => {
if (!chatStore || !activeProjectId) return;
const activeTaskId = chatStore.activeTaskId;
const activeTaskId = chatStore.taskId;
if (!activeTaskId) return;
const task = chatStore.tasks[activeTaskId];
const task = chatStore.task;
if (!task) return;
const currentMessageCount = task.messages.length;
// Check if a new user message was added
if (currentMessageCount > lastMessageCount) {
const lastMessage = task.messages[task.messages.length - 1];
// If the last message is from user, scroll to bottom
if (lastMessage && lastMessage.role === 'user') {
scrollToBottom();
}
}
setLastMessageCount(currentMessageCount);
}, [chatStore?.tasks[chatStore.activeTaskId as string]?.messages, lastMessageCount, scrollToBottom, activeProjectId]);
}, [chatStore?.task?.messages, lastMessageCount, scrollToBottom, activeProjectId]);
// Reset message count when active task changes
useEffect(() => {
setLastMessageCount(0);
}, [chatStore?.activeTaskId]);
}, [chatStore?.taskId]);
// Intersection Observer for scroll-based animations
useEffect(() => {
@ -142,13 +142,13 @@ export const ProjectChatContainer: React.FC<ProjectChatContainerProps> = ({
<AnimatePresence mode="popLayout">
{chatStores.map(({ chatId, chatStore }) => {
const chatState = chatStore.getState();
const activeTaskId = chatState.activeTaskId;
if (!activeTaskId || !chatState.tasks[activeTaskId]) {
const activeTaskId = chatState.taskId;
if (!activeTaskId || !chatState.task) {
return null;
}
const task = chatState.tasks[activeTaskId];
const task = chatState.task;
const messages = task.messages || [];
// Only render if there are actual user messages (not just empty or system messages)

View file

@ -56,13 +56,13 @@ export const ProjectSection = React.forwardRef<HTMLDivElement, ProjectSectionPro
};
}, [chatStore]);
const activeTaskId = chatState.activeTaskId;
const activeTaskId = chatState.taskId;
if (!activeTaskId || !chatState.tasks[activeTaskId]) {
if (!activeTaskId || !chatState.task) {
return null;
}
const task = chatState.tasks[activeTaskId];
const task = chatState.task;
const messages = task.messages || [];
// Create a stable key based on messages content to prevent excessive re-renders

View file

@ -101,20 +101,20 @@ export function TaskCard({
const isAllTaskFinished = useMemo(() => {
return (
chatStore.tasks[chatStore.activeTaskId as string].status === "finished"
chatStore.task?.status === "finished"
);
}, [chatStore.tasks[chatStore.activeTaskId as string].status]);
}, [chatStore.task?.status]);
useEffect(() => {
if (
chatStore.tasks[chatStore.activeTaskId as string].activeWorkSpace ===
chatStore.task?.activeWorkSpace ===
"workflow"
) {
setIsExpanded(false);
} else {
setIsExpanded(true);
}
}, [chatStore.tasks[chatStore.activeTaskId as string].activeWorkSpace]);
}, [chatStore.task?.activeWorkSpace]);
// Improved height calculation logic
useEffect(() => {
@ -347,11 +347,9 @@ export function TaskCard({
// Set the active workspace and agent
chatStore.setActiveWorkSpace(
chatStore.activeTaskId as string,
"workflow"
);
chatStore.setActiveAgent(
chatStore.activeTaskId as string,
task.agent?.agent_id
);
window.electronAPI.hideAllWebview();
@ -386,9 +384,7 @@ export function TaskCard({
<LoaderCircle
size={16}
className={`text-icon-information ${
chatStore.tasks[
chatStore.activeTaskId as string
].status === "running" && "animate-spin"
chatStore.task?.status === "running" && "animate-spin"
} `}
/>
)}

View file

@ -37,16 +37,15 @@ export const UserQueryGroup: React.FC<UserQueryGroupProps> = ({
const taskBoxRef = useRef<HTMLDivElement>(null);
const [isTaskBoxSticky, setIsTaskBoxSticky] = useState(false);
const chatState = chatStore.getState();
const activeTaskId = chatState.activeTaskId;
const activeTaskId = chatState.taskId;
// 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 || '';
if (!state.taskId || !state.task) return '';
return state.task.streamingDecomposeText || '';
}
);
@ -55,11 +54,11 @@ export const UserQueryGroup: React.FC<UserQueryGroupProps> = ({
// Exclude human-reply scenarios (when user is replying to an activeAsk)
const isHumanReply = queryGroup.userMessage &&
activeTaskId &&
chatState.tasks[activeTaskId] &&
(chatState.tasks[activeTaskId].activeAsk ||
chatState.task &&
(chatState.task.activeAsk ||
// Check if this user message follows an 'ask' message in the message sequence
(() => {
const messages = chatState.tasks[activeTaskId].messages;
const messages = chatState.task.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
@ -72,22 +71,22 @@ export const UserQueryGroup: React.FC<UserQueryGroupProps> = ({
const isLastUserQuery = !queryGroup.taskMessage &&
!isHumanReply &&
activeTaskId &&
chatState.tasks[activeTaskId] &&
chatState.task &&
queryGroup.userMessage &&
queryGroup.userMessage.id === chatState.tasks[activeTaskId].messages.filter((m: any) => m.role === 'user').pop()?.id &&
queryGroup.userMessage.id === chatState.task.messages.filter((m: any) => m.role === 'user').pop()?.id &&
// Only show during active phases (not finished)
chatState.tasks[activeTaskId].status !== 'finished';
chatState.task.status !== 'finished';
// 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 && !isDecomposing;
isLastUserQuery && activeTaskId && !chatState.task?.hasWaitComfirm && !isDecomposing;
const task =
(queryGroup.taskMessage || shouldShowFallbackTask) && activeTaskId
? chatState.tasks[activeTaskId]
? chatState.task
: null;
// Set up intersection observer for this query group
@ -230,15 +229,15 @@ export const UserQueryGroup: React.FC<UserQueryGroupProps> = ({
progressValue={task?.progressValue || 0}
summaryTask={task?.summaryTask || ""}
onAddTask={() => {
chatState.setIsTaskEdit(activeTaskId as string, true);
chatState.setIsTaskEdit(true);
chatState.addTaskInfo();
}}
onUpdateTask={(taskIndex, content) => {
chatState.setIsTaskEdit(activeTaskId as string, true);
chatState.setIsTaskEdit(true);
chatState.updateTaskInfo(taskIndex, content);
}}
onDeleteTask={(taskIndex) => {
chatState.setIsTaskEdit(activeTaskId as string, true);
chatState.setIsTaskEdit(true);
chatState.deleteTaskInfo(taskIndex);
}}
clickable={true}
@ -279,8 +278,8 @@ export const UserQueryGroup: React.FC<UserQueryGroupProps> = ({
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.3 }}
onClick={() => {
chatState.setSelectedFile(activeTaskId as string, file);
chatState.setActiveWorkSpace(activeTaskId as string, "documentWorkSpace");
chatState.setSelectedFile(file);
chatState.setActiveWorkSpace("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"
>
@ -356,8 +355,8 @@ export const UserQueryGroup: React.FC<UserQueryGroupProps> = ({
animate={{ opacity: 1, scale: 1 }}
transition={{ delay: 0.3 }}
onClick={() => {
chatState.setSelectedFile(activeTaskId as string, file);
chatState.setActiveWorkSpace(activeTaskId as string, "documentWorkSpace");
chatState.setSelectedFile(file);
chatState.setActiveWorkSpace("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"
>

View file

@ -92,17 +92,17 @@ export default function ChatBox(): JSX.Element {
const navigate = useNavigate();
const handleSend = async (messageStr?: string, taskId?: string) => {
const _taskId = taskId || chatStore.activeTaskId;
const _taskId = taskId || chatStore.taskId;
if (message.trim() === "" && !messageStr) return;
const tempMessageContent = messageStr || message;
chatStore.setHasMessages(_taskId as string, true);
chatStore.setHasMessages(true);
if (!_taskId) return;
// Multi-turn support: Check if task is running or planning (splitting/confirm)
const task = chatStore.tasks[_taskId];
const task = chatStore.task;
const requiresHumanReply = Boolean(task?.activeAsk);
const isTaskInProgress = ["running", "pause"].includes(task?.status || "");
const isTaskBusy = (
const isTaskBusy = task ? (
// running or paused counts as busy
(task.status === 'running' && task.hasMessages) || task.status === 'pause' ||
// splitting phase: has to_sub_tasks not confirmed OR skeleton computing
@ -110,7 +110,7 @@ export default function ChatBox(): JSX.Element {
((!task.messages.find(m => m.step === 'to_sub_tasks') && !task.hasWaitComfirm && task.messages.length > 0) || task.isTakeControl) ||
// explicit confirm wait while task is pending but card not confirmed yet
(!!task.messages.find(m => m.step === 'to_sub_tasks' && !m.isConfirm) && task.status === 'pending')
);
) : false;
const isReplayChatStore = task?.type === "replay";
if (!requiresHumanReply && isTaskBusy && !isReplayChatStore) {
toast.error("Current task is in progress. Please wait for it to finish before sending a new request.", {
@ -122,48 +122,48 @@ export default function ChatBox(): JSX.Element {
if (textareaRef.current) textareaRef.current.style.height = "60px";
try {
if (requiresHumanReply) {
chatStore.addMessages(_taskId, {
chatStore.addMessages({
id: generateUniqueId(),
role: "user",
content: tempMessageContent,
attaches:
JSON.parse(JSON.stringify(chatStore.tasks[_taskId]?.attaches)) || [],
JSON.parse(JSON.stringify(chatStore.task?.attaches)) || [],
});
setMessage("");
// Scroll to bottom after adding user message
setTimeout(() => {
scrollToBottom();
}, 200);
chatStore.setIsPending(_taskId, true);
chatStore.setIsPending(true);
await fetchPost(`/chat/${projectStore.activeProjectId}/human-reply`, {
agent: chatStore.tasks[_taskId].activeAsk,
agent: chatStore.task?.activeAsk,
reply: tempMessageContent,
});
if (chatStore.tasks[_taskId].askList.length === 0) {
chatStore.setActiveAsk(_taskId, "");
if ((chatStore.task?.askList?.length ?? 0) === 0) {
chatStore.setActiveAsk("");
} else {
let activeAskList = chatStore.tasks[_taskId].askList;
let activeAskList = [...(chatStore.task?.askList || [])];
console.log(
"activeAskList",
JSON.parse(JSON.stringify(activeAskList))
);
let message = activeAskList.shift();
chatStore.setActiveAskList(_taskId, [...activeAskList]);
chatStore.setActiveAsk(_taskId, message?.agent_name || "");
chatStore.setIsPending(_taskId, false);
chatStore.addMessages(_taskId, message!);
chatStore.setActiveAskList([...activeAskList]);
chatStore.setActiveAsk(message?.agent_name || "");
chatStore.setIsPending(false);
chatStore.addMessages(message!);
}
} else {
// Check if we should continue the conversation or start a new task
const hasMessages = chatStore.tasks[_taskId as string].messages.length > 0;
const isFinished = chatStore.tasks[_taskId as string].status === "finished";
const hasWaitComfirm = chatStore.tasks[_taskId as string]?.hasWaitComfirm;
const hasMessages = (chatStore.task?.messages?.length ?? 0) > 0;
const isFinished = chatStore.task?.status === "finished";
const hasWaitComfirm = chatStore.task?.hasWaitComfirm;
// Check if this task was manually stopped (finished but without natural completion)
const wasTaskStopped = isFinished && !chatStore.tasks[_taskId as string].messages.some(
const wasTaskStopped = isFinished && !chatStore.task?.messages?.some(
m => m.step === "end" // Natural completion has an "end" step message
);
@ -171,29 +171,29 @@ export default function ChatBox(): JSX.Element {
// 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 && !wasTaskStopped) || (isFinished && !wasTaskStopped) || (hasMessages && chatStore.tasks[_taskId as string].status === "pending");
const shouldContinueConversation = (hasWaitComfirm && !wasTaskStopped) || (isFinished && !wasTaskStopped) || (hasMessages && chatStore.task?.status === "pending");
if (shouldContinueConversation) {
// Check if this is the very first message and task hasn't started
const hasSimpleResponse = chatStore.tasks[_taskId as string].messages.some(
const hasSimpleResponse = chatStore.task?.messages?.some(
m => m.step === "wait_confirm"
);
const hasComplexTask = chatStore.tasks[_taskId as string].messages.some(
const hasComplexTask = chatStore.task?.messages?.some(
m => m.step === "to_sub_tasks"
);
const hasErrorMessage = chatStore.tasks[_taskId as string].messages.some(
const hasErrorMessage = chatStore.task?.messages?.some(
m => m.role === "agent" && m.content.startsWith("❌ **Error**:")
);
// Only start a new task if: pending, no messages processed yet
// OR while or after replaying a project
if ((chatStore.tasks[_taskId as string].status === "pending" && !hasSimpleResponse && !hasComplexTask && !isFinished)
|| chatStore.tasks[_taskId].type === "replay" || hasErrorMessage) {
if ((chatStore.task?.status === "pending" && !hasSimpleResponse && !hasComplexTask && !isFinished)
|| chatStore.task?.type === "replay" || hasErrorMessage) {
setMessage("");
// Pass the message content to startTask instead of adding it to current chatStore
const attachesToSend = JSON.parse(JSON.stringify(chatStore.tasks[_taskId]?.attaches)) || [];
const attachesToSend = JSON.parse(JSON.stringify(chatStore.task?.attaches)) || [];
try {
await chatStore.startTask(_taskId, undefined, undefined, undefined, tempMessageContent, attachesToSend);
await chatStore.startTask(undefined, undefined, undefined, tempMessageContent, attachesToSend);
} catch (err: any) {
console.error("Failed to start task:", err);
toast.error(err?.message || "Failed to start task. Please check your model configuration.");
@ -214,15 +214,15 @@ export default function ChatBox(): JSX.Element {
question: tempMessageContent,
task_id: nextTaskId
});
chatStore.setIsPending(_taskId, true);
chatStore.setIsPending(true);
// Add the user message to show it in UI
chatStore.addMessages(_taskId, {
chatStore.addMessages({
id: generateUniqueId(),
role: "user",
content: tempMessageContent,
attaches: JSON.parse(JSON.stringify(chatStore.tasks[_taskId]?.attaches)) || [],
attaches: JSON.parse(JSON.stringify(chatStore.task?.attaches)) || [],
});
chatStore.setAttaches(_taskId, []);
chatStore.setAttaches([]);
setMessage("");
}
} else {
@ -246,13 +246,13 @@ export default function ChatBox(): JSX.Element {
setTimeout(() => {
scrollToBottom();
}, 200);
// For the very first message, add it to the current chatStore first, then call startTask
const attachesToSend = JSON.parse(JSON.stringify(chatStore.tasks[_taskId]?.attaches)) || [];
const attachesToSend = JSON.parse(JSON.stringify(chatStore.task?.attaches)) || [];
setMessage("");
try {
await chatStore.startTask(_taskId, undefined, undefined, undefined, tempMessageContent, attachesToSend);
chatStore.setHasWaitComfirm(_taskId as string, true);
await chatStore.startTask(undefined, undefined, undefined, tempMessageContent, attachesToSend);
chatStore.setHasWaitComfirm(true);
} catch (err: any) {
console.error("Failed to start task:", err);
toast.error(err?.message || "Failed to start task. Please check your model configuration.");
@ -282,20 +282,19 @@ export default function ChatBox(): JSX.Element {
return;
}
let _token: string = token.split("__")[0];
let taskId: string = token.split("__")[1];
chatStore.create(taskId, "share");
chatStore.setHasMessages(taskId, true);
let shareTaskId: string = token.split("__")[1];
chatStore.create(shareTaskId, "share");
chatStore.setHasMessages(true);
const res = await proxyFetchGet(`/api/chat/share/info/${_token}`);
if (res?.question) {
chatStore.addMessages(taskId, {
chatStore.addMessages({
id: generateUniqueId(),
role: "user",
content: res.question.split("|")[0],
});
try {
await chatStore.startTask(taskId, "share", _token, 0.1);
chatStore.setActiveTaskId(taskId);
chatStore.handleConfirmTask(projectStore.activeProjectId, taskId, "share");
await chatStore.startTask("share", _token, 0.1);
chatStore.handleConfirmTask(projectStore.activeProjectId, "share");
} catch (err: any) {
console.error("Failed to start shared task:", err);
toast.error(err?.message || "Failed to start task. Please check your model configuration.");
@ -352,13 +351,12 @@ export default function ChatBox(): JSX.Element {
}, []);
const [loading, setLoading] = useState(false);
const handleConfirmTask = async (taskId?: string) => {
const _taskId = taskId || chatStore.activeTaskId;
if (!_taskId || !projectStore.activeProjectId) {
const handleConfirmTask = async () => {
if (!chatStore.taskId || !projectStore.activeProjectId) {
return;
}
setLoading(true);
await chatStore.handleConfirmTask(projectStore.activeProjectId, _taskId);
await chatStore.handleConfirmTask(projectStore.activeProjectId);
setLoading(false);
};
@ -371,14 +369,13 @@ export default function ChatBox(): JSX.Element {
});
if (result.success && result.files && result.files.length > 0) {
const taskId = chatStore.activeTaskId as string;
const files = [
...chatStore.tasks[taskId].attaches.filter(
...(chatStore.task?.attaches || []).filter(
(f) => !result.files.find((r: File) => r.filePath === f.filePath)
),
...result.files,
];
chatStore.setAttaches(taskId, files);
chatStore.setAttaches(files);
}
} catch (error) {
console.error("Select File Error:", error);
@ -396,23 +393,23 @@ export default function ChatBox(): JSX.Element {
// Pause/Resume handler
const [isPauseResumeLoading, setIsPauseResumeLoading] = useState(false);
const handlePauseResume = () => {
const taskId = chatStore.activeTaskId as string;
const task = chatStore.tasks[taskId];
const task = chatStore.task;
if (!task) return;
const type = task.status === 'running' ? 'pause' : 'resume';
setIsPauseResumeLoading(true);
if (type === 'pause') {
let { taskTime, elapsed } = task;
const now = Date.now();
elapsed += now - taskTime;
chatStore.setElapsed(taskId, elapsed);
chatStore.setTaskTime(taskId, 0);
chatStore.setStatus(taskId, 'pause');
chatStore.setElapsed(elapsed);
chatStore.setTaskTime(0);
chatStore.setStatus('pause');
} else {
chatStore.setTaskTime(taskId, Date.now());
chatStore.setStatus(taskId, 'running');
chatStore.setTaskTime(Date.now());
chatStore.setStatus('running');
}
fetchPut(`/task/${projectStore.activeProjectId}/take-control`, {
action: type,
});
@ -421,7 +418,7 @@ export default function ChatBox(): JSX.Element {
// Stop task handler - triggers Action.skip_task which preserves context
const handleSkip = async () => {
const taskId = chatStore.activeTaskId as string;
const taskId = chatStore.taskId as string;
console.log("=" .repeat(80));
console.log("🛑 [STOP-BUTTON] handleSkip CALLED from frontend");
console.log(`[STOP-BUTTON] taskId: ${taskId}, projectId: ${projectStore.activeProjectId}`);
@ -443,7 +440,7 @@ export default function ChatBox(): JSX.Element {
console.log("[STOP-BUTTON] ⚠️ SSE connection kept alive, waiting for backend 'end' event");
// Only set isPending to false so UI shows task is stopped
chatStore.setIsPending(taskId, false);
chatStore.setIsPending(false);
console.log("[STOP-BUTTON] ✅ Task marked as not pending, SSE connection remains open");
toast.success("Task stopped successfully", {
@ -455,8 +452,8 @@ export default function ChatBox(): JSX.Element {
// If backend call failed, close SSE connection as fallback
console.log("[STOP-BUTTON] Backend call failed, closing SSE connection as fallback");
try {
chatStore.stopTask(taskId);
chatStore.setIsPending(taskId, false);
chatStore.stopTask();
chatStore.setIsPending(false);
console.log("[STOP-BUTTON] ⚠️ SSE connection closed due to backend failure");
toast.warning("Task stopped locally, but backend notification failed. Backend task may continue running.", {
closeButton: true,
@ -476,20 +473,20 @@ export default function ChatBox(): JSX.Element {
// Edit query handler
const handleEditQuery = async () => {
const taskId = chatStore.activeTaskId as string;
const taskId = chatStore.taskId as string;
const projectId = projectStore.activeProjectId;
// Early validation
if (!projectId) {
if (!projectId || !chatStore.task) {
console.error("No active project ID found for edit operation");
return;
}
// Get question and attachments before any deletions
const messageIndex = chatStore.tasks[taskId].messages.findLastIndex(
const messageIndex = chatStore.task.messages.findLastIndex(
(item) => item.step === "to_sub_tasks"
);
const questionMessage = chatStore.tasks[taskId].messages[messageIndex - 2];
const questionMessage = chatStore.task.messages[messageIndex - 2];
const question = questionMessage.content;
// Get the file attachments from the original user message (not from task.attaches which gets cleared after sending)
const attachments = questionMessage.attaches || [];
@ -516,56 +513,56 @@ export default function ChatBox(): JSX.Element {
// Create new task and clean up locally
let id = chatStore.create();
chatStore.setHasMessages(id, true);
chatStore.setHasMessages(true);
// Copy the file attachments to the new task
if (attachments.length > 0) {
chatStore.setAttaches(id, attachments);
chatStore.setAttaches(attachments);
}
chatStore.removeTask(taskId);
chatStore.removeTask();
setMessage(question);
};
// Task time tracking
const [taskTime, setTaskTime] = useState(
chatStore.getFormattedTaskTime(chatStore.activeTaskId as string)
chatStore.getFormattedTaskTime()
);
useEffect(() => {
const interval = setInterval(() => {
if (chatStore.activeTaskId) {
if (chatStore.taskId) {
setTaskTime(
chatStore.getFormattedTaskTime(chatStore.activeTaskId)
chatStore.getFormattedTaskTime()
);
}
}, 500);
return () => clearInterval(interval);
}, [chatStore.activeTaskId]);
}, [chatStore.taskId]);
// Determine BottomBox state
const getBottomBoxState = () => {
if (!chatStore.activeTaskId) return "input";
const task = chatStore.tasks[chatStore.activeTaskId];
if (!chatStore.taskId) return "input";
const task = chatStore.task;
// Queued messages no longer change BottomBox state; QueuedBox renders independently
// Check for any to_sub_tasks message (confirmed or not)
const anyToSubTasksMessage = task.messages.find((m) => m.step === "to_sub_tasks");
const toSubTasksMessage = task.messages.find((m) => (m.step === "to_sub_tasks" && !m.isConfirm));
const anyToSubTasksMessage = task?.messages.find((m: Message) => m.step === "to_sub_tasks");
const toSubTasksMessage = task?.messages.find((m: Message) => (m.step === "to_sub_tasks" && !m.isConfirm));
// Determine if we're in the "splitting in progress" phase (skeleton visible)
// Only show splitting if there's NO to_sub_tasks message yet (not even confirmed)
const isSkeletonPhase = (
task.status !== 'finished' &&
!anyToSubTasksMessage &&
!task.hasWaitComfirm &&
task.messages.length > 0) ||
(task.isTakeControl && !anyToSubTasksMessage);
task?.status !== 'finished' &&
!anyToSubTasksMessage &&
!task?.hasWaitComfirm &&
(task?.messages?.length ?? 0) > 0) ||
(task?.isTakeControl && !anyToSubTasksMessage);
if (isSkeletonPhase) {
return "splitting";
}
// After splitting completes and TaskCard is awaiting user confirmation,
// the Task becomes 'pending' and we show the confirm state.
if (toSubTasksMessage && !toSubTasksMessage.isConfirm && task.status === 'pending') {
if (toSubTasksMessage && !toSubTasksMessage.isConfirm && task?.status === 'pending') {
return "confirm";
}
@ -575,11 +572,11 @@ export default function ChatBox(): JSX.Element {
}
// Check task status
if (task.status === 'running' || task.status === 'pause') {
if (task?.status === 'running' || task?.status === 'pause') {
return "running";
}
if (task.status === 'finished' && task.type !== '') {
if (task?.status === 'finished' && task?.type !== '') {
return "finished";
}
@ -589,22 +586,18 @@ export default function ChatBox(): JSX.Element {
const [hasSubTask, setHasSubTask] = useState(false);
useEffect(() => {
const _hasSubTask = chatStore.tasks[
chatStore.activeTaskId as string
]?.messages?.find((message) => message.step === "to_sub_tasks")
const _hasSubTask = chatStore.task?.messages?.find((message: Message) => message.step === "to_sub_tasks")
? true
: false;
setHasSubTask(_hasSubTask);
}, [chatStore?.tasks[chatStore.activeTaskId as string]?.messages]);
}, [chatStore.task?.messages]);
useEffect(() => {
const activeAsk =
chatStore?.tasks[chatStore.activeTaskId as string]?.activeAsk;
const activeAsk = chatStore.task?.activeAsk;
let timer: NodeJS.Timeout;
if (activeAsk && activeAsk !== "") {
const _taskId = chatStore.activeTaskId as string;
timer = setTimeout(() => {
handleSend("skip", _taskId);
handleSend("skip");
}, 30000); // 30 seconds
return () => clearTimeout(timer); // clear previous timer
}
@ -613,7 +606,7 @@ export default function ChatBox(): JSX.Element {
clearTimeout(timer);
};
}, [
chatStore?.tasks[chatStore.activeTaskId as string]?.activeAsk,
chatStore.task?.activeAsk,
message, // depend on message
]);
@ -648,7 +641,7 @@ export default function ChatBox(): JSX.Element {
// Always try to call the backend to remove the task
// The backend will handle the error gracefully if workforce is not initialized
// Note: Replay creates a new chatstore, so no conflicts
const task = chatStore.tasks[chatStore.activeTaskId as string];
const task = chatStore.task;
// Only skip backend call if task is finished or hasn't started yet (no messages)
if(task && task.messages.length > 0 && task.status !== 'finished') {
try {
@ -677,42 +670,42 @@ export default function ChatBox(): JSX.Element {
// Check if any chat store in the project has messages
const hasAnyMessages = useMemo(() => {
// First check current active chat store
if (chatStore.activeTaskId && chatStore.tasks[chatStore.activeTaskId]) {
const activeTask = chatStore.tasks[chatStore.activeTaskId];
if (chatStore.taskId && chatStore.task) {
const activeTask = chatStore.task;
if ((activeTask.messages && activeTask.messages.length > 0) || activeTask.hasMessages) {
return true;
}
}
// Then check all other chat stores in the project
return getAllChatStoresMemoized.some(({chatStore: store}) => {
return getAllChatStoresMemoized.some(({chatStore: store}: {chatStore: any}) => {
const state = store.getState();
return state.activeTaskId &&
state.tasks[state.activeTaskId] &&
(state.tasks[state.activeTaskId].messages.length > 0 ||
state.tasks[state.activeTaskId].hasMessages);
return state.taskId &&
state.task &&
(state.task.messages.length > 0 ||
state.task.hasMessages);
});
}, [chatStore, getAllChatStoresMemoized]);
const isTaskBusy = useMemo(() => {
if (!chatStore.activeTaskId || !chatStore.tasks[chatStore.activeTaskId]) return false;
const task = chatStore.tasks[chatStore.activeTaskId];
if (!chatStore.taskId || !chatStore.task) return false;
const task = chatStore.task;
return (
// running or paused
task.status === 'running' ||
task.status === 'running' ||
task.status === 'pause' ||
// splitting phase
task.messages.some(m => m.step === 'to_sub_tasks' && !m.isConfirm) ||
task.messages.some((m: Message) => m.step === 'to_sub_tasks' && !m.isConfirm) ||
// skeleton/computing phase
((!task.messages.find(m => m.step === 'to_sub_tasks') && !task.hasWaitComfirm && task.messages.length > 0) || task.isTakeControl)
((!task.messages.find((m: Message) => m.step === 'to_sub_tasks') && !task.hasWaitComfirm && task.messages.length > 0) || task.isTakeControl)
);
}, [chatStore.activeTaskId, chatStore.tasks]);
}, [chatStore.taskId, chatStore.task]);
const isInputDisabled = useMemo(() => {
if (!chatStore.activeTaskId || !chatStore.tasks[chatStore.activeTaskId]) return true;
const task = chatStore.tasks[chatStore.activeTaskId];
if (!chatStore.taskId || !chatStore.task) return true;
const task = chatStore.task;
// If ask human is active, allow input
if (task.activeAsk) return false;
@ -725,8 +718,8 @@ export default function ChatBox(): JSX.Element {
return false;
}, [
chatStore.activeTaskId,
chatStore.tasks,
chatStore.taskId,
chatStore.task,
privacy,
useCloudModelInDev,
isTaskBusy
@ -742,7 +735,7 @@ export default function ChatBox(): JSX.Element {
onSkip={handleSkip}
isPauseResumeLoading={isPauseResumeLoading}
/>
{chatStore.activeTaskId && (
{chatStore.taskId && (
<BottomBox
state={getBottomBoxState()}
queuedMessages={isTaskBusy ? [] : projectStore.getProjectById(projectStore.activeProjectId || '')?.queuedMessages?.map(m => ({
@ -751,21 +744,21 @@ export default function ChatBox(): JSX.Element {
timestamp: m.timestamp
})) || []}
onRemoveQueuedMessage={(id) => handleRemoveTaskQueue(id)}
subtitle={getBottomBoxState() === 'confirm'
subtitle={getBottomBoxState() === 'confirm'
? (() => {
// Find the last message where role is "user"
const messages = chatStore.tasks[chatStore.activeTaskId]?.messages || [];
const lastUserMessage = messages.slice().reverse().find(msg => msg.role === "user");
return lastUserMessage?.content || chatStore.tasks[chatStore.activeTaskId]?.summaryTask;
const messages = chatStore.task?.messages || [];
const lastUserMessage = messages.slice().reverse().find((msg: Message) => msg.role === "user");
return lastUserMessage?.content || chatStore.task?.summaryTask;
})()
: chatStore.tasks[chatStore.activeTaskId]?.summaryTask}
: chatStore.task?.summaryTask}
onStartTask={() => handleConfirmTask()}
onEdit={handleEditQuery}
tokens={chatStore.tasks[chatStore.activeTaskId]?.tokens || 0}
tokens={chatStore.task?.tokens || 0}
taskTime={taskTime}
taskStatus={chatStore.tasks[chatStore.activeTaskId]?.status}
taskStatus={chatStore.task?.status}
onReplay={handleReplay}
replayDisabled={chatStore.tasks[chatStore.activeTaskId]?.status !== 'finished'}
replayDisabled={chatStore.task?.status !== 'finished'}
replayLoading={isReplayLoading}
onPauseResume={handlePauseResume}
pauseResumeLoading={isPauseResumeLoading}
@ -774,11 +767,11 @@ export default function ChatBox(): JSX.Element {
value: message,
onChange: setMessage,
onSend: handleSend,
files: chatStore.tasks[chatStore.activeTaskId]?.attaches?.map(f => ({
files: chatStore.task?.attaches?.map((f: any) => ({
fileName: f.fileName,
filePath: f.filePath
})) || [],
onFilesChange: (files) => chatStore.setAttaches(chatStore.activeTaskId as string, files as any),
onFilesChange: (files) => chatStore.setAttaches(files as any),
onAddFile: handleFileSelect,
placeholder: t("chat.ask-placeholder"),
disabled: isInputDisabled,
@ -804,18 +797,18 @@ export default function ChatBox(): JSX.Element {
</div>
</div>
{chatStore.activeTaskId && (
{chatStore.taskId && (
<BottomBox
state="input"
inputProps={{
value: message,
onChange: setMessage,
onSend: handleSend,
files: chatStore.tasks[chatStore.activeTaskId]?.attaches?.map(f => ({
files: chatStore.task?.attaches?.map((f: any) => ({
fileName: f.fileName,
filePath: f.filePath
})) || [],
onFilesChange: (files) => chatStore.setAttaches(chatStore.activeTaskId as string, files as any),
onFilesChange: (files) => chatStore.setAttaches(files as any),
onAddFile: handleFileSelect,
placeholder: t("chat.ask-placeholder"),
disabled: isInputDisabled,

View file

@ -189,7 +189,7 @@ export default function Folder({ data }: { data?: Agent }) {
.invoke("read-file-dataurl", file.path)
.then((dataUrl: string) => {
setSelectedFile({ ...file, content: dataUrl });
chatStore.setSelectedFile(chatStore.activeTaskId as string, file);
chatStore.setSelectedFile(file);
setLoading(false);
})
.catch((error) => {
@ -204,7 +204,7 @@ export default function Folder({ data }: { data?: Agent }) {
.invoke("open-file", file.type, file.path, isShowSourceCode)
.then((res) => {
setSelectedFile({ ...file, content: res });
chatStore.setSelectedFile(chatStore.activeTaskId as string, file);
chatStore.setSelectedFile(file);
setLoading(false);
})
.catch((error) => {
@ -304,10 +304,10 @@ export default function Folder({ data }: { data?: Agent }) {
const hasFetchedRemote = useRef(false);
// Reset hasFetchedRemote when activeTaskId changes
// Reset hasFetchedRemote when taskId changes
useEffect(() => {
hasFetchedRemote.current = false;
}, [chatStore.activeTaskId]);
}, [chatStore.taskId]);
useEffect(() => {
const setFileList = async () => {
@ -348,7 +348,7 @@ export default function Folder({ data }: { data?: Agent }) {
// Keep the old structure for compatibility
setFileGroups((prev) => {
const chatStoreSelectedFile =
chatStore.tasks[chatStore.activeTaskId as string]?.selectedFile;
chatStore.task?.selectedFile;
if (chatStoreSelectedFile) {
console.log(res, chatStoreSelectedFile);
const file = res.find(
@ -368,11 +368,11 @@ export default function Folder({ data }: { data?: Agent }) {
});
};
setFileList();
}, [chatStore.tasks[chatStore.activeTaskId as string]?.taskAssigning]);
}, [chatStore.task?.taskAssigning]);
useEffect(() => {
const chatStoreSelectedFile =
chatStore.tasks[chatStore.activeTaskId as string]?.selectedFile;
chatStore.task?.selectedFile;
if (chatStoreSelectedFile && fileGroups[0]?.files) {
const file = fileGroups[0].files.find(
(item: any) => item.path === chatStoreSelectedFile.path
@ -382,14 +382,14 @@ export default function Folder({ data }: { data?: Agent }) {
}
}
}, [
chatStore.tasks[chatStore.activeTaskId as string]?.selectedFile?.path,
chatStore.task?.selectedFile?.path,
fileGroups,
isShowSourceCode,
chatStore.activeTaskId,
chatStore.taskId,
]);
const handleBack = () => {
chatStore.setActiveWorkSpace(chatStore.activeTaskId as string, "workflow");
chatStore.setActiveWorkSpace("workflow");
};
return (

View file

@ -115,18 +115,17 @@ export default function ProjectGroup({
// Calculate if project has issues (requiring human in the loop)
// Find tasks in chatStore where task_id matches any task in the project
const hasHumanInLoop = useMemo(() => {
if (!chatStore?.tasks || !project.tasks?.length) return false;
if (!chatStore?.task || !project.tasks?.length) return false;
// Get all task_ids from the project, filtering out undefined/null values
const projectTaskIds = project.tasks
.map(task => task.task_id)
.filter((id): id is string => !!id);
// Check if any task in chatStore with matching task_id has pending status
return Object.entries(chatStore.tasks).some(([taskId, task]) =>
projectTaskIds.includes(taskId) && task.status === 'pending'
);
}, [chatStore?.tasks, project.tasks]);
// Check if current task matches any project task and has pending status
const currentTaskId = chatStore.taskId;
return currentTaskId && projectTaskIds.includes(currentTaskId) && chatStore.task.status === 'pending';
}, [chatStore?.task, chatStore?.taskId, project.tasks]);
const hasIssue = hasHumanInLoop;
// Calculate agent count (placeholder - count unique agents from tasks if available)

View file

@ -101,20 +101,18 @@ export default function HistorySidebar() {
// Check all chat stores for ongoing tasks
chatStores.forEach(({ chatStore: cs }) => {
const csState = cs.getState();
Object.keys(csState.tasks || {}).forEach((taskId) => {
const task = csState.tasks[taskId];
// Only include ongoing tasks
if (task.status !== "finished" && !task.type) {
hasOngoingTasks = true;
taskCount++;
if (task.tokens) {
totalTokens += task.tokens;
}
if (!lastPrompt && task.messages?.[0]?.content) {
lastPrompt = task.messages[0].content;
}
const task = csState.task;
// Only include ongoing tasks
if (task && task.status !== "finished" && !task.type) {
hasOngoingTasks = true;
taskCount++;
if (task.tokens) {
totalTokens += task.tokens;
}
});
if (!lastPrompt && task.messages?.[0]?.content) {
lastPrompt = task.messages[0].content;
}
}
});
// Only add project if it has ongoing tasks

View file

@ -40,8 +40,8 @@ const Layout = () => {
useEffect(() => {
const handleBeforeClose = () => {
const currentStatus = chatStore.tasks[chatStore.activeTaskId as string]?.status;
if(["running", "pause"].includes(currentStatus)) {
const currentStatus = chatStore.task?.status;
if(currentStatus && ["running", "pause"].includes(currentStatus)) {
setNoticeOpen(true);
} else {
window.electronAPI.closeWindow(true);
@ -53,7 +53,7 @@ const Layout = () => {
return () => {
window.ipcRenderer.removeAllListeners("before-close");
};
}, [chatStore.tasks, chatStore.activeTaskId]);
}, [chatStore.task, chatStore.taskId]);
// Determine what to show based on states
const shouldShowOnboarding = initState === "done" && isFirstLaunch && !isInstalling;

View file

@ -78,12 +78,12 @@ export default function Home() {
const [activeAgent, setActiveAgent] = useState<Agent | null>(null);
useEffect(() => {
const taskAssigning =
chatStore.tasks[chatStore.activeTaskId as string]?.taskAssigning;
chatStore.task?.taskAssigning;
if (taskAssigning) {
const activeAgent = taskAssigning.find(
(item) =>
item.agent_id ===
chatStore.tasks[chatStore.activeTaskId as string]?.activeWorkSpace
chatStore.task?.activeWorkSpace
);
setActiveAgent(() => {
if (activeAgent) {
@ -93,8 +93,8 @@ export default function Home() {
});
}
}, [
chatStore.tasks[chatStore.activeTaskId as string].taskAssigning,
chatStore.tasks[chatStore.activeTaskId as string].activeWorkSpace,
chatStore.task?.taskAssigning,
chatStore.task?.activeWorkSpace,
]);
const [isTakeControl, setIsTakeControl] = useState(false);
@ -193,7 +193,6 @@ export default function Home() {
variant="ghost"
onClick={() => {
chatStore.setActiveWorkSpace(
chatStore.activeTaskId as string,
"workflow"
);
}}

View file

@ -104,7 +104,7 @@ export function SearchHistoryDialog() {
onTaskSelect={handleSetActive}
onTaskDelete={handleDelete}
onTaskShare={handleShare}
activeTaskId={chatStore.activeTaskId || undefined}
activeTaskId={chatStore.taskId || undefined}
/>
</div>
) : (

View file

@ -138,7 +138,7 @@ export const TaskState = ({
(isSelected("ongoing") || forceVisible) &&
"!text-icon-information"
} ${
chatStore.tasks[chatStore.activeTaskId as string]?.status ===
chatStore.task?.status ===
"running" && "animate-spin"
}`}
/>

View file

@ -305,7 +305,7 @@ export default function TerminalComponent({
}
// if there is history data, re-write
if (chatStore.activeTaskId) {
if (chatStore.taskId) {
const terminalData = content || [];
if (terminalData.length > 0) {
xtermRef.current.writeln("\x1b[90m--- Previous Output ---\x1b[0m");
@ -332,7 +332,7 @@ export default function TerminalComponent({
// show prompt
xtermRef.current.write(promptText);
}, 200);
}, [chatStore.activeTaskId, showWelcome, instanceId]);
}, [chatStore.taskId, showWelcome, instanceId]);
// render terminal component
return (

View file

@ -80,12 +80,12 @@ export default function TerminalAgentWrokSpace() {
const [activeAgent, setActiveAgent] = useState<Agent | null>(null);
useEffect(() => {
const taskAssigning =
chatStore.tasks[chatStore.activeTaskId as string]?.taskAssigning;
chatStore.task?.taskAssigning;
if (taskAssigning) {
const activeAgent = taskAssigning.find(
(item) =>
item.agent_id ===
chatStore.tasks[chatStore.activeTaskId as string]?.activeWorkSpace
chatStore.task?.activeWorkSpace
);
setActiveAgent(() => {
if (activeAgent) {
@ -95,8 +95,8 @@ export default function TerminalAgentWrokSpace() {
});
}
}, [
chatStore.tasks[chatStore.activeTaskId as string].taskAssigning,
chatStore.tasks[chatStore.activeTaskId as string].activeWorkSpace,
chatStore.task?.taskAssigning,
chatStore.task?.activeWorkSpace,
]);
const [isTakeControl, setIsTakeControl] = useState(false);
@ -145,7 +145,6 @@ export default function TerminalAgentWrokSpace() {
variant="ghost"
onClick={() => {
chatStore.setActiveWorkSpace(
chatStore.activeTaskId as string,
"workflow"
);
}}

View file

@ -116,17 +116,15 @@ function HeaderWin() {
const activeTaskTitle = useMemo(() => {
if (
chatStore.activeTaskId &&
chatStore.tasks[chatStore.activeTaskId as string]?.summaryTask
chatStore.taskId &&
chatStore.task?.summaryTask
) {
return chatStore.tasks[
chatStore.activeTaskId as string
].summaryTask.split("|")[0];
return chatStore.task.summaryTask.split("|")[0];
}
return t("layout.new-project");
}, [
chatStore.activeTaskId,
chatStore.tasks[chatStore.activeTaskId as string]?.summaryTask,
chatStore.taskId,
chatStore.task?.summaryTask,
]);
const getReferFriendsLink = async () => {
@ -147,7 +145,7 @@ function HeaderWin() {
//TODO: Mark ChatStore details as completed
const handleEndProject = async () => {
const taskId = chatStore.activeTaskId;
const taskId = chatStore.taskId;
const projectId = projectStore.activeProjectId;
if (!taskId) {
@ -158,7 +156,7 @@ function HeaderWin() {
const historyId = projectId ? projectStore.getHistoryId(projectId) : null;
try {
const task = chatStore.tasks[taskId];
const task = chatStore.task;
// Stop the task if it's running
if (task && task.status === 'running') {
@ -175,11 +173,11 @@ function HeaderWin() {
}
// Delete from history using historyId
if (historyId && task.status !== "finished") {
if (historyId && task?.status !== "finished") {
try {
await proxyFetchDelete(`/api/chat/history/${historyId}`);
// Remove from local store
chatStore.removeTask(taskId);
chatStore.removeTask();
} catch (error) {
console.log("History may not exist:", error);
}
@ -312,12 +310,12 @@ function HeaderWin() {
platform === "darwin" && "pr-2"
} flex h-full items-center z-50 relative no-drag gap-1`}
>
{chatStore.activeTaskId &&
chatStore.tasks[chatStore.activeTaskId as string] &&
{chatStore.taskId &&
chatStore.task &&
(
(chatStore.tasks[chatStore.activeTaskId as string]?.messages?.length || 0) > 0 ||
chatStore.tasks[chatStore.activeTaskId as string]?.hasMessages ||
chatStore.tasks[chatStore.activeTaskId as string]?.status !== 'pending'
(chatStore.task?.messages?.length || 0) > 0 ||
chatStore.task?.hasMessages ||
chatStore.task?.status !== 'pending'
) && (
<TooltipSimple content={t("layout.end-project")} side="bottom" align="end">
<Button
@ -331,11 +329,11 @@ function HeaderWin() {
</Button>
</TooltipSimple>
)}
{chatStore.activeTaskId &&
chatStore.tasks[chatStore.activeTaskId as string]?.status === 'finished' && (
{chatStore.taskId &&
chatStore.task?.status === 'finished' && (
<TooltipSimple content={t("layout.share")} side="bottom" align="end">
<Button
onClick={() => handleShare(chatStore.activeTaskId as string)}
onClick={() => handleShare(chatStore.taskId as string)}
variant="ghost"
size="xs"
className="no-drag !text-button-fill-information-foreground bg-button-fill-information"
@ -355,7 +353,7 @@ function HeaderWin() {
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-36">
{chatStore.activeTaskId && chatStore.tasks[chatStore.activeTaskId as string] && (
{chatStore.taskId && chatStore.task && (
<DropdownMenuItem onClick={exportLog} className="cursor-pointer">
<FileDown className="w-4 h-4" />
{t("layout.report-bug")}

View file

@ -150,7 +150,7 @@ export function Node({ id, data }: NodeProps) {
};
useEffect(() => {
if (chatStore.tasks[chatStore.activeTaskId as string]?.activeAgent === id) {
if (chatStore.task?.activeAgent === id) {
const node = getNode(id);
if (node) {
setTimeout(() => {
@ -164,7 +164,7 @@ export function Node({ id, data }: NodeProps) {
}
}
}, [
chatStore.tasks[chatStore.activeTaskId as string]?.activeAgent,
chatStore.task?.activeAgent,
id,
setCenter,
getNode,
@ -328,7 +328,7 @@ export function Node({ id, data }: NodeProps) {
} ${
data.isEditMode ? "h-full" : "max-h-[calc(100vh-200px)]"
} border-worker-border-default flex border border-solid rounded-xl overflow-hidden bg-worker-surface-primary ${
chatStore.tasks[chatStore.activeTaskId as string].activeAgent === id
chatStore.task?.activeAgent === id
? `${agentMap[data.type]?.borderColor} z-50`
: "border-worker-border-default z-10"
} transition-all duration-300 ease-in-out ${
@ -355,8 +355,7 @@ export function Node({ id, data }: NodeProps) {
{isExpanded ? <SquareChevronLeft /> : <SquareCode />}
</Button>
{!Object.keys(agentMap).find((key) => key === data.type) &&
chatStore.tasks[chatStore.activeTaskId as string].messages
.length === 0 && (
(chatStore.task?.messages?.length ?? 0) === 0 && (
<Popover>
<PopoverTrigger asChild>
<Button
@ -419,7 +418,6 @@ export function Node({ id, data }: NodeProps) {
className="max-h-[180px]"
onClick={() => {
chatStore.setActiveWorkSpace(
chatStore.activeTaskId as string,
data.agent?.agent_id as string
);
@ -564,11 +562,9 @@ export function Node({ id, data }: NodeProps) {
data.onExpandChange(id, true);
if (task.agent) {
chatStore.setActiveWorkSpace(
chatStore.activeTaskId as string,
"workflow"
);
chatStore.setActiveAgent(
chatStore.activeTaskId as string,
task.agent?.agent_id
);
window.electronAPI.hideAllWebview();
@ -622,9 +618,7 @@ export function Node({ id, data }: NodeProps) {
<LoaderCircle
size={16}
className={`text-icon-information ${
chatStore.tasks[
chatStore.activeTaskId as string
].status === "running" && "animate-spin"
chatStore.task?.status === "running" && "animate-spin"
}`}
/>
)}
@ -711,9 +705,7 @@ export function Node({ id, data }: NodeProps) {
)}
<div
className={`${
chatStore.tasks[
chatStore.activeTaskId as string
].activeWorkSpace
chatStore.task?.activeWorkSpace
? "!w-[100px]"
: "!w-[500px]"
} pt-1 flex-shrink-0 flex-grow-0 min-w-0 text-text-primary text-xs leading-17 overflow-hidden text-ellipsis whitespace-nowrap`}
@ -773,9 +765,7 @@ export function Node({ id, data }: NodeProps) {
onClick={(e) => {
e.stopPropagation();
if (toolkit.toolkitMethods === "write to file") {
chatStore.tasks[
chatStore.activeTaskId as string
].activeWorkSpace = "documentWorkSpace";
chatStore.setActiveWorkSpace("documentWorkSpace");
} else if (
toolkit.toolkitMethods === "visit page"
) {
@ -794,9 +784,7 @@ export function Node({ id, data }: NodeProps) {
<LoaderCircle
size={16}
className={`${
chatStore.tasks[
chatStore.activeTaskId as string
].status === "running" && "animate-spin"
chatStore.task?.status === "running" && "animate-spin"
}`}
/>
) : (

View file

@ -68,25 +68,21 @@ export function WorkSpaceMenu() {
];
const [agentList, setAgentList] = useState<Agent[]>([]);
useEffect(() => {
const taskAssigning =
chatStore.tasks[chatStore.activeTaskId as string]?.taskAssigning;
const taskAssigning = chatStore.task?.taskAssigning;
const base = [...baseWorker, ...workerList].filter(
(worker) => !taskAssigning.find((agent) => agent.type === worker.type)
(worker) => !taskAssigning?.find((agent) => agent.type === worker.type)
);
setAgentList([...base, ...taskAssigning]);
setAgentList([...base, ...(taskAssigning || [])]);
}, [
chatStore.tasks[chatStore.activeTaskId as string]?.taskAssigning,
chatStore.task?.taskAssigning,
workerList,
]);
useEffect(() => {
const cleanup = window.electronAPI.onWebviewNavigated((id: string, url: string) => {
let webViewUrls = [
...chatStore.tasks[chatStore.activeTaskId as string].webViewUrls,
];
let taskAssigning = [
...chatStore.tasks[chatStore.activeTaskId as string].taskAssigning,
];
if (!chatStore.task) return;
let webViewUrls = [...chatStore.task.webViewUrls];
let taskAssigning = [...chatStore.task.taskAssigning];
const hasId = taskAssigning.find((item) =>
item.activeWebviewIds?.find((webview) => webview.id === id)
);
@ -109,10 +105,7 @@ export function WorkSpaceMenu() {
img: "",
processTaskId: hasUrl?.processTaskId || "",
});
chatStore.setTaskAssigning(
chatStore.activeTaskId as string,
taskAssigning
);
chatStore.setTaskAssigning(taskAssigning);
}
} else {
taskAssigning[activeAgentIndex].activeWebviewIds?.push({
@ -121,18 +114,13 @@ export function WorkSpaceMenu() {
img: "",
processTaskId: hasUrl?.processTaskId || "",
});
chatStore.setTaskAssigning(
chatStore.activeTaskId as string,
taskAssigning
);
chatStore.setTaskAssigning(taskAssigning);
}
const urlIndex = webViewUrls.findIndex((item) => item.url === url);
if (urlIndex !== -1) {
webViewUrls.splice(urlIndex, 1);
}
chatStore.setWebViewUrls(chatStore.activeTaskId as string, [
...webViewUrls,
]);
chatStore.setWebViewUrls([...webViewUrls]);
} else {
// If no URL match found, also try to add to search_agent
const searchAgentIndex = taskAssigning.findIndex((item) => item.type === 'search_agent');
@ -143,10 +131,7 @@ export function WorkSpaceMenu() {
img: "",
processTaskId: webViewUrls[0]?.processTaskId || "",
});
chatStore.setTaskAssigning(
chatStore.activeTaskId as string,
taskAssigning
);
chatStore.setTaskAssigning(taskAssigning);
}
}
}
@ -171,10 +156,8 @@ export function WorkSpaceMenu() {
window.ipcRenderer
.invoke("capture-webview", webview.id)
.then((base64: string) => {
let taskAssigning = [
...chatStore.tasks[chatStore.activeTaskId as string]
.taskAssigning,
];
if (!chatStore.task) return;
let taskAssigning = [...chatStore.task.taskAssigning];
const searchAgentIndex = taskAssigning.findIndex(
(agent) => agent.agent_id === webview.agent_id
);
@ -188,10 +171,7 @@ export function WorkSpaceMenu() {
webview.index
].img = base64;
chatStore.setTaskAssigning(
chatStore.activeTaskId as string,
taskAssigning
);
chatStore.setTaskAssigning(taskAssigning);
}
})
.catch((error) => {
@ -207,9 +187,9 @@ export function WorkSpaceMenu() {
// Cleanup function to remove listener when component unmounts or dependencies change
return cleanup;
}, [
chatStore.activeTaskId,
chatStore.tasks[chatStore.activeTaskId as string]?.webViewUrls,
chatStore.tasks[chatStore.activeTaskId as string]?.taskAssigning,
chatStore.taskId,
chatStore.task?.webViewUrls,
chatStore.task?.taskAssigning,
]);
const agentMap = {
@ -288,15 +268,15 @@ export function WorkSpaceMenu() {
};
const onValueChange = (val: string) => {
if (!chatStore.activeTaskId) return;
if (!chatStore.taskId) return;
if (val === "") {
chatStore.setActiveWorkSpace(chatStore.activeTaskId, "workflow");
chatStore.setActiveWorkSpace("workflow");
return;
}
if (val === "documentWorkSpace") {
chatStore.setNuwFileNum(chatStore.activeTaskId, 0);
chatStore.setNuwFileNum(0);
}
chatStore.setActiveWorkSpace(chatStore.activeTaskId, val);
chatStore.setActiveWorkSpace(val);
window.electronAPI.hideAllWebview();
};
@ -305,13 +285,12 @@ export function WorkSpaceMenu() {
<div className="h-full">
<div className="h-full flex items-center flex-start">
<div className="flex items-center flex-start gap-1 mr-3">
{chatStore.activeTaskId && (
{chatStore.taskId && (
<ToggleGroup
type="single"
size="sm"
value={
chatStore.tasks[chatStore.activeTaskId as string]
.activeWorkSpace as string
chatStore.task?.activeWorkSpace as string
}
onValueChange={onValueChange}
className="flex items-center gap-2"
@ -323,16 +302,13 @@ export function WorkSpaceMenu() {
value="documentWorkSpace"
className="!w-10 !h-10 p-2 relative"
>
{chatStore.tasks[chatStore.activeTaskId as string].nuwFileNum >
{(chatStore.task?.nuwFileNum ?? 0) >
0 && (
<Badge
className="absolute top-0.5 right-0.5 h-4 min-w-4 rounded-full px-1 font-mono tabular-nums bg-icon-cuation text-white-100%"
variant="destructive"
>
{
chatStore.tasks[chatStore.activeTaskId as string]
.nuwFileNum
}
{chatStore.task?.nuwFileNum}
</Badge>
)}
<Inbox className="!h-6 !w-6" />
@ -353,8 +329,7 @@ export function WorkSpaceMenu() {
<ToggleGroup
type="single"
value={
chatStore.tasks[chatStore.activeTaskId as string]
.activeWorkSpace as string
chatStore.task?.activeWorkSpace as string
}
onValueChange={onValueChange}
className="flex items-center gap-2 max-w-[500px] overflow-x-auto scrollbar-horizontal"

View file

@ -40,7 +40,7 @@ export const replayActiveTask = async (
projectStore: ProjectStore,
navigate: NavigateFunction
) => {
const taskId = chatStore.activeTaskId as string;
const taskId = chatStore.taskId as string;
const projectId = projectStore.activeProjectId as string;
if (!taskId || !projectId) {
@ -75,8 +75,8 @@ export const replayActiveTask = async (
}
// Fallback to current task's first message if no question found
if (!question && chatStore.tasks[taskId] && chatStore.tasks[taskId].messages[0]) {
question = chatStore.tasks[taskId].messages[0].content;
if (!question && chatStore.task && chatStore.task.messages[0]) {
question = chatStore.task.messages[0].content;
console.log("[REPLAY] question fall back to ", question);
}

View file

@ -139,9 +139,8 @@ export default function Project() {
};
const handleClickAgent = (taskId: string, agent_id: string) => {
chatStore.setActiveTaskId(taskId);
chatStore.setActiveWorkSpace(taskId, "workflow");
chatStore.setActiveAgent(taskId, agent_id);
chatStore.setActiveWorkSpace("workflow");
chatStore.setActiveAgent(agent_id);
navigate(`/`);
};
@ -157,8 +156,8 @@ export default function Project() {
try {
await proxyFetchDelete(`/api/chat/history/${id}`);
setHistoryTasks((list) => list.filter((item) => item.id !== id));
if (chatStore.tasks[id]) {
chatStore.removeTask(id);
if (chatStore.taskId === id) {
chatStore.removeTask();
}
} catch (error) {
console.error("Failed to delete history task:", error);
@ -196,15 +195,14 @@ export default function Project() {
};
const handleReplay = async (taskId: string, question: string) => {
chatStore.replay(taskId, question, 0);
chatStore.replay(question, 0);
navigate({ pathname: "/" });
};
const handleSetActive = (taskId: string, question: string) => {
const task = chatStore.tasks[taskId];
if (task) {
const hasTask = chatStore.taskId === taskId && chatStore.task;
if (hasTask) {
// if there is a record, display the result
chatStore.setActiveTaskId(taskId);
navigate(`/`);
} else {
// if there is no record, execute replay
@ -213,23 +211,24 @@ export default function Project() {
};
const handleTakeControl = (type: "pause" | "resume", taskId: string) => {
if (!chatStore.task || chatStore.taskId !== taskId) return;
if (type === "pause") {
let { taskTime, elapsed } = chatStore.tasks[taskId];
let { taskTime, elapsed } = chatStore.task;
const now = Date.now();
elapsed += now - taskTime;
chatStore.setElapsed(taskId, elapsed);
chatStore.setTaskTime(taskId, 0);
chatStore.setElapsed(elapsed);
chatStore.setTaskTime(0);
} else {
chatStore.setTaskTime(taskId, Date.now());
chatStore.setTaskTime(Date.now());
}
fetchPut(`/task/${taskId}/take-control`, {
action: type,
});
if (type === "pause") {
chatStore.setStatus(taskId, "pause");
chatStore.setStatus("pause");
} else {
chatStore.setStatus(taskId, "running");
chatStore.setStatus("running");
}
};
@ -282,10 +281,9 @@ export default function Project() {
onTaskSelect={handleSetActive}
onTaskDelete={handleDelete}
onTaskShare={handleShare}
activeTaskId={chatStore.activeTaskId || undefined}
ongoingTasks={chatStore.tasks}
activeTaskId={chatStore.taskId || undefined}
ongoingTasks={chatStore.task && chatStore.taskId ? { [chatStore.taskId]: chatStore.task } : {}}
onOngoingTaskClick={(taskId) => {
chatStore.setActiveTaskId(taskId);
navigate(`/`);
}}
onOngoingTaskPause={(taskId) => handleTakeControl("pause", taskId)}

View file

@ -44,7 +44,7 @@ export default function Home() {
useEffect(() => {
let taskAssigning = [
...(chatStore.tasks[chatStore.activeTaskId as string]?.taskAssigning ||
...(chatStore.task?.taskAssigning ||
[]),
];
let webviews: { id: string; agent_id: string; index: number }[] = [];
@ -75,7 +75,7 @@ export default function Home() {
// capture webview
const captureWebview = async () => {
const activeTask = chatStore.tasks[chatStore.activeTaskId as string];
const activeTask = chatStore.task;
if (!activeTask || activeTask.status === "finished") {
return;
}
@ -83,7 +83,7 @@ export default function Home() {
window.ipcRenderer
.invoke("capture-webview", webview.id)
.then((base64: string) => {
const currentTask = chatStore.tasks[chatStore.activeTaskId as string];
const currentTask = chatStore.task;
if (!currentTask || currentTask.type) return;
let taskAssigning = [
...currentTask.taskAssigning,
@ -100,15 +100,14 @@ export default function Home() {
webview.index
].img = base64;
chatStore.setTaskAssigning(
chatStore.activeTaskId as string,
taskAssigning
);
const { processTaskId, url } =
taskAssigning[searchAgentIndex].activeWebviewIds![
webview.index
];
chatStore.setSnapshotsTemp(chatStore.activeTaskId as string, {
api_task_id: chatStore.activeTaskId,
chatStore.setSnapshotsTemp({
api_task_id: chatStore.taskId,
camel_task_id: processTaskId,
browser_url: url,
image_base64: base64,
@ -150,10 +149,10 @@ export default function Home() {
clearInterval(intervalTimer);
}
};
}, [chatStore.tasks[chatStore.activeTaskId as string]?.taskAssigning]);
}, [chatStore.task?.taskAssigning]);
useEffect(() => {
if (!chatStore.activeTaskId) {
if (!chatStore.taskId) {
projectStore.createProject("new project");
}
@ -194,22 +193,19 @@ export default function Home() {
</ResizablePanel>
<ResizableHandle withHandle={true} className="custom-resizable-handle" />
<ResizablePanel>
{chatStore.tasks[chatStore.activeTaskId as string]
{chatStore.task
?.activeWorkSpace && (
<div className="w-full h-full flex-1 flex flex-col animate-in fade-in-0 pr-2 slide-in-from-right-2 duration-300">
{chatStore.tasks[
chatStore.activeTaskId as string
]?.taskAssigning?.find(
{chatStore.task?.taskAssigning?.find(
(agent) =>
agent.agent_id ===
chatStore.tasks[chatStore.activeTaskId as string]
.activeWorkSpace
chatStore.task?.activeWorkSpace
)?.type === "search_agent" && (
<div className="w-full h-[calc(100vh-104px)] flex-1 flex animate-in fade-in-0 slide-in-from-right-2 duration-300">
<SearchAgentWrokSpace />
</div>
)}
{chatStore.tasks[chatStore.activeTaskId as string]
{chatStore.task
?.activeWorkSpace === "workflow" && (
<div className="w-full h-full flex-1 flex items-center justify-center animate-in fade-in-0 slide-in-from-right-2 duration-300">
<div className="w-full h-full flex flex-col rounded-2xl border border-transparent border-solid p-2 relative">
@ -218,7 +214,7 @@ export default function Home() {
<div className="w-full h-full relative z-10">
<Workflow
taskAssigning={
chatStore.tasks[chatStore.activeTaskId as string]
chatStore.task
?.taskAssigning || []
}
/>
@ -226,21 +222,17 @@ export default function Home() {
</div>
</div>
)}
{chatStore.tasks[
chatStore.activeTaskId as string
]?.taskAssigning?.find(
{chatStore.task?.taskAssigning?.find(
(agent) =>
agent.agent_id ===
chatStore.tasks[chatStore.activeTaskId as string]
.activeWorkSpace
chatStore.task?.activeWorkSpace
)?.type === "developer_agent" && (
<div className="w-full h-[calc(100vh-104px)] flex-1 flex animate-in fade-in-0 slide-in-from-right-2 duration-300">
<TerminalAgentWrokSpace></TerminalAgentWrokSpace>
{/* <Terminal content={[]} /> */}
</div>
)}
{chatStore.tasks[chatStore.activeTaskId as string]
.activeWorkSpace === "documentWorkSpace" && (
{chatStore.task?.activeWorkSpace === "documentWorkSpace" && (
<div className="w-full h-[calc(100vh-104px)] flex-1 flex items-center justify-center animate-in fade-in-0 slide-in-from-right-2 duration-300">
<div className="w-full h-[calc(100vh-104px)] flex flex-col rounded-2xl border border-zinc-300 border-solid relative">
{/*filter blur */}
@ -251,13 +243,10 @@ export default function Home() {
</div>
</div>
)}
{chatStore.tasks[
chatStore.activeTaskId as string
]?.taskAssigning?.find(
{chatStore.task?.taskAssigning?.find(
(agent) =>
agent.agent_id ===
chatStore.tasks[chatStore.activeTaskId as string]
.activeWorkSpace
chatStore.task?.activeWorkSpace
)?.type === "document_agent" && (
<div className="w-full h-[calc(100vh-104px)] flex-1 flex items-center justify-center animate-in fade-in-0 slide-in-from-right-2 duration-300">
<div className="w-full h-[calc(100vh-104px)] flex flex-col rounded-2xl border border-zinc-300 border-solid relative">
@ -265,13 +254,10 @@ export default function Home() {
<div className="absolute inset-0 blur-bg pointer-events-none bg-white-50 rounded-xl"></div>
<div className="w-full h-full relative z-10">
<Folder
data={chatStore.tasks[
chatStore.activeTaskId as string
]?.taskAssigning?.find(
data={chatStore.task?.taskAssigning?.find(
(agent) =>
agent.agent_id ===
chatStore.tasks[chatStore.activeTaskId as string]
.activeWorkSpace
chatStore.task?.activeWorkSpace
)}
/>
</div>

View file

@ -163,7 +163,7 @@ export default function SettingGeneral() {
variant="outline"
size="xs"
onClick={() => {
chatStore.clearTasks();
chatStore.clearTask();
resetInstallation(); // Reset installation state for new account
setNeedsBackendRestart(true); // Mark that backend is restarting

File diff suppressed because it is too large Load diff

View file

@ -93,14 +93,9 @@ const isEmptyProject = (project: Project): boolean => {
}
const chatState = chatStore.getState();
const taskIds = Object.keys(chatState.tasks);
// Check if chat store has only one task
if (taskIds.length !== 1) {
return false;
}
const task = chatState.tasks[taskIds[0]];
// Check if chat store has a task
const task = chatState.task;
if (!task) {
return false;
}
@ -309,9 +304,6 @@ const projectStore = create<ProjectStore>()((set, get) => ({
// Create a new task in the new chat store with the queued content
const newTaskId = newChatStore.getState().create(customTaskId);
//Set the initTask as the active taskId
newChatStore.getState().setActiveTaskId(newTaskId);
return {taskId: newTaskId, chatStore: newChatStore};
},
@ -483,9 +475,9 @@ const projectStore = create<ProjectStore>()((set, get) => ({
const chatStore = project.chatStores[chatId];
if (chatStore) {
// Call replay on the chat store with the taskId, question, and 0 delay
// Call replay on the chat store with the question and delay
try {
await chatStore.getState().replay(taskId, question, 0.2);
await chatStore.getState().replay(question, 0.2);
console.log(`[ProjectStore] Started replay for task ${taskId}`);
} catch (error) {
console.error(`[ProjectStore] Failed to replay task ${taskId}:`, error);
@ -771,12 +763,10 @@ const projectStore = create<ProjectStore>()((set, get) => ({
Object.values(project.chatStores).forEach(chatStore => {
if (chatStore && chatStore.getState) {
const chatState = chatStore.getState();
// Iterate through all tasks in the chat store
Object.values(chatState.tasks).forEach(task => {
if (task && typeof task.tokens === 'number') {
totalTokens += task.tokens;
}
});
// Get tokens from the task
if (chatState.task && typeof chatState.task.tokens === 'number') {
totalTokens += chatState.task.tokens;
}
}
});