Merge branch 'main' into pr-feat-i18n

This commit is contained in:
sw3205933776 2025-09-16 17:52:33 +08:00
commit 09d2d6a05c
40 changed files with 1891 additions and 1162 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 KiB

After

Width:  |  Height:  |  Size: 161 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

After

Width:  |  Height:  |  Size: 162 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 162 KiB

After

Width:  |  Height:  |  Size: 162 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 159 KiB

After

Width:  |  Height:  |  Size: 162 KiB

Before After
Before After

View file

@ -231,6 +231,7 @@ const ToolSelect = forwardRef<
// select management
const addOption = (item: McpItem, isLocal?: boolean) => {
setKeyword("");
const currentSelected = initialSelectedTools || [];
console.log(currentSelected.find((i) => i.id === item.id));
if (isLocal) {
@ -245,6 +246,7 @@ const ToolSelect = forwardRef<
const newSelected = [...currentSelected, { ...item, isLocal }];
onSelectedToolsChange?.(newSelected);
}
};
const removeOption = (item: McpItem) => {
@ -472,7 +474,7 @@ const ToolSelect = forwardRef<
onChange={(e) => setKeyword(e.target.value)}
onFocus={() => setIsOpen(true)}
ref={inputRef}
className="bg-transparent border-none !shadow-none text-sm leading-normal !ring-0 !ring-offset-0 w-10 !h-[20px] p-0"
className="bg-transparent border-none !shadow-none text-sm leading-normal !ring-0 !ring-offset-0 w-auto !h-[20px] p-0"
/>
</div>
</div>

View file

@ -22,7 +22,7 @@ import {
CircleSlash,
} from "lucide-react";
import { useMemo, useState, useRef, useEffect } from "react";
import { TaskState } from "../TaskState";
import { TaskState, TaskStateType } from "../TaskState";
interface TaskCardProps {
taskInfo: any[];
@ -34,6 +34,9 @@ interface TaskCardProps {
onAddTask: () => void;
onUpdateTask: (taskIndex: number, content: string) => void;
onDeleteTask: (taskIndex: number) => void;
selectedStates?: TaskStateType[];
onStateChange?: (selectedStates: TaskStateType[]) => void;
clickable?: boolean;
}
export function TaskCard({
@ -45,6 +48,9 @@ export function TaskCard({
onAddTask,
onUpdateTask,
onDeleteTask,
selectedStates = [],
onStateChange,
clickable = true,
}: TaskCardProps) {
const [isExpanded, setIsExpanded] = useState(true);
const contentRef = useRef<HTMLDivElement>(null);
@ -141,11 +147,33 @@ export function TaskCard({
<div className="flex items-center gap-2 ">
{taskType === 1 && (
<TaskState
done={0}
progress={
taskInfo.filter((task) => task.content !== "").length || 0
done={
taskInfo.filter(
(task) =>
task.status === "completed" || task.status === "failed"
).length || 0
}
skipped={0}
progress={
taskInfo.filter(
(task) =>
task.status !== "completed" &&
task.status !== "failed" &&
task.status !== "skipped" &&
task.status !== "waiting" &&
task.status !== ""
).length || 0
}
skipped={
taskInfo.filter(
(task) =>
task.status === "skipped" ||
task.status === "waiting" ||
task.status === ""
).length || 0
}
selectedStates={selectedStates}
onStateChange={onStateChange}
clickable={clickable}
/>
)}
{taskType !== 1 && (
@ -162,13 +190,19 @@ export function TaskCard({
task.status !== "completed" &&
task.status !== "failed" &&
task.status !== "skipped" &&
task.content !== ""
task.status !== "waiting" &&
task.status !== ""
).length || 0
}
skipped={
taskRunning?.filter((task) => task.status === "skipped")
.length || 0
taskRunning?.filter(
(task) =>
task.status === "skipped" || task.status === "waiting" || task.status === ""
).length || 0
}
selectedStates={selectedStates}
onStateChange={onStateChange}
clickable={clickable}
/>
)}
</div>
@ -279,7 +313,7 @@ export function TaskCard({
{task.status === "running" && (
<LoaderCircle
size={16}
className={`text-icon-success ${
className={`text-icon-information ${
chatStore.tasks[
chatStore.activeTaskId as string
].status === "running" && "animate-spin"
@ -316,7 +350,7 @@ export function TaskCard({
</div>
<div className="flex-1 flex flex-col items-start justify-center">
<div
className={` w-full ${
className={` w-full break-words [overflow-wrap:anywhere] whitespace-pre-line ${
task.status === "failed"
? "text-text-cuation-default"
: task.status === "blocked"

View file

@ -11,11 +11,12 @@ import { proxyFetchGet } from "@/api/http";
import { useNavigate, useSearchParams } from "react-router-dom";
import { NoticeCard } from "./NoticeCard";
import { useAuthStore } from "@/store/authStore";
import { PrivacyDialog } from "../Dialog/Privacy";
import { useTranslation } from "react-i18next";
import { TaskStateType } from "../TaskState";
export default function ChatBox(): JSX.Element {
const [message, setMessage] = useState<string>("");
const [selectedStates, setSelectedStates] = useState<TaskStateType[]>([]);
const chatStore = useChatStore();
const { t } = useTranslation();
const textareaRef = useRef<HTMLTextAreaElement>(null);
@ -464,6 +465,9 @@ export default function ChatBox(): JSX.Element {
);
chatStore.deleteTaskInfo(taskIndex);
}}
selectedStates={selectedStates}
onStateChange={setSelectedStates}
clickable={true}
/>
);
}

View file

@ -0,0 +1,39 @@
import { useCallback } from "react";
import { Button } from "../ui/button";
import { Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "../ui/dialog";
interface Props {
open: boolean;
onOpenChange: (open: boolean) => void;
trigger?: React.ReactNode;
}
export default function CloseNoticeDialog({open, onOpenChange, trigger}: Props) {
const onSubmit = useCallback(() => {
window.electronAPI.closeWindow(true)
}, [])
return <Dialog open={open} onOpenChange={onOpenChange}>
{trigger && <DialogTrigger asChild>{trigger}</DialogTrigger>}
<DialogContent className="sm:max-w-[600px] p-0 !bg-popup-surface gap-0 !rounded-xl border border-zinc-300 shadow-sm">
<DialogHeader className="!bg-popup-surface !rounded-t-xl p-md">
<DialogTitle className="m-0">
Close notice
</DialogTitle>
</DialogHeader>
<div className="flex flex-col gap-md bg-popup-bg p-md">
A task is currently running. Exiting will terminate it. Are you sure you want to exit?
</div>
<DialogFooter className="bg-white-100% !rounded-b-xl p-md">
<DialogClose asChild>
<Button variant="ghost" size="md">
Cancel
</Button>
</DialogClose>
<Button size="md" onClick={onSubmit} variant="primary">
Yes
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
}

View file

@ -337,21 +337,21 @@ export default function HistorySidebar() {
className={`transition-all duration-300 flex justify-start items-center gap-1 px-sm py-xs bg-menutabs-bg-default hover:bg-white-100% rounded-lg border border-solid border-white-100% shadow-history-item ${
agentMap[
taskAssigning.type as keyof typeof agentMap
].borderColor
]?.borderColor
}`}
>
<Bot
className={`w-3 h-3 ${
agentMap[
taskAssigning.type as keyof typeof agentMap
].textColor
]?.textColor
}`}
/>
<div
className={`${
agentMap[
taskAssigning.type as keyof typeof agentMap
].textColor
]?.textColor
} text-xs leading-17 font-medium`}
>
{taskAssigning.name}

View file

@ -6,10 +6,32 @@ import { useAuthStore } from "@/store/authStore";
import { useEffect, useState } from "react";
import { AnimationJson } from "@/components/AnimationJson";
import animationData from "@/assets/animation/onboarding_success.json";
import CloseNoticeDialog from "../Dialog/CloseNotice";
import { useChatStore } from "@/store/chatStore";
const Layout = () => {
const { initState, setInitState, isFirstLaunch, setIsFirstLaunch } =
useAuthStore();
const [isInstalling, setIsInstalling] = useState(false);
const [noticeOpen, setNoticeOpen] = useState(false);
const chatStore = useChatStore();
useEffect(() => {
const handleBeforeClose = () => {
const currentStatus = chatStore.tasks[chatStore.activeTaskId as string]?.status;
if(["pending", "running", "pause"].includes(currentStatus)) {
setNoticeOpen(true);
} else {
window.electronAPI.closeWindow(true);
}
};
window.ipcRenderer.on("before-close", handleBeforeClose);
return () => {
window.ipcRenderer.removeAllListeners("before-close");
};
}, [chatStore.tasks, chatStore.activeTaskId]);
useEffect(() => {
const checkToolInstalled = async () => {
// in render process
@ -25,6 +47,7 @@ const Layout = () => {
};
checkToolInstalled();
}, []);
return (
<div className="h-full flex flex-col">
@ -46,6 +69,10 @@ const Layout = () => {
)}
<Outlet />
<HistorySidebar />
<CloseNoticeDialog
onOpenChange={setNoticeOpen}
open={noticeOpen}
/>
</div>
</div>
);

View file

@ -202,6 +202,8 @@ export default function Home() {
{agentMap[activeAgent?.type as keyof typeof agentMap]?.name}
</div>
<TaskState
all={activeAgent?.tasks?.length || 0}
reAssignTo={activeAgent?.tasks?.filter((task) => task.reAssignTo).length || 0}
done={
activeAgent?.tasks?.filter(
(task) =>
@ -213,11 +215,12 @@ export default function Home() {
(task) =>
task.status !== "failed" &&
task.status !== "completed" &&
task.status !== "skipped"
task.status !== "skipped"&&
task.status !== "waiting"
).length || 0
}
skipped={
activeAgent?.tasks?.filter((task) => task.status === "skipped")
activeAgent?.tasks?.filter((task) => task.status === "skipped"||task.status==="waiting")
.length || 0
}
/>

View file

@ -1,49 +1,186 @@
import { CircleCheckBig, LoaderCircle } from "lucide-react";
import { CircleCheckBig, CircleSlash2, LoaderCircle } from "lucide-react";
import { useChatStore } from "@/store/chatStore";
import { useTranslation } from "react-i18next";
export const TaskState = ({
done,
progress,
skipped,
}: {
export type TaskStateType =
| "all"
| "done"
| "reassigned"
| "ongoing"
| "pending";
export interface TaskStateProps {
all?: number;
done: number;
progress: number;
skipped: number;
}) => {
reAssignTo?: number;
selectedStates?: TaskStateType[];
onStateChange?: (selectedStates: TaskStateType[]) => void;
clickable?: boolean;
}
export const TaskState = ({
all,
done,
reAssignTo,
progress,
skipped,
selectedStates = [],
onStateChange,
clickable = true,
}: TaskStateProps) => {
const chatStore = useChatStore();
const { t } = useTranslation();
const handleStateClick = (state: TaskStateType) => {
if (!clickable || !onStateChange) return;
let newSelectedStates: TaskStateType[];
if (state === "all") {
newSelectedStates = selectedStates.includes("all") ? [] : ["all"];
} else {
const otherStates = selectedStates.filter((s) => s !== "all");
if (otherStates.includes(state)) {
newSelectedStates = otherStates.filter((s) => s !== state);
} else {
newSelectedStates = [...otherStates, state];
}
}
onStateChange(newSelectedStates);
};
const isSelected = (state: TaskStateType) => {
return selectedStates.includes(state);
};
const fadeWidthClass = (selected: boolean) =>
`inline-block overflow-hidden align-bottom transition-all duration-300 ease-in-out
${selected ? "max-w-[40px] opacity-100" : "max-w-0 opacity-0"}
group-hover:max-w-[40px] group-hover:opacity-100`;
return (
<div>
<div className="w-auto bg-transparent flex items-center gap-1">
<div className="flex gap-1 items-center py-0.5">
<CircleCheckBig className="w-4 h-4 text-icon-primary" />
<span className="text-text-body text-xs leading-tight font-normal">
{done} {t("chat.done")}
<div className="w-auto bg-transparent flex items-center gap-1 flex-wrap">
{/* All */}
{all && (
<div
className={`group hover:bg-tag-surface flex gap-xs items-center py-0.5 px-2 transition-all duration-200 ${
isSelected("all") ? "bg-tag-surface" : "bg-transparent"
} ${clickable ? "cursor-pointer" : ""}`}
onClick={() => handleStateClick("all")}
>
<span className="text-xs font-normal text-text-body">
All{" "}
<span className={fadeWidthClass(isSelected("all"))}>{all}</span>
</span>
</div>
)}
{/* Done */}
<div
className={`group hover:bg-tag-surface flex gap-xs items-center px-0.5 py-0.5 transition-all duration-200 ${
isSelected("done") && "bg-tag-surface"
} ${
clickable && "cursor-pointer hover:opacity-80 transition-opacity"
}`}
onClick={() => handleStateClick("done")}
>
<CircleCheckBig
className={`w-[10px] h-[10px] text-icon-secondary group-hover:text-icon-success ${
isSelected("done") && "text-icon-success"
}`}
/>
<span
className={`transition-all duration-200 text-xs leading-tight font-normal text-text-label group-hover:text-text-success ${
isSelected("done") && "text-text-success"
}`}
>
Done{" "}
<span className={fadeWidthClass(isSelected("done"))}>{done}</span>
</span>
</div>
{progress !== 0 && (
<div className="flex gap-1 items-center py-0.5">
<LoaderCircle
className={`w-4 h-4 text-icon-success ${
chatStore.tasks[chatStore.activeTaskId as string].status ===
"running" && "animate-spin"
}`}
/>
<span className="text-text-success text-xs leading-tight font-normal">
{progress} {t("chat.in-progress")}
{/* Reassigned */}
{reAssignTo && <div
className={`group hover:bg-tag-surface flex gap-xs items-center px-0.5 py-0.5 transition-all duration-200 ${
isSelected("reassigned") && "bg-tag-surface"
} ${
clickable && "cursor-pointer hover:opacity-80 transition-opacity"
}`}
onClick={() => handleStateClick("reassigned")}
>
<CircleSlash2
className={`w-[10px] h-[10px] text-icon-secondary group-hover:text-icon-warning ${
isSelected("reassigned") && "text-icon-warning"
}`}
/>
<span
className={`transition-all duration-200 text-xs leading-tight font-normal text-text-label group-hover:text-text-warning ${
isSelected("reassigned") && "text-text-warning"
}`}
>
Reassigned{" "}
<span className={fadeWidthClass(isSelected("reassigned"))}>
{reAssignTo}
</span>
</div>
)}
{skipped !== 0 && (
<div className="flex gap-1 items-center py-0.5">
<LoaderCircle
className={`w-4 h-4 text-icon-secondary`}
/>
<span className="text-text-label text-xs leading-tight font-normal">
{skipped} {t("chat.unfinished")}
</span>
</div>}
{/* Ongoing */}
<div
className={`group hover:bg-tag-surface flex gap-xs items-center px-0.5 py-0.5 ${
isSelected("ongoing") && "bg-tag-surface"
} ${
clickable && "cursor-pointer hover:opacity-80 transition-opacity"
}`}
onClick={() => handleStateClick("ongoing")}
>
<LoaderCircle
className={`w-[10px] h-[10px] text-icon-secondary group-hover:text-icon-information ${
isSelected("ongoing") && "!text-icon-information"
} ${
chatStore.tasks[chatStore.activeTaskId as string]?.status ===
"running" && "animate-spin"
}`}
/>
<span
className={`transition-all duration-200 text-xs leading-tight font-normal text-text-label group-hover:text-text-information ${
isSelected("ongoing") && "!text-text-information"
}`}
>
Ongoing{" "}
<span className={fadeWidthClass(isSelected("ongoing"))}>
{progress}
</span>
</div>
)}
</span>
</div>
{/* Pending */}
<div
className={`group hover:bg-tag-surface flex gap-xs items-center px-0.5 py-0.5 ${
isSelected("pending") ? "bg-tag-surface" : "bg-transparent"
} ${
clickable && "cursor-pointer hover:opacity-80 transition-opacity"
}`}
onClick={() => handleStateClick("pending")}
>
<LoaderCircle
className={`w-[10px] h-[10px] text-icon-secondary group-hover:text-primary-foreground ${
isSelected("pending") && "text-primary-foreground"
}`}
/>
<span
className={`text-xs leading-tight font-normal text-text-label group-hover:text-primary-foreground ${
isSelected("pending") && "text-primary-foreground"
}`}
>
Pending{" "}
<span className={fadeWidthClass(isSelected("pending"))}>
{skipped}
</span>
</span>
</div>
</div>
</div>
);

View file

@ -133,7 +133,7 @@ export default function Workflow({
return prev.map((node) => {
// calculate node width and position based on expansion state
const nodeWidth = node.data.isExpanded ? 560 : 280;
const nodeWidth = node.data.isExpanded ? 684 : 342;
const newPosition = { x: currentX, y: node.position.y };
currentX += nodeWidth + 20; // 20 is the spacing between nodes
@ -205,7 +205,7 @@ export default function Workflow({
};
// calculate node width and position based on expansion state
const nodeWidth = updatedNode.data.isExpanded ? 560 : 280;
const nodeWidth = updatedNode.data.isExpanded ? 684 : 342;
const newPosition = { x: currentX, y: node.position.y };
currentX += nodeWidth + 20; // 20 is the spacing between nodes
@ -245,7 +245,7 @@ export default function Workflow({
},
position: isEditMode
? node.position
: { x: index * 300 + 8, y: 16 },
: { x: index * (342+20) + 8, y: 16 },
};
} else {
return {
@ -259,7 +259,7 @@ export default function Workflow({
isEditMode: isEditMode,
workerInfo: agent?.workerInfo,
},
position: { x: index * 300 + 8, y: 16 },
position: { x: index * (342+20) + 8, y: 16 },
type: "node",
};
}
@ -303,7 +303,7 @@ export default function Workflow({
<div className="text-text-body font-bold text-lg leading-relaxed">
{t("workforce.your-ai-workforce")}
</div>
<div className="flex items-center justify-center gap-sm ">
<div className="flex items-center justify-center gap-sm">
{/* <Button
variant="outline"
size="icon"

View file

@ -17,6 +17,7 @@ import {
Trash2,
Edit,
SquareChevronLeft,
CircleSlash2,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import Folder from "../Folder";
@ -27,7 +28,7 @@ import ShinyText from "../ui/ShinyText/ShinyText";
import { MarkDown } from "./MarkDown";
import { Tooltip, TooltipTrigger } from "../ui/tooltip";
import { TooltipContent } from "@radix-ui/react-tooltip";
import { TaskState } from "../TaskState";
import { TaskState, TaskStateType } from "../TaskState";
import {
Popover,
PopoverClose,
@ -58,6 +59,46 @@ interface NodeProps {
export function Node({ id, data }: NodeProps) {
const [isExpanded, setIsExpanded] = useState(data.isExpanded);
const [selectedTask, setSelectedTask] = useState<any>(null);
const [selectedStates, setSelectedStates] = useState<TaskStateType[]>(['all']);
const [filterTasks, setFilterTasks] = useState<any[]>([]);
useEffect(() => {
const tasks = data.agent?.tasks || [];
if (selectedStates.includes("all") || selectedStates.length === 0) {
setFilterTasks(tasks);
} else {
const newFiltered = tasks.filter((task) => {
return selectedStates.some((state) => {
switch (state) {
case "done":
return (task.status === "completed" || task.status === "failed") && !task.reAssignTo;
case "reassigned":
return !!task.reAssignTo;
case "ongoing":
return (
task.status !== "failed" &&
task.status !== "completed" &&
task.status !== "skipped" &&
task.status !== "waiting" &&
task.status !== "" &&
!task.reAssignTo
);
case "pending":
return (
(task.status === "skipped" ||
task.status === "waiting" ||
task.status === "") &&
!task.reAssignTo
);
default:
return false;
}
});
});
setFilterTasks(newFiltered);
}
}, [selectedStates, data.agent?.tasks]);
const chatStore = useChatStore();
const { setCenter, getNode, setViewport, setNodes } = useReactFlow();
@ -72,7 +113,7 @@ export function Node({ id, data }: NodeProps) {
// manually control node size
useEffect(() => {
if (data.isEditMode) {
const targetWidth = isExpanded ? 560 : 280;
const targetWidth = isExpanded ? 684 : 342;
const targetHeight = 600;
setNodes((nodes) =>
@ -251,15 +292,15 @@ export function Node({ id, data }: NodeProps) {
const list = taskId.split(".");
let idStr = "";
list.shift();
list.map((i: string) => {
idStr += Number(i) + ".";
list.map((i: string, index: number) => {
idStr += Number(i) + (index === list.length - 1 ? "" : ".");
});
return idStr;
};
return (
<>
<NodeResizer
minWidth={isExpanded ? 560 : 280}
minWidth={isExpanded ? 684 : 342}
minHeight={300}
isVisible={data.isEditMode}
keepAspectRatio={false}
@ -276,10 +317,10 @@ export function Node({ id, data }: NodeProps) {
ref={nodeRef}
className={`${
data.isEditMode
? `w-full ${isExpanded ? "min-w-[560px]" : "min-w-[280px]"}`
? `w-full ${isExpanded ? "min-w-[560px]" : "min-w-[342px]"}`
: isExpanded
? "w-[560px]"
: "w-[280px]"
? "w-[684px]"
: "w-[342px]"
} ${
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 ${
@ -292,7 +333,7 @@ export function Node({ id, data }: NodeProps) {
>
<div
className={`py-2 px-3 pr-0 flex flex-col ${
data.isEditMode ? "flex-1 min-w-[280px]" : "w-[280px] "
data.isEditMode ? "flex-1 min-w-[342px]" : "w-[342px] "
}`}
>
<div className=" flex items-center justify-between gap-sm pr-3">
@ -447,28 +488,43 @@ export function Node({ id, data }: NodeProps) {
</div>
{data.agent?.tasks && data.agent?.tasks.length > 0 && (
<div className="flex flex-col items-start justify-between gap-1 pt-sm border-[0px] border-t border-solid border-task-border-default pr-3">
<div className="font-bold leading-tight text-xs">Subtasks</div>
{/* <div className="font-bold leading-tight text-xs">Subtasks</div> */}
<div className="flex-1 flex justify-end">
<TaskState
all={data.agent.tasks?.length || 0}
done={
data.agent?.tasks?.filter(
(task) =>
task.status === "failed" || task.status === "completed"
(task.status === "failed" || task.status === "completed") && !task.reAssignTo
).length || 0
}
reAssignTo={
data.agent.tasks?.filter((task) => task.reAssignTo)
?.length || 0
}
progress={
data.agent?.tasks?.filter(
(task) =>
task.status !== "failed" &&
task.status !== "completed" &&
task.status !== "skipped"
task.status !== "skipped" &&
task.status !== "waiting" &&
task.status !== "" &&
!task.reAssignTo
).length || 0
}
skipped={
data.agent?.tasks?.filter(
(task) => task.status === "skipped"
(task) =>
(task.status === "skipped" ||
task.status === "waiting" ||
task.status === "") &&
!task.reAssignTo
).length || 0
}
selectedStates={selectedStates}
onStateChange={setSelectedStates}
clickable={true}
/>
</div>
</div>
@ -489,7 +545,7 @@ export function Node({ id, data }: NodeProps) {
}}
>
{data.agent?.tasks &&
data.agent?.tasks.map((task, index) => {
filterTasks.map((task, index) => {
return (
<div
onClick={() => {
@ -510,7 +566,9 @@ export function Node({ id, data }: NodeProps) {
}}
key={`taskList-${task.id}-${task.failure_count}`}
className={`rounded-lg flex gap-2 py-sm px-sm transition-all duration-300 ease-in-out animate-in fade-in-0 slide-in-from-left-2 ${
task.status === "completed"
task.reAssignTo
? "bg-task-fill-warning"
: task.status === "completed"
? "bg-green-50"
: task.status === "failed"
? "bg-task-fill-error"
@ -543,53 +601,89 @@ export function Node({ id, data }: NodeProps) {
: "border-transparent"
}`}
>
<div className="pt-0.5">
{task.status === "running" && (
<LoaderCircle
size={16}
className={`text-icon-success ${
chatStore.tasks[chatStore.activeTaskId as string]
.status === "running" && "animate-spin"
}`}
/>
)}
{task.status === "skipped" && (
<LoaderCircle
size={16}
className={`text-icon-secondary `}
/>
)}
{task.status === "completed" && (
<CircleCheckBig
size={16}
className="text-icon-success"
/>
)}
{task.status === "failed" && (
<CircleSlash size={16} className="text-icon-cuation" />
)}
{task.status === "blocked" && (
<TriangleAlert
size={16}
className="text-icon-warning"
/>
)}
{(task.status === "" || task.status === "waiting") && (
<Circle size={16} className="text-slate-400" />
<div className="">
{task.reAssignTo ? (
// reassign to other agent
<CircleSlash2 size={16} className="text-icon-warning" />
) : (
// normal task
<>
{task.status === "running" && (
<LoaderCircle
size={16}
className={`text-icon-information ${
chatStore.tasks[
chatStore.activeTaskId as string
].status === "running" && "animate-spin"
}`}
/>
)}
{task.status === "skipped" && (
<LoaderCircle
size={16}
className={`text-icon-secondary `}
/>
)}
{task.status === "completed" && (
<CircleCheckBig
size={16}
className="text-icon-success"
/>
)}
{task.status === "failed" && (
<CircleSlash
size={16}
className="text-icon-cuation"
/>
)}
{task.status === "blocked" && (
<TriangleAlert
size={16}
className="text-icon-warning"
/>
)}
{(task.status === "" ||
task.status === "waiting") && (
<Circle size={16} className="text-slate-400" />
)}
</>
)}
</div>
<div className="flex-1 flex flex-col items-start justify-center">
<div
className={` w-full flex-grow-0 ${
className={`w-full flex-grow-0 ${
task.status === "failed"
? "text-text-cuation-default"
: task.status === "blocked"
? "text-text-body"
: "text-text-primary"
} text-sm font-medium leading-13 select-text pointer-events-auto break-all text-wrap whitespace-pre-line`}
} text-xs font-medium leading-13 select-text pointer-events-auto break-all text-wrap whitespace-pre-line`}
>
{getTaskId(task.id)}
{task.content}
<div className="flex items-center gap-sm">
<div className="text-text-body text-xs font-bold leading-13">
No. {getTaskId(task.id)}
</div>
{task.reAssignTo ? (
<div className="text-text-warning text-xs font-bold leading-none rounded-lg px-1 py-0.5 bg-tag-fill-document">
Reassigned to {task.reAssignTo}
</div>
) : (
(task.failure_count ?? 0) > 0 && (
<div
className={`${
task.status === "failed"
? "bg-red-100 text-text-cuation"
: task.status === "completed"
? "bg-tag-fill-developer text-text-success-default"
: "bg-tag-surface-hover text-text-label"
} text-xs font-bold leading-none rounded-lg px-1 py-0.5`}
>
Attempt {task.failure_count}
</div>
)
)}
</div>
<div>{task.content}</div>
</div>
{task?.status === "running" && (
<div className="flex items-center gap-2 mt-xs animate-in fade-in-0 slide-in-from-bottom-2 duration-400">
@ -597,7 +691,7 @@ export function Node({ id, data }: NodeProps) {
{task.toolkits &&
task.toolkits.length > 0 &&
task.toolkits
.filter((tool) => tool.toolkitName !== "notice")
.filter((tool: any) => tool.toolkitName !== "notice")
.at(-1)?.toolkitStatus === "running" && (
<div className="flex-1 min-w-0 flex justify-start items-center gap-sm animate-in fade-in-0 slide-in-from-right-2 duration-300">
{agentMap[data.type]?.icon ?? (
@ -621,11 +715,6 @@ export function Node({ id, data }: NodeProps) {
)}
</div>
)}
{(task.failure_count ?? 0) > 0&& (
<div className="text-text-cuation-default text-xs leading-17">
retry {task.failure_count} times
</div>
)}
</div>
</div>
);
@ -636,7 +725,7 @@ export function Node({ id, data }: NodeProps) {
<div
key={selectedTask?.id || "empty"}
className={`${
data.isEditMode ? "flex-1" : "w-[280px]"
data.isEditMode ? "flex-1" : "w-[342px]"
} flex flex-col gap-sm border-l bg-worker-surface-secondary rounded-r-xl px-sm pr-0 py-3 pt-sm animate-in fade-in-0 slide-in-from-right-2 duration-300 `}
>
<div

View file

@ -344,21 +344,21 @@ export default function Home() {
className={`transition-all duration-300 flex justify-start items-center gap-1 px-sm py-xs bg-menutabs-bg-default rounded-lg border border-solid border-white-100% ${
agentMap[
taskAssigning.type as keyof typeof agentMap
].borderColor
]?.borderColor
}`}
>
<Bot
className={`w-3 h-3 ${
agentMap[
taskAssigning.type as keyof typeof agentMap
].textColor
]?.textColor
}`}
/>
<div
className={`${
agentMap[
taskAssigning.type as keyof typeof agentMap
].textColor
]?.textColor
} text-xs leading-17 font-medium`}
>
{taskAssigning.name}

View file

@ -21,6 +21,7 @@ import { useAuthStore } from "@/store/authStore";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { ConfigFile } from "electron/main/utils/mcpConfig";
export default function SettingMCP() {
const navigate = useNavigate();
@ -197,13 +198,28 @@ export default function SettingMCP() {
setSaving(true);
setErrorMsg(null);
try {
await proxyFetchPut(`/api/mcp/users/${showConfig.id}`, {
const mcpData = {
mcp_name: configForm.mcp_name,
mcp_desc: configForm.mcp_desc,
command: configForm.command,
args: arrayToArgsJson(configForm.argsArr),
env: configForm.env,
});
}
await proxyFetchPut(`/api/mcp/users/${showConfig.id}`, mcpData);
if (window.ipcRenderer) {
//Partial payload to empty env {}
const payload: any = {
description: configForm.mcp_desc,
command: configForm.command,
args: arrayToArgsJson(configForm.argsArr),
};
if (configForm.env && Object.keys(configForm.env).length > 0) {
payload.env = configForm.env;
}
window.ipcRenderer.invoke("mcp-update", mcpData.mcp_name, payload);
}
setShowConfig(null);
fetchList();
} catch (err: any) {
@ -238,9 +254,27 @@ export default function SettingMCP() {
setInstalling(true);
try {
if (addType === "local") {
let data;
let data:ConfigFile;
try {
data = JSON.parse(localJson);
// validate mcpServers structure
if (!data.mcpServers || typeof data.mcpServers !== "object") {
throw new Error("Invalid mcpServers");
}
// check for name conflicts with existing items
const serverNames = Object.keys(data.mcpServers);
const conflict = serverNames.find((name) =>
items.some((d) => d.mcp_name === name)
);
if (conflict) {
toast.error(`MCP server "${conflict}" already exists`, {
closeButton: true,
});
setInstalling(false);
return;
}
} catch (e) {
toast.error(t("setting.invalid-json"), { closeButton: true });
setInstalling(false);
@ -254,19 +288,14 @@ export default function SettingMCP() {
}
if (window.ipcRenderer) {
const mcpServers = data["mcpServers"];
Object.entries(mcpServers).forEach(async ([key, value]) => {
for (const [key, value] of Object.entries(mcpServers)) {
await window.ipcRenderer.invoke("mcp-install", key, value);
});
}
}
}
setShowAdd(false);
setLocalJson(`{
"mcp_id": 0,
"mcp_name": "",
"mcp_desc": "",
"command": "",
"args": "",
"env": {}
"mcpServers": {}
}`);
setRemoteName("");
setRemoteUrl("");
@ -337,13 +366,13 @@ export default function SettingMCP() {
{!isLoading && !error && items.length === 0 && (
<div className="text-center py-8 text-gray-400">{t("setting.no-mcp-servers")}</div>
)}
<MCPList
{!isLoading && <MCPList
items={items}
onSetting={setShowConfig}
onDelete={setDeleteTarget}
onSwitch={handleSwitch}
switchLoading={switchLoading}
/>
/>}
<MCPConfigDialog
open={!!showConfig}
form={configForm}

View file

@ -684,9 +684,6 @@ export default function SettingModels() {
<SelectItem value="gpt-5">GPT-5</SelectItem>
<SelectItem value="gpt-5-mini">GPT-5 mini</SelectItem>
<SelectItem value="gpt-5-nano">GPT-5 nano</SelectItem>
<SelectItem value="claude-opus-4-1-20250805">
Claude Opus 4.1
</SelectItem>
<SelectItem value="claude-sonnet-4-20250514">
Claude Sonnet 4
</SelectItem>

View file

@ -34,7 +34,7 @@ export default function MCPConfigDialog({ open, form, mcp, onChange, onSave, onC
<form onSubmit={onSave} className="space-y-4">
<div>
<label className="block text-sm font-medium mb-1">{t("setting.name")}</label>
<input autoComplete="off" className="w-full border rounded px-3 py-2 text-sm" value={form.mcp_name} onChange={e => onChange({ ...form, mcp_name: e.target.value })} disabled={loading} />
<input autoComplete="off" className="w-full border rounded px-3 py-2 text-sm" value={form.mcp_name} onChange={e => onChange({ ...form, mcp_name: e.target.value })} disabled readOnly />
</div>
<div>
<label className="block text-sm font-medium mb-1">{t("setting.description")}</label>

View file

@ -12,9 +12,9 @@ interface MCPListProps {
export default function MCPList({ items, onSetting, onDelete, onSwitch, switchLoading }: MCPListProps) {
return (
<div className='pt-4'>
{items.map(item => (
{items.map((item) => (
<MCPListItem
key={item.mcp_id}
key={item.id}
item={item}
onSetting={onSetting}
onDelete={onDelete}

View file

@ -1,11 +1,37 @@
export function parseArgsToArray(args: string): string[] {
try {
// Try parsing as JSON array first
const arr = JSON.parse(args);
if (Array.isArray(arr)) return arr.map(String);
} catch { }
// Handle malformed JSON by manually trimming { } and trying again
if (args.trim().startsWith('{') && args.trim().endsWith('}')) {
const trimmed = args.trim().slice(1, -1); // Remove { }
try {
// Try parsing the trimmed version as JSON array
const arr = JSON.parse(`[${trimmed}]`);
if (Array.isArray(arr)) return arr.map(String);
} catch { }
// If still fails, treat as comma-separated
if (trimmed.trim()) {
return trimmed.split(',').map(arg => arg.trim()).filter(arg => arg !== '');
}
}
// If not JSON, treat as comma-separated string
if (args.trim()) {
return args.split(',').map(arg => arg.trim()).filter(arg => arg !== '');
}
return [];
}
export function arrayToArgsJson(arr: string[]): string {
return JSON.stringify(arr.filter(v => v.trim() !== ''));
const filtered = arr.filter(v => v.trim() !== '');
if (filtered.length === 0) return '';
// Return as JSON stringified array
return JSON.stringify(filtered);
}

View file

@ -450,7 +450,7 @@ const chatStore = create<ChatStore>()(
let taskRunning = [...tasks[taskId].taskRunning]
let taskAssigning = [...tasks[taskId].taskAssigning]
const targetTaskIndex = taskRunning.findIndex((task) => task.id === task_id)
const targetTaskAssigningIndex = taskAssigning.findIndex((agent) => agent.tasks.find((task: TaskInfo) => task.id === task_id && (task.failure_count == 0 || !task.failure_count)))
const targetTaskAssigningIndex = taskAssigning.findIndex((agent) => agent.tasks.find((task: TaskInfo) => task.id === task_id && !task.reAssignTo))
if (targetTaskAssigningIndex !== -1) {
const taskIndex = taskAssigning[targetTaskAssigningIndex].tasks.findIndex((task: TaskInfo) => task.id === task_id)
taskAssigning[targetTaskAssigningIndex].tasks[taskIndex].status = state === "DONE" ? "completed" : "failed";
@ -484,20 +484,19 @@ const chatStore = create<ChatStore>()(
content: targetResult,
step: "failed",
})
setStatus(taskId, 'pause')
}
}
}
if (targetTaskIndex !== -1) {
console.log("targetTaskIndex", targetTaskIndex,state)
taskRunning[targetTaskIndex].status = state === "DONE" ? "completed" : "failed";
}
setTaskRunning(taskId, taskRunning)
setTaskAssigning(taskId, taskAssigning)
return;
}
// Activate agent
if (agentMessages.step === "activate_agent" || agentMessages.step === "deactivate_agent") {
let taskAssigning = [...tasks[taskId].taskAssigning]
@ -540,7 +539,6 @@ const chatStore = create<ChatStore>()(
setTaskAssigning(taskId, [...taskAssigning]);
}
if (agentMessages.step === "deactivate_agent") {
taskAssigning[agentIndex].status = "completed";
if (message) {
const index = taskAssigning[agentIndex].log.findLastIndex((log) => log.data.method_name === agentMessages.data.method_name && log.data.toolkit_name === agentMessages.data.toolkit_name)
if (index != -1) {
@ -549,12 +547,11 @@ const chatStore = create<ChatStore>()(
}
}
// const taskIndex = taskRunning!.findLastIndex((task) => task.agent?.agent_id === agent_id && task.status !== 'completed' && task.status !== 'failed');
const taskIndex = taskRunning.findIndex((task) => task.id === process_task_id);
if (taskIndex !== -1) {
taskRunning![taskIndex].agent!.status = "completed";
taskRunning![taskIndex]!.status = "completed";
}
// const taskIndex = taskRunning.findIndex((task) => task.id === process_task_id);
// if (taskIndex !== -1) {
// taskRunning![taskIndex].agent!.status = "completed";
// taskRunning![taskIndex]!.status = "completed";
// }
if (!type && historyId) {
@ -580,11 +577,12 @@ const chatStore = create<ChatStore>()(
if (agentMessages.step === "assign_task") {
if (!agentMessages.data?.assignee_id || !agentMessages.data?.task_id) return;
const { assignee_id, task_id, content = "", state: taskState } = agentMessages.data as any;
const { assignee_id, task_id, content = "", state: taskState, failure_count } = agentMessages.data as any;
let taskAssigning = [...tasks[taskId].taskAssigning]
let taskRunning = [...tasks[taskId].taskRunning]
let taskInfo = [...tasks[taskId].taskInfo]
// Find the index of the agent corresponding to assignee_id
const assigneeAgentIndex = taskAssigning!.findIndex((agent: Agent) => agent.agent_id === assignee_id);
// Find task corresponding to task_id
const task = taskInfo!.find((task: TaskInfo) => task.id === task_id);
@ -594,6 +592,26 @@ const chatStore = create<ChatStore>()(
if (assigneeAgentIndex === -1) return;
const taskAgent = taskAssigning![assigneeAgentIndex];
// Find the agent to reassign the task to
const target = taskAssigning
.map((agent, agentIndex) => {
if (agent.agent_id === assignee_id) return null
const taskIndex = agent.tasks.findIndex(
(task: TaskInfo) => task.id === task_id && !task.reAssignTo
)
return taskIndex !== -1 ? { agentIndex, taskIndex } : null
})
.find(Boolean)
if (target) {
const { agentIndex, taskIndex } = target
const agentName = taskAssigning.find((agent: Agent) => agent.agent_id === assignee_id)?.name
taskAssigning[agentIndex].tasks[taskIndex].reAssignTo = agentName
}
// If the state is "waiting", only mark it in the agent's task list and do not add it to taskRunning
if (taskState === "waiting") {
if (!taskAssigning[assigneeAgentIndex].tasks.find(item => item.id === task_id)) {
@ -607,10 +625,13 @@ const chatStore = create<ChatStore>()(
if (taskAssigning && taskAssigning[assigneeAgentIndex]) {
// Check if task already exists in the agent's task list
const existingTaskIndex = taskAssigning[assigneeAgentIndex].tasks.findIndex(item => item.id === task_id);
if (existingTaskIndex !== -1) {
// Task already exists, update its status
taskAssigning[assigneeAgentIndex].tasks[existingTaskIndex].status = "running";
if (failure_count !== 0) {
taskAssigning[assigneeAgentIndex].tasks[existingTaskIndex].failure_count = failure_count;
}
} else {
// Task doesn't exist, add it
let taskTemp = null
@ -624,7 +645,7 @@ const chatStore = create<ChatStore>()(
taskAssigning[assigneeAgentIndex].tasks.push(taskTemp ?? { id: task_id, content, status: "running", });
}
}
// Only update or add to taskRunning, never duplicate
if (taskRunningIndex === -1) {
// Task not in taskRunning, add it
@ -640,7 +661,6 @@ const chatStore = create<ChatStore>()(
// Task already in taskRunning, update it
taskRunning![taskRunningIndex] = {
...taskRunning![taskRunningIndex],
content,
status: "",
agent: JSON.parse(JSON.stringify(taskAgent)),
};
@ -1638,7 +1658,17 @@ const chatStore = create<ChatStore>()(
clearTasks: () => {
const { create } = get()
console.log('clearTasks')
fetchDelete('/task/stop-all')
window.ipcRenderer.invoke('restart-backend')
.then((res) => {
console.log('restart-backend', res)
})
.catch((error) => {
console.error('Error in clearTasks cleanup:', error)
})
// Immediately create new task to maintain UI responsiveness
const newTaskId = create()
set((state) => ({
...state,

View file

@ -16,6 +16,7 @@ declare global {
toolkitStatus?: AgentStatus;
}[];
failure_count?: number;
reAssignTo?:string;
}
interface FileInfo {
name: string;