846 feature request onboarding flow Redesign (#1058)
Some checks are pending
CodeQL Advanced / Analyze (actions) (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
CodeQL Advanced / Analyze (python) (push) Waiting to run
Pre-commit / pre-commit (push) Waiting to run
Test / Run Python Tests (push) Waiting to run

Co-authored-by: 4pmtong <web_chentong@163.com>
This commit is contained in:
Douglas Lai 2026-02-06 14:03:32 +00:00 committed by GitHub
parent 09200a8cf6
commit 77112a227d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
54 changed files with 4358 additions and 4008 deletions

View file

@ -6,6 +6,7 @@ package/@stackframe/
dist/
dist-electron/
build/
release/
# Cache
.cache/

View file

@ -94,6 +94,18 @@
]
}
},
"dmg": {
"background": "src/assets/dmg-background.png",
"window": {
"width": 660,
"height": 400
},
"iconSize": 128,
"contents": [
{ "x": 180, "y": 200, "type": "file" },
{ "x": 480, "y": 200, "type": "link", "path": "/Applications" }
]
},
"win": {
"icon": "build/icon.ico",
"artifactName": "${productName}.Setup.${version}.exe",

View file

@ -52,6 +52,7 @@ export default [
'dist/**',
'dist-electron/**',
'build/**',
'release/**',
// Cache
'.cache/**',
'.vite/**',

View file

@ -1,4 +1,5 @@
#!/usr/bin/env node
/* global console process */
/**
* Comprehensive test for prebuilt dependencies
* Verifies pyvenv.cfg and Python symlinks are correct
@ -96,7 +97,7 @@ function testPythonSymlinks(venvPath, venvName) {
fail(` Target: ${target}`);
warn('Run: npm run fix-symlinks');
} catch (err) {
fail(`${symlinkName}: missing`);
fail(`${symlinkName}: missing - ${err.message}`);
warn('Run: npm run fix-symlinks');
}
continue;
@ -172,12 +173,12 @@ function main() {
const venvs = [
{
path: path.join(projectRoot, 'resources', 'prebuilt', 'venv'),
name: 'Backend venv'
name: 'Backend venv',
},
{
path: path.join(projectRoot, 'resources', 'prebuilt', 'terminal_venv'),
name: 'Terminal venv'
}
name: 'Terminal venv',
},
];
for (const venv of venvs) {
@ -193,7 +194,9 @@ function main() {
}
console.log('\n' + '='.repeat(50));
console.log(`\n📊 Test Results: ${testsPassed} passed, ${testsFailed} failed`);
console.log(
`\n📊 Test Results: ${testsPassed} passed, ${testsFailed} failed`
);
if (testsFailed > 0) {
console.log('\n❌ Some tests failed!');

View file

@ -1,4 +1,5 @@
#!/usr/bin/env node
/* global console process */
/**
* Test script to verify pyvenv.cfg fix works correctly
*/
@ -17,12 +18,24 @@ function testVenvFix() {
const testCases = [
{
name: 'Backend venv',
path: path.join(projectRoot, 'resources', 'prebuilt', 'venv', 'pyvenv.cfg')
path: path.join(
projectRoot,
'resources',
'prebuilt',
'venv',
'pyvenv.cfg'
),
},
{
name: 'Terminal venv',
path: path.join(projectRoot, 'resources', 'prebuilt', 'terminal_venv', 'pyvenv.cfg')
}
path: path.join(
projectRoot,
'resources',
'prebuilt',
'terminal_venv',
'pyvenv.cfg'
),
},
];
let allPassed = true;
@ -52,12 +65,15 @@ function testVenvFix() {
console.log(` Home: ${homePath}`);
// Verify placeholder format (accept both / and \ for cross-platform)
const expectedPattern = /^\{\{PREBUILT_PYTHON_DIR\}\}[\/\\]cpython-[\w\.\-]+[\/\\](bin|Scripts)$/;
const expectedPattern =
/^\{\{PREBUILT_PYTHON_DIR\}\}[/\\]cpython-[\w.-]+[/\\](bin|Scripts)$/;
if (expectedPattern.test(homePath)) {
console.log(` ✅ PASS: Placeholder format is correct`);
} else {
console.log(` ⚠️ WARNING: Placeholder format might be incorrect`);
console.log(` Expected: {{PREBUILT_PYTHON_DIR}}/cpython-X.Y.Z-platform/bin (or \\ on Windows)`);
console.log(
` Expected: {{PREBUILT_PYTHON_DIR}}/cpython-X.Y.Z-platform/bin (or \\ on Windows)`
);
console.log(` Got: ${homePath}`);
}
} else {

View file

@ -12,12 +12,10 @@
// limitations under the License.
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
import animationData from '@/assets/animation/openning_animaiton.json';
import { AnimationJson } from '@/components/AnimationJson';
import AppRoutes from '@/routers/index';
import { stackClientApp } from '@/stack/client';
import { StackProvider, StackTheme } from '@stackframe/react';
import React, { useEffect, useState } from 'react';
import React, { useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { Toaster } from 'sonner';
import { hasStackKeys } from './lib';
@ -28,8 +26,6 @@ const HAS_STACK_KEYS = hasStackKeys();
function App() {
const navigate = useNavigate();
const { setInitState } = useAuthStore();
const [animationFinished, setAnimationFinished] = useState(false);
const { isFirstLaunch } = useAuthStore();
useEffect(() => {
const handleShareCode = (event: any, share_token: string) => {
@ -67,19 +63,6 @@ function App() {
};
}, [navigate, setInitState]);
// render main content
const renderMainContent = () => {
if (isFirstLaunch && !animationFinished) {
return (
<AnimationJson
onComplete={() => setAnimationFinished(true)}
animationData={animationData}
/>
);
}
return <AppRoutes />;
};
// render wrapper
const renderWrapper = (children: React.ReactNode) => {
if (HAS_STACK_KEYS) {
@ -98,7 +81,7 @@ function App() {
);
};
return renderWrapper(renderMainContent());
return renderWrapper(<AppRoutes />);
}
export default App;

BIN
src/assets/background.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

File diff suppressed because it is too large Load diff

View file

@ -14,6 +14,7 @@
import { fetchPut } from '@/api/http';
import useChatStoreAdapter from '@/hooks/useChatStoreAdapter';
import { TaskStatus } from '@/types/constants';
import {
ArrowDown,
ArrowUp,
@ -29,7 +30,6 @@ import {
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { TaskState } from '../TaskState';
import { Button } from '../ui/button';
import { TaskStatus } from "@/types/constants";
export default function Home() {
//Get Chatstore for the active project's task
@ -224,7 +224,8 @@ export default function Home() {
}
done={
activeAgent?.tasks?.filter(
(task) => task.status === TaskStatus.COMPLETED && !task.reAssignTo
(task) =>
task.status === TaskStatus.COMPLETED && !task.reAssignTo
).length || 0
}
progress={
@ -240,7 +241,8 @@ export default function Home() {
}
failed={
activeAgent?.tasks?.filter(
(task) => task.status === TaskStatus.FAILED && !task.reAssignTo
(task) =>
task.status === TaskStatus.FAILED && !task.reAssignTo
).length || 0
}
skipped={

View file

@ -12,11 +12,11 @@
// limitations under the License.
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
import { type ChatTaskStatusType } from '@/types/constants';
import { BoxAction } from './BoxAction';
import { BoxHeaderConfirm, BoxHeaderSplitting } from './BoxHeader';
import { FileAttachment, Inputbox, InputboxProps } from './InputBox';
import { QueuedBox, QueuedMessage } from './QueuedBox';
import { type ChatTaskStatusType } from "@/types/constants";
export type BottomBoxState =
| 'input'

View file

@ -12,133 +12,133 @@
// limitations under the License.
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
import { useState, useEffect } from "react";
import ReactMarkdown from "react-markdown";
import { isHtmlDocument } from "@/lib/htmlFontStyles";
import { isHtmlDocument } from '@/lib/htmlFontStyles';
import { useEffect, useState } from 'react';
import ReactMarkdown from 'react-markdown';
export const SummaryMarkDown = ({
content,
speed = 15,
onTyping,
enableTypewriter = true,
content,
speed = 15,
onTyping,
enableTypewriter = true,
}: {
content: string;
speed?: number;
onTyping?: () => void;
enableTypewriter?: boolean;
content: string;
speed?: number;
onTyping?: () => void;
enableTypewriter?: boolean;
}) => {
const [displayedContent, setDisplayedContent] = useState("");
const [isTyping, setIsTyping] = useState(true);
const [displayedContent, setDisplayedContent] = useState('');
const [_isTyping, setIsTyping] = useState(true);
useEffect(() => {
if (!enableTypewriter) {
setDisplayedContent(content);
setIsTyping(false);
return;
}
useEffect(() => {
if (!enableTypewriter) {
setDisplayedContent(content);
setIsTyping(false);
return;
}
setDisplayedContent("");
setIsTyping(true);
let index = 0;
setDisplayedContent('');
setIsTyping(true);
let index = 0;
const timer = setInterval(() => {
if (index < content.length) {
setDisplayedContent(content.slice(0, index + 1));
index++;
if (onTyping) {
onTyping();
}
} else {
setIsTyping(false);
clearInterval(timer);
}
}, speed);
const timer = setInterval(() => {
if (index < content.length) {
setDisplayedContent(content.slice(0, index + 1));
index++;
if (onTyping) {
onTyping();
}
} else {
setIsTyping(false);
clearInterval(timer);
}
}, speed);
return () => clearInterval(timer);
}, [content, speed, onTyping]);
return () => clearInterval(timer);
}, [content, speed, onTyping, enableTypewriter]);
// If content is a pure HTML document, render in a styled pre block
if (isHtmlDocument(content)) {
// Trim leading whitespace from each line for consistent alignment
const formattedHtml = displayedContent
.split('\n')
.map(line => line.trimStart())
.join('\n')
.trim();
return (
<div className="prose prose-sm max-w-none">
<pre className="bg-emerald-50 border border-emerald-200 p-3 rounded-lg text-xs font-mono overflow-x-auto whitespace-pre-wrap mb-3">
<code>{formattedHtml}</code>
</pre>
</div>
);
}
// If content is a pure HTML document, render in a styled pre block
if (isHtmlDocument(content)) {
// Trim leading whitespace from each line for consistent alignment
const formattedHtml = displayedContent
.split('\n')
.map((line) => line.trimStart())
.join('\n')
.trim();
return (
<div className="prose prose-sm max-w-none">
<pre className="mb-3 overflow-x-auto whitespace-pre-wrap rounded-lg border border-emerald-200 bg-emerald-50 p-3 font-mono text-xs">
<code>{formattedHtml}</code>
</pre>
</div>
);
}
return (
<div className="prose prose-sm max-w-none">
<ReactMarkdown
components={{
h1: ({ children }) => (
<h1 className="text-xl font-bold text-emerald-800 mb-3 flex items-center gap-2 border-b border-emerald-200 pb-2">
{children}
</h1>
),
h2: ({ children }) => (
<h2 className="text-lg font-semibold text-emerald-700 mb-3 mt-4 flex items-center gap-2">
{children}
</h2>
),
h3: ({ children }) => (
<h3 className="text-base font-medium text-emerald-600 mb-2 mt-3">
{children}
</h3>
),
p: ({ children }) => (
<p className="m-0 text-sm font-normal text-gray-700 leading-relaxed mb-3 whitespace-pre-wrap">
{children}
</p>
),
ul: ({ children }) => (
<ul className="list-disc list-inside text-sm text-gray-700 mb-3 space-y-1 ml-2">
{children}
</ul>
),
ol: ({ children }) => (
<ol className="list-decimal list-inside text-sm text-gray-700 mb-3 space-y-1 ml-2">
{children}
</ol>
),
li: ({ children }) => (
<li className="mb-1 text-gray-700 leading-relaxed">{children}</li>
),
code: ({ children }) => (
<code className="bg-surface-success-subtle text-text-success px-2 py-1 rounded text-xs font-mono">
{children}
</code>
),
pre: ({ children }) => (
<pre className="bg-emerald-50 border border-emerald-200 p-3 rounded-lg text-xs font-mono overflow-x-auto whitespace-pre-wrap mb-3">
{children}
</pre>
),
blockquote: ({ children }) => (
<blockquote className="border-l-4 border-emerald-300 pl-4 italic text-emerald-700 bg-emerald-50 py-2 rounded-r-lg mb-3">
{children}
</blockquote>
),
strong: ({ children }) => (
<strong className="font-semibold text-emerald-800">{children}</strong>
),
em: ({ children }) => (
<em className="italic text-emerald-600">{children}</em>
),
hr: () => (
<hr className="border-emerald-200 my-4" />
),
}}
>
{displayedContent}
</ReactMarkdown>
</div>
);
return (
<div className="prose prose-sm max-w-none">
<ReactMarkdown
components={{
h1: ({ children }) => (
<h1 className="mb-3 flex items-center gap-2 border-b border-emerald-200 pb-2 text-xl font-bold text-emerald-800">
{children}
</h1>
),
h2: ({ children }) => (
<h2 className="mb-3 mt-4 flex items-center gap-2 text-lg font-semibold text-emerald-700">
{children}
</h2>
),
h3: ({ children }) => (
<h3 className="mb-2 mt-3 text-base font-medium text-emerald-600">
{children}
</h3>
),
p: ({ children }) => (
<p className="m-0 mb-3 whitespace-pre-wrap text-sm font-normal leading-relaxed text-gray-700">
{children}
</p>
),
ul: ({ children }) => (
<ul className="mb-3 ml-2 list-inside list-disc space-y-1 text-sm text-gray-700">
{children}
</ul>
),
ol: ({ children }) => (
<ol className="mb-3 ml-2 list-inside list-decimal space-y-1 text-sm text-gray-700">
{children}
</ol>
),
li: ({ children }) => (
<li className="mb-1 leading-relaxed text-gray-700">{children}</li>
),
code: ({ children }) => (
<code className="rounded bg-surface-success-subtle px-2 py-1 font-mono text-xs text-text-success">
{children}
</code>
),
pre: ({ children }) => (
<pre className="mb-3 overflow-x-auto whitespace-pre-wrap rounded-lg border border-emerald-200 bg-emerald-50 p-3 font-mono text-xs">
{children}
</pre>
),
blockquote: ({ children }) => (
<blockquote className="mb-3 rounded-r-lg border-l-4 border-emerald-300 bg-emerald-50 py-2 pl-4 italic text-emerald-700">
{children}
</blockquote>
),
strong: ({ children }) => (
<strong className="font-semibold text-emerald-800">
{children}
</strong>
),
em: ({ children }) => (
<em className="italic text-emerald-600">{children}</em>
),
hr: () => <hr className="my-4 border-emerald-200" />,
}}
>
{displayedContent}
</ReactMarkdown>
</div>
);
};

View file

@ -13,11 +13,11 @@
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
import { VanillaChatStore } from '@/store/chatStore';
import { AgentStep } from '@/types/constants';
import { motion } from 'framer-motion';
import React from 'react';
import { FloatingAction } from './FloatingAction';
import { UserQueryGroup } from './UserQueryGroup';
import { AgentStep } from '@/types/constants';
interface ProjectSectionProps {
chatId: string;

View file

@ -81,7 +81,7 @@ export function StreamingTaskList({ streamingText }: StreamingTaskListProps) {
size={16}
className="animate-spin text-icon-information"
/>
<span className="text-text-secondary animate-pulse text-sm">
<span className="animate-pulse text-sm text-text-secondary">
{t('layout.task-splitting')}...
</span>
</div>
@ -101,7 +101,7 @@ export function StreamingTaskList({ streamingText }: StreamingTaskListProps) {
{/* Task type badge */}
<div className="mb-2 flex items-center gap-2 px-sm">
<TaskType type={1} />
<span className="text-text-tertiary text-xs font-medium">
<span className="text-xs font-medium text-text-tertiary">
{t('layout.tasks')} {tasks.length}
</span>
</div>
@ -131,7 +131,7 @@ export function StreamingTaskList({ streamingText }: StreamingTaskListProps) {
{/* Task content */}
<div className="relative flex min-h-4 w-full items-start border-[0px] border-b border-solid border-task-border-default pb-2">
<span className="text-text-primary text-xs leading-[20px]">
<span className="text-xs leading-[20px] text-text-primary">
{task}
{isCurrentlyStreaming && (
<span className="ml-0.5 inline-block h-4 w-1 animate-pulse bg-icon-information" />

View file

@ -56,7 +56,7 @@ export const TypeCardSkeleton = ({
<div className="transition-all duration-300 ease-in-out">
<div className="flex items-center gap-2 duration-300 animate-in fade-in-0 slide-in-from-right-2">
<div className="text-text-tertiary text-xs font-medium leading-17">
<div className="text-xs font-medium leading-17 text-text-tertiary">
{t('layout.tasks')}
</div>
<Button variant="ghost" size="icon">

View file

@ -13,6 +13,7 @@
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
import { VanillaChatStore } from '@/store/chatStore';
import { AgentStep, ChatTaskStatus } from '@/types/constants';
import { motion } from 'framer-motion';
import { FileText } from 'lucide-react';
import React, {
@ -27,7 +28,6 @@ import { UserMessageCard } from './MessageItem/UserMessageCard';
import { StreamingTaskList } from './TaskBox/StreamingTaskList';
import { TaskCard } from './TaskBox/TaskCard';
import { TypeCardSkeleton } from './TaskBox/TypeCardSkeleton';
import { AgentStep, ChatTaskStatus } from '@/types/constants';
interface QueryGroup {
queryId: string;
@ -87,7 +87,9 @@ export const UserQueryGroup: React.FC<UserQueryGroupProps> = ({
if (userMessageIndex > 0) {
// Check the previous message - if it's an agent message with step 'ask', this is a human-reply
const prevMessage = messages[userMessageIndex - 1];
return prevMessage?.role === 'agent' && prevMessage?.step === AgentStep.ASK;
return (
prevMessage?.role === 'agent' && prevMessage?.step === AgentStep.ASK
);
}
return false;
})());

View file

@ -12,50 +12,65 @@
// limitations under the License.
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
import { useCallback } from "react";
import { Button } from "@/components/ui/button";
import { useTranslation } from "react-i18next";
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogClose,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
Dialog,
DialogClose,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog';
import { useCallback } from 'react';
import { useTranslation } from 'react-i18next';
interface Props {
open: boolean;
onOpenChange: (open: boolean) => void;
trigger?: React.ReactNode;
onConfirm: () => void;
loading?: boolean;
open: boolean;
onOpenChange: (open: boolean) => void;
trigger?: React.ReactNode;
onConfirm: () => void;
loading?: boolean;
}
export default function EndNoticeDialog({ open, onOpenChange, trigger, onConfirm, loading = false }: Props) {
const { t } = useTranslation();
const onSubmit = useCallback(() => {
onConfirm();
}, [onConfirm]);
export default function EndNoticeDialog({
open,
onOpenChange,
trigger,
onConfirm,
loading = false,
}: Props) {
const { t } = useTranslation();
const onSubmit = useCallback(() => {
onConfirm();
}, [onConfirm]);
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-border-subtle-strong shadow-sm">
<DialogHeader className="!bg-popup-surface !rounded-t-xl p-md justify-start">
<DialogTitle className="m-0">{t("layout.end-project")}</DialogTitle>
</DialogHeader>
<div className="flex flex-col gap-md bg-popup-bg p-md">
{t("layout.ending-this-project-will-stop")}
</div>
<DialogFooter className="bg-white-100% !rounded-b-xl p-md">
<DialogClose asChild>
<Button variant="ghost" size="md" disabled={loading}>{t("layout.cancel")}</Button>
</DialogClose>
<Button size="md" onClick={onSubmit} variant="cuation" disabled={loading}>{t("layout.yes-end-project")}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
return (
<Dialog open={open} onOpenChange={onOpenChange}>
{trigger && <DialogTrigger asChild>{trigger}</DialogTrigger>}
<DialogContent className="gap-0 !rounded-xl border border-border-subtle-strong !bg-popup-surface p-0 shadow-sm sm:max-w-[600px]">
<DialogHeader className="justify-start !rounded-t-xl !bg-popup-surface p-md">
<DialogTitle className="m-0">{t('layout.end-project')}</DialogTitle>
</DialogHeader>
<div className="flex flex-col gap-md bg-popup-bg p-md">
{t('layout.ending-this-project-will-stop')}
</div>
<DialogFooter className="!rounded-b-xl bg-white-100% p-md">
<DialogClose asChild>
<Button variant="ghost" size="md" disabled={loading}>
{t('layout.cancel')}
</Button>
</DialogClose>
<Button
size="md"
onClick={onSubmit}
variant="cuation"
disabled={loading}
>
{t('layout.yes-end-project')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View file

@ -12,186 +12,190 @@
// limitations under the License.
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
import { Button } from "@/components/ui/button";
import { proxyFetchGet, proxyFetchPut } from '@/api/http';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogClose,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Switch } from "@/components/ui/switch";
Dialog,
DialogClose,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '@/components/ui/dialog';
import { Switch } from '@/components/ui/switch';
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { AlertCircle } from "lucide-react";
import { useState, useEffect } from "react";
import { proxyFetchGet, proxyFetchPut } from "@/api/http";
import { useTranslation } from "react-i18next";
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip';
import { AlertCircle } from 'lucide-react';
import { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
interface PrivacyDialogProps {
open: boolean;
onOpenChange: (open: boolean) => void;
trigger?: React.ReactNode;
open: boolean;
onOpenChange: (open: boolean) => void;
trigger?: React.ReactNode;
}
export function PrivacyDialog({ open, onOpenChange, trigger }: PrivacyDialogProps) {
const { t } = useTranslation();
const API_FIELDS = [
"take_screenshot",
"access_local_software",
"access_your_address",
"password_storage",
];
export function PrivacyDialog({
open,
onOpenChange,
trigger,
}: PrivacyDialogProps) {
const { t } = useTranslation();
const API_FIELDS = useMemo(
() => [
'take_screenshot',
'access_local_software',
'access_your_address',
'password_storage',
],
[]
);
const [settings, setSettings] = useState([
{
title: t("layout.allow-agent-to-take-screenshots"),
description: t("layout.permit-the-agent-to-capture"),
checked: false,
},
{
title: t("layout.allow-agent-to-access-local-software"),
description: t("layout.grant-the-agent-permission"),
checked: false,
},
{
title: t("layout.allow-agent-to-access-your-address"),
description: t("layout.authorize-the-agent-to-view"),
checked: false,
},
{
title: t("layout.password-storage"),
description: t("layout.determine-how-passwords-are-handled"),
checked: false,
},
]);
const [settings, setSettings] = useState([
{
title: t('layout.allow-agent-to-take-screenshots'),
description: t('layout.permit-the-agent-to-capture'),
checked: false,
},
{
title: t('layout.allow-agent-to-access-local-software'),
description: t('layout.grant-the-agent-permission'),
checked: false,
},
{
title: t('layout.allow-agent-to-access-your-address'),
description: t('layout.authorize-the-agent-to-view'),
checked: false,
},
{
title: t('layout.password-storage'),
description: t('layout.determine-how-passwords-are-handled'),
checked: false,
},
]);
useEffect(() => {
proxyFetchGet("/api/user/privacy")
.then((res) => {
setSettings((prev) =>
prev.map((item, index) => ({
...item,
checked: res[API_FIELDS[index]] || false,
}))
);
})
.catch((err) => console.error("Failed to fetch settings:", err));
}, []);
useEffect(() => {
proxyFetchGet('/api/user/privacy')
.then((res) => {
setSettings((prev) =>
prev.map((item, index) => ({
...item,
checked: res[API_FIELDS[index]] || false,
}))
);
})
.catch((err) => console.error('Failed to fetch settings:', err));
}, [API_FIELDS]);
const handleToggle = (index: number) => {
setSettings((prev) => {
const newSettings = [...prev];
newSettings[index] = {
...newSettings[index],
checked: !newSettings[index].checked,
};
return newSettings;
});
const handleToggle = (index: number) => {
setSettings((prev) => {
const newSettings = [...prev];
newSettings[index] = {
...newSettings[index],
checked: !newSettings[index].checked,
};
return newSettings;
});
const requestData = {
[API_FIELDS[0]]: settings[0].checked,
[API_FIELDS[1]]: settings[1].checked,
[API_FIELDS[2]]: settings[2].checked,
[API_FIELDS[3]]: settings[3].checked,
};
const requestData = {
[API_FIELDS[0]]: settings[0].checked,
[API_FIELDS[1]]: settings[1].checked,
[API_FIELDS[2]]: settings[2].checked,
[API_FIELDS[3]]: settings[3].checked,
};
requestData[API_FIELDS[index]] = !settings[index].checked;
requestData[API_FIELDS[index]] = !settings[index].checked;
proxyFetchPut("/api/user/privacy", requestData).catch((err) =>
console.error("Failed to update settings:", err)
);
};
proxyFetchPut('/api/user/privacy', requestData).catch((err) =>
console.error('Failed to update settings:', err)
);
};
const handleTurnOnAll = () => {
const newSettings = settings.map((item) => ({
...item,
checked: true,
}));
setSettings(newSettings);
const handleTurnOnAll = () => {
const newSettings = settings.map((item) => ({
...item,
checked: true,
}));
setSettings(newSettings);
const requestData = {
[API_FIELDS[0]]: true,
[API_FIELDS[1]]: true,
[API_FIELDS[2]]: true,
[API_FIELDS[3]]: true,
};
const requestData = {
[API_FIELDS[0]]: true,
[API_FIELDS[1]]: true,
[API_FIELDS[2]]: true,
[API_FIELDS[3]]: true,
};
proxyFetchPut("/api/user/privacy", requestData)
.then(() => {
onOpenChange(false);
})
.catch((err) => console.error("Failed to update settings:", err));
};
proxyFetchPut('/api/user/privacy', requestData)
.then(() => {
onOpenChange(false);
})
.catch((err) => console.error('Failed to update settings:', err));
};
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-border-subtle-strong shadow-sm">
<DialogHeader className="!bg-popup-surface !rounded-t-xl p-md">
<DialogTitle className="m-0">
<div className="flex items-center gap-2">
<div className="text-base font-bold leading-10 text-text-action">
{t("layout.turn-on-all-privacy-settings")}
</div>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<AlertCircle
size={16}
className="text-icon-primary cursor-pointer"
/>
</TooltipTrigger>
<TooltipContent className="max-w-[340px]">
<p className="text-text-body text-sm">{t("layout.eigent-is-a-desktop-software")}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</DialogTitle>
</DialogHeader>
return (
<Dialog open={open} onOpenChange={onOpenChange}>
{trigger && <DialogTrigger asChild>{trigger}</DialogTrigger>}
<DialogContent className="gap-0 !rounded-xl border border-border-subtle-strong !bg-popup-surface p-0 shadow-sm sm:max-w-[600px]">
<DialogHeader className="!rounded-t-xl !bg-popup-surface p-md">
<DialogTitle className="m-0">
<div className="flex items-center gap-2">
<div className="text-base font-bold leading-10 text-text-action">
{t('layout.turn-on-all-privacy-settings')}
</div>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<AlertCircle
size={16}
className="cursor-pointer text-icon-primary"
/>
</TooltipTrigger>
<TooltipContent className="max-w-[340px]">
<p className="text-sm text-text-body">
{t('layout.eigent-is-a-desktop-software')}
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</DialogTitle>
</DialogHeader>
<div className="flex flex-col gap-md bg-popup-bg p-md">
{settings.map((item, index) => (
<div
key={item.title}
className="flex gap-md items-start mb-4"
>
<div className="flex-1">
<div className="text-sm font-bold text-text-primary mb-1">
{item.title}
</div>
<div className="text-xs text-text-body">
{item.description}
</div>
</div>
<div className="flex-shrink-0">
<Switch
checked={item.checked}
onCheckedChange={() => handleToggle(index)}
/>
</div>
</div>
))}
</div>
<div className="flex flex-col gap-md bg-popup-bg p-md">
{settings.map((item, index) => (
<div key={item.title} className="mb-4 flex items-start gap-md">
<div className="flex-1">
<div className="mb-1 text-sm font-bold text-text-primary">
{item.title}
</div>
<div className="text-xs text-text-body">{item.description}</div>
</div>
<div className="flex-shrink-0">
<Switch
checked={item.checked}
onCheckedChange={() => handleToggle(index)}
/>
</div>
</div>
))}
</div>
<DialogFooter className="bg-white-100% !rounded-b-xl p-md">
<DialogClose asChild>
<Button variant="ghost" size="md">
{t("layout.cancel")}
</Button>
</DialogClose>
<Button size="md" onClick={handleTurnOnAll} variant="primary">
{t("layout.turn-on-all-and-finish")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
<DialogFooter className="!rounded-b-xl bg-white-100% p-md">
<DialogClose asChild>
<Button variant="ghost" size="md">
{t('layout.cancel')}
</Button>
</DialogClose>
<Button size="md" onClick={handleTurnOnAll} variant="primary">
{t('layout.turn-on-all-and-finish')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View file

@ -390,6 +390,9 @@ export default function Folder({ data: _data }: { data?: Agent }) {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [chatStore?.tasks[chatStore?.activeTaskId as string]?.taskAssigning]);
const selectedFilePath =
chatStore?.tasks[chatStore?.activeTaskId as string]?.selectedFile?.path;
useEffect(() => {
if (!chatStore) return;
const chatStoreSelectedFile =
@ -403,12 +406,7 @@ export default function Folder({ data: _data }: { data?: Agent }) {
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
chatStore?.tasks[chatStore?.activeTaskId as string]?.selectedFile?.path,
fileGroups,
isShowSourceCode,
chatStore?.activeTaskId,
]);
}, [selectedFilePath, fileGroups, isShowSourceCode, chatStore?.activeTaskId]);
if (!chatStore) {
return <div>Loading...</div>;

View file

@ -48,8 +48,8 @@ export function GlobalSearch() {
className="bg-bg-surface-secondary no-drag flex h-6 w-60 items-center justify-center space-x-2 rounded-lg"
onClick={() => setOpen(true)}
>
<Search className="text-text-secondary h-4 w-4"></Search>
<span className="text-text-secondary font-inter text-[10px] leading-4">
<Search className="h-4 w-4 text-text-secondary"></Search>
<span className="font-inter text-[10px] leading-4 text-text-secondary">
{t('dashboard.search-for-a-task-or-document')}
</span>
</div>

View file

@ -24,8 +24,8 @@ import { TooltipSimple } from '@/components/ui/tooltip';
import useChatStoreAdapter from '@/hooks/useChatStoreAdapter';
import { replayProject } from '@/lib/replay';
import { useProjectStore } from '@/store/projectStore';
import { ChatTaskStatus } from '@/types/constants';
import { ProjectGroup as ProjectGroupType } from '@/types/history';
import { ChatTaskStatus } from "@/types/constants";
import { motion } from 'framer-motion';
import {
Edit,
@ -101,7 +101,8 @@ export default function ProjectGroup({
// 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 === ChatTaskStatus.PENDING
projectTaskIds.includes(taskId) &&
task.status === ChatTaskStatus.PENDING
);
}, [chatStore?.tasks, project.tasks]);
const _hasIssue = hasHumanInLoop;

View file

@ -21,8 +21,8 @@ import {
} from '@/components/ui/popover';
import { Tag } from '@/components/ui/tag';
import { TooltipSimple } from '@/components/ui/tooltip';
import { HistoryTask } from '@/types/history';
import { ChatTaskStatus } from '@/types/constants';
import { HistoryTask } from '@/types/history';
import {
CheckCircle,
CirclePause,

View file

@ -293,7 +293,7 @@ export default function GroupedHistoryView({
return (
<div className="flex items-center justify-center p-8">
<Loader2 className="h-6 w-6 animate-spin text-icon-secondary" />
<span className="text-text-secondary ml-2">{t('layout.loading')}</span>
<span className="ml-2 text-text-secondary">{t('layout.loading')}</span>
</div>
);
}
@ -302,13 +302,13 @@ export default function GroupedHistoryView({
return (
<div className="flex flex-col items-center justify-center p-8 text-center">
<FolderOpen className="text-icon-tertiary mb-4 h-12 w-12" />
<div className="text-text-secondary text-sm">
<div className="text-sm text-text-secondary">
{searchValue
? t('dashboard.no-projects-match-search')
: t('dashboard.no-projects-found')}
</div>
{searchValue && (
<div className="text-text-tertiary mt-1 text-xs">
<div className="mt-1 text-xs text-text-tertiary">
{t('dashboard.try-different-search')}
</div>
)}

View file

@ -21,6 +21,7 @@ import { share } from '@/lib/share';
import { fetchGroupedHistoryTasks } from '@/service/historyApi';
import { getAuthStore } from '@/store/authStore';
import { useSidebarStore } from '@/store/sidebarStore';
import { ChatTaskStatus } from '@/types/constants';
import { HistoryTask, ProjectGroup } from '@/types/history';
import { AnimatePresence, motion } from 'framer-motion';
import {
@ -45,7 +46,6 @@ import {
import { Tag } from '../ui/tag';
import { TooltipSimple } from '../ui/tooltip';
import SearchInput from './SearchInput';
import { ChatTaskStatus } from "@/types/constants";
export default function HistorySidebar() {
const { t } = useTranslation();

View file

@ -12,6 +12,10 @@
// limitations under the License.
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
import addWorkerVideo from '@/assets/add_worker.mp4';
import dynamicWorkforceVideo from '@/assets/dynamic_workforce.mp4';
import localModelVideo from '@/assets/local_model.mp4';
import { Button } from '@/components/ui/button';
import { CardContent } from '@/components/ui/card';
import {
Carousel,
@ -19,12 +23,9 @@ import {
CarouselItem,
} from '@/components/ui/carousel';
import { useAuthStore } from '@/store/authStore';
import { Pause, Play } from 'lucide-react';
import React, { useEffect, useRef, useState } from 'react';
import addWorkerVideo from '@/assets/add_worker.mp4';
import dynamicWorkforceVideo from '@/assets/dynamic_workforce.mp4';
import localModelVideo from '@/assets/local_model.mp4';
export const CarouselStep: React.FC = () => {
const { setInitState: _setInitState } = useAuthStore();
const [currentSlide, setCurrentSlide] = useState(0);
@ -32,11 +33,19 @@ export const CarouselStep: React.FC = () => {
const [api, setApi] = useState<any>(null);
const [isDismissed, _setIsDismissed] = useState(false);
const videoRefs = useRef<(HTMLVideoElement | null)[]>([]);
const [isPaused, setIsPaused] = useState(false);
const videoEndTimeoutRef = useRef<NodeJS.Timeout | null>(null);
// listen to carousel change
useEffect(() => {
if (!api) return;
const onSelect = () => {
// Clear any pending video end timeout when slide changes manually
if (videoEndTimeoutRef.current) {
clearTimeout(videoEndTimeoutRef.current);
videoEndTimeoutRef.current = null;
}
setCurrentSlide(api.selectedScrollSnap());
};
@ -46,6 +55,15 @@ export const CarouselStep: React.FC = () => {
};
}, [api]);
// Cleanup timeout on unmount
useEffect(() => {
return () => {
if (videoEndTimeoutRef.current) {
clearTimeout(videoEndTimeoutRef.current);
}
};
}, []);
// click indicator to jump to corresponding slide
const scrollTo = (index: number) => {
if (api) {
@ -65,17 +83,34 @@ export const CarouselStep: React.FC = () => {
const handleIndicatorHover = (index: number) => {
scrollTo(index);
};
const handleTogglePause = () => {
const newPausedState = !isPaused;
setIsPaused(newPausedState);
const currentVideo = videoRefs.current[currentSlide];
if (currentVideo) {
if (newPausedState) {
currentVideo.pause();
} else {
currentVideo.play().catch((err) => {
console.warn('video.play() error:', err);
});
}
}
};
const carouselItems = [
{
title: '“Dynamic Workforce break it down, get task done”',
title: 'Dynamic Workforce break it down, get task done',
video: dynamicWorkforceVideo,
},
{
title: '“Add worker with pluggable mcp”',
title: 'Add worker with pluggable MCP',
video: addWorkerVideo,
},
{
title: '“private and secure with local model settings”',
title: 'Private and secure with local model settings',
video: localModelVideo,
},
];
@ -87,9 +122,11 @@ export const CarouselStep: React.FC = () => {
if (video) {
const tryPlay = () => {
video.currentTime = 0;
video.play().catch((err) => {
console.warn('video.play() error:', err);
});
if (!isPaused) {
video.play().catch((err) => {
console.warn('video.play() error:', err);
});
}
};
if (video.readyState >= 1) {
@ -104,7 +141,7 @@ export const CarouselStep: React.FC = () => {
video.addEventListener('loadedmetadata', handler);
}
}
}, [currentSlide, api]);
}, [currentSlide, api, isPaused]);
// If carousel is dismissed, don't show anything
// The actual transition to 'done' will be handled by useInstallationSetup
@ -114,14 +151,14 @@ export const CarouselStep: React.FC = () => {
}
return (
<div className="flex w-[1120px] flex-col gap-lg max-lg:w-[100%]">
<div className="flex flex-col gap-md">
<div className="text-4xl font-bold leading-5xl text-text-heading">
<div className="flex h-full w-full flex-col">
<div className="flex h-full min-h-0 w-full flex-col">
<div className="mb-md text-heading-sm font-bold text-text-heading">
{carouselItems[currentSlide].title}
</div>
<Carousel
className="scrollbar max-h-[490px] min-h-[400px] rounded-3xl bg-white-100% p-0 short:max-h-[300px] short:overflow-y-auto"
className="min-h-0 flex-1 bg-transparent"
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
setApi={setApi}
@ -129,26 +166,45 @@ export const CarouselStep: React.FC = () => {
<CarouselContent className="h-full">
{carouselItems.map((_, index) => (
<CarouselItem key={index} className="h-full">
<div className="h-full p-0">
<CardContent className="h-full w-full items-center justify-center p-0">
<video
ref={(el) => (videoRefs.current[index] = el)}
src={carouselItems[index].video}
muted
playsInline
preload="auto"
onEnded={() => {
if (api) {
const currentIndex = api.selectedScrollSnap();
if (currentIndex < carouselItems.length - 1) {
api.scrollNext();
} else {
api.scrollTo(0);
<div className="h-full w-full p-0">
<CardContent className="flex h-full w-full items-center justify-center p-0">
<div
key={
index === currentSlide
? `slide-active-${currentSlide}`
: `slide-${index}`
}
className={`h-full w-full ${
index === currentSlide ? 'animate-fade-in' : ''
}`}
>
<video
ref={(el) => (videoRefs.current[index] = el)}
src={carouselItems[index].video}
muted
playsInline
preload="auto"
onEnded={() => {
if (api && !isPaused) {
// Clear any existing timeout
if (videoEndTimeoutRef.current) {
clearTimeout(videoEndTimeoutRef.current);
}
// Wait 2 seconds before moving to next video
videoEndTimeoutRef.current = setTimeout(() => {
const currentIndex = api.selectedScrollSnap();
if (currentIndex < carouselItems.length - 1) {
api.scrollNext();
} else {
api.scrollTo(0);
}
videoEndTimeoutRef.current = null;
}, 500);
}
}
}}
className="h-full w-full rounded-3xl object-contain"
/>
}}
className="h-full w-full rounded-3xl object-contain"
/>
</div>
</CardContent>
</div>
</CarouselItem>
@ -156,20 +212,33 @@ export const CarouselStep: React.FC = () => {
</CarouselContent>
</Carousel>
</div>
<div className="flex items-center justify-center gap-sm">
<div className="relative mt-2 flex items-center justify-center gap-sm">
<div className="flex items-center justify-center gap-6">
{carouselItems.map((item, index) => (
<div
key={index}
onMouseEnter={() => handleIndicatorHover(index)}
className={`h-1.5 w-[120px] cursor-pointer rounded-full transition-all duration-300 ${
className={`h-1 w-32 cursor-pointer rounded-full transition-all duration-300 ${
index === currentSlide
? 'bg-fill-fill-secondary'
: 'bg-white-100% hover:bg-fill-fill-secondary'
: 'bg-fill-fill-tertiary hover:bg-fill-fill-secondary'
}`}
></div>
))}
</div>
<Button
onClick={handleTogglePause}
variant="ghost"
size="icon"
className="absolute bottom-0 right-0 rounded-full"
aria-label={isPaused ? 'Resume' : 'Pause'}
>
{isPaused ? (
<Play className="h-4 w-4" />
) : (
<Pause className="h-4 w-4" />
)}
</Button>
</div>
</div>
);

View file

@ -26,32 +26,44 @@ export const InstallDependencies: React.FC = () => {
useInstallationUI();
return (
<div className="fixed inset-0 !z-[100] flex h-full w-full items-center justify-center bg-opacity-80 backdrop-blur-sm">
<div className="flex h-full w-[1200px] flex-col justify-center gap-xl p-[40px]">
<div className="relative">
<div className="fixed inset-0 !z-[100] flex h-full w-full items-center justify-center overflow-hidden px-2 pb-2 pt-10">
<div className="flex h-full w-full flex-row justify-center gap-lg rounded-2xl border-solid border-border-tertiary bg-surface-secondary p-md">
<div className="flex h-full w-1/3 pt-6">
{/* {isInstalling.toString()} */}
<div>
<div className="flex w-full flex-col">
<ProgressInstall
value={
isInstalling || installationState === 'waiting-backend'
? progress
: 100
}
className="w-full"
className="mb-4 w-full"
/>
<div className="flex items-center justify-between gap-2">
<div className="text-xs font-normal leading-tight text-text-label">
{isInstalling
? 'System Installing ...'
: installationState === 'waiting-backend'
? 'Starting backend service...'
: ''}
<span className="pl-2">{latestLog?.data}</span>
<div className="mt-2 flex w-full flex-col items-start justify-between gap-4">
<div className="flex w-full flex-row items-start justify-between">
<div className="text-body-sm font-medium leading-normal text-text-heading">
{isInstalling
? 'System Installing ...'
: installationState === 'waiting-backend'
? 'Starting backend service...'
: ''}
</div>
<div className="text-body-sm font-medium leading-normal text-text-heading">
{Math.round(
(isInstalling || installationState === 'waiting-backend'
? progress
: 100) ?? 0
)}
%
</div>
</div>
<div className="w-full text-body-sm font-normal leading-normal text-text-label">
{latestLog?.data}
</div>
</div>
</div>
</div>
<div>
<div className="flex h-full w-2/3 rounded-2xl bg-surface-tertiary p-md">
{initState === 'permissions' && <Permissions />}
{initState === 'carousel' &&
installationState !== 'waiting-backend' && <CarouselStep />}

View file

@ -12,13 +12,12 @@
// limitations under the License.
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
DialogContentSection,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { t } from 'i18next';
@ -42,18 +41,20 @@ const InstallationErrorDialog = ({
if (backendError) {
return (
<Dialog open={true}>
<DialogContent className="bg-white-100%">
<DialogHeader>
<DialogTitle>{t('layout.backend-startup-failed')}</DialogTitle>
</DialogHeader>
<div className="mb-4 text-xs font-normal leading-tight text-text-label">
<div className="mb-1">
<span className="text-text-label/60">{backendError}</span>
<DialogContent size="sm">
<DialogHeader title={t('layout.backend-startup-failed')} />
<DialogContentSection>
<div className="text-xs font-normal leading-normal text-text-label">
<div className="mb-1">
<span className="text-text-label">{backendError}</span>
</div>
</div>
</div>
<DialogFooter>
<Button onClick={retryBackend}>{t('layout.retry')}</Button>
</DialogFooter>
</DialogContentSection>
<DialogFooter
showConfirmButton
confirmButtonText={t('layout.retry')}
onConfirm={retryBackend}
/>
</DialogContent>
</Dialog>
);
@ -61,18 +62,20 @@ const InstallationErrorDialog = ({
return (
<Dialog open={installationState == 'error'}>
<DialogContent className="bg-white-100%">
<DialogHeader>
<DialogTitle>{t('layout.installation-failed')}</DialogTitle>
</DialogHeader>
<div className="mb-4 text-xs font-normal leading-tight text-text-label">
<div className="mb-1">
<span className="text-text-label/60">{error}</span>
<DialogContent size="sm">
<DialogHeader title={t('layout.installation-failed')} />
<DialogContentSection>
<div className="text-xs font-normal leading-normal text-text-label">
<div className="mb-1">
<span className="text-text-label">{error}</span>
</div>
</div>
</div>
<DialogFooter>
<Button onClick={retryInstallation}>{t('layout.retry')}</Button>
</DialogFooter>
</DialogContentSection>
<DialogFooter
showConfirmButton
confirmButtonText={t('layout.retry')}
onConfirm={retryInstallation}
/>
</DialogContent>
</Dialog>
);

View file

@ -84,13 +84,13 @@ export const Permissions: React.FC = () => {
);
};
return (
<div className="flex flex-col gap-lg">
<div className="flex h-[568px] gap-md">
<div className="flex w-[438px] flex-col gap-md">
<div className="text-4xl font-bold leading-5xl text-text-heading">
<div>Enable Permissions</div>
<div className="flex h-full w-full flex-col gap-lg">
<div className="flex h-full w-full gap-md">
<div className="flex w-full flex-col gap-md">
<div className="text-heading-sm font-bold text-text-heading">
Enable Permissions
</div>
<div className="text-xl font-medium leading-2xl text-text-body">
<div className="text-body-md font-medium text-text-body">
${`Grant permission to activate the Agent's autonomous actions.`}
</div>
{settings.map((item, index) => (
@ -120,8 +120,8 @@ export const Permissions: React.FC = () => {
/>
</div>
</div>
<div className="flex items-center justify-end gap-sm">
<div className="flex items-center justify-center gap-sm">
<div className="flex h-full w-full items-center justify-end gap-sm">
<div className="flex w-full items-center justify-center gap-sm">
<Button
onClick={() => setInitState('carousel')}
variant="ghost"

View file

@ -12,374 +12,376 @@
// limitations under the License.
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
import { useEffect, useRef, useState, useCallback } from "react";
import { Terminal } from "@xterm/xterm";
import { FitAddon } from "@xterm/addon-fit";
import { WebLinksAddon } from "@xterm/addon-web-links";
import "@xterm/xterm/css/xterm.css";
import useChatStoreAdapter from "@/hooks/useChatStoreAdapter";
import useChatStoreAdapter from '@/hooks/useChatStoreAdapter';
import { FitAddon } from '@xterm/addon-fit';
import { WebLinksAddon } from '@xterm/addon-web-links';
import { Terminal } from '@xterm/xterm';
import '@xterm/xterm/css/xterm.css';
import { useCallback, useEffect, useRef, useState } from 'react';
// Terminal Component Properties Interface
interface TerminalComponentProps {
content?: string[]; // terminal content array
instanceId?: string; // terminal instance identifier, for multiple terminals
showWelcome?: boolean; // whether to show welcome information
content?: string[]; // terminal content array
instanceId?: string; // terminal instance identifier, for multiple terminals
showWelcome?: boolean; // whether to show welcome information
}
export default function TerminalComponent({
content,
instanceId = "default",
showWelcome = false,
content,
instanceId = 'default',
showWelcome = false,
}: TerminalComponentProps) {
//Get Chatstore for the active project's task
const { chatStore } = useChatStoreAdapter();
if (!chatStore) {
return <div>Loading...</div>;
}
//Get Chatstore for the active project's task
const { chatStore } = useChatStoreAdapter();
// DOM references
const terminalContainerRef = useRef<HTMLDivElement>(null); // terminal container reference
const terminalRef = useRef<HTMLDivElement>(null); // terminal element reference
// DOM references
const terminalContainerRef = useRef<HTMLDivElement>(null); // terminal container reference
const terminalRef = useRef<HTMLDivElement>(null); // terminal element reference
// xterm.js related references
const xtermRef = useRef<Terminal | null>(null); // xterm instance reference
const fitAddonRef = useRef<FitAddon | null>(null); // fit addon reference
// xterm.js related references
const xtermRef = useRef<Terminal | null>(null); // xterm instance reference
const fitAddonRef = useRef<FitAddon | null>(null); // fit addon reference
// state management
const lastTerminalLength = useRef<number>(0); // record last content length, for incremental update
const [currentLine, setCurrentLine] = useState<string>(''); // current input line
const [cursorPos, setCursorPos] = useState<number>(0); // cursor position
const currentLineRef = useRef<string>(''); // current input line ref, for event handling
const cursorPosRef = useRef<number>(0); // cursor position ref, for event handling
// state management
const lastTerminalLength = useRef<number>(0); // record last content length, for incremental update
const [currentLine, setCurrentLine] = useState<string>(""); // current input line
const [cursorPos, setCursorPos] = useState<number>(0); // cursor position
const currentLineRef = useRef<string>(""); // current input line ref, for event handling
const cursorPosRef = useRef<number>(0); // cursor position ref, for event handling
// terminal configuration
const promptText = 'Eigent:~$ '; // prompt text
const isInitialized = useRef<boolean>(false); // initialization identifier, prevent duplicate initialization
// terminal configuration
const promptText = "Eigent:~$ "; // prompt text
const isInitialized = useRef<boolean>(false); // initialization identifier, prevent duplicate initialization
// synchronize state to ref, for event handling
useEffect(() => {
currentLineRef.current = currentLine;
}, [currentLine]);
// synchronize state to ref, for event handling
useEffect(() => {
currentLineRef.current = currentLine;
}, [currentLine]);
useEffect(() => {
cursorPosRef.current = cursorPos;
}, [cursorPos]);
useEffect(() => {
cursorPosRef.current = cursorPos;
}, [cursorPos]);
// keyboard input handling function
const handleKeyInput = useCallback(
({ key, domEvent }: { key: string; domEvent: KeyboardEvent }) => {
const ev = domEvent;
const printable = !ev.altKey && !ev.ctrlKey && !ev.metaKey; // check if it is printable character
const terminal = xtermRef.current;
if (!terminal) return;
// keyboard input handling function
const handleKeyInput = useCallback(
({ key, domEvent }: { key: string; domEvent: KeyboardEvent }) => {
const ev = domEvent;
const printable = !ev.altKey && !ev.ctrlKey && !ev.metaKey; // check if it is printable character
const terminal = xtermRef.current;
if (!terminal) return;
if (ev.keyCode === 13) {
// Enter key: execute command
terminal.writeln('');
if (currentLineRef.current.trim()) {
terminal.writeln(
`\x1b[90m# Executed: ${currentLineRef.current}\x1b[0m`
);
terminal.writeln(
`\x1b[33m⚠ Interactive mode not fully implemented\x1b[0m`
);
}
setCurrentLine('');
setCursorPos(0);
terminal.write(promptText);
} else if (ev.keyCode === 8) {
// Backspace key: delete character
if (cursorPosRef.current > 0) {
const newLine =
currentLineRef.current.slice(0, cursorPosRef.current - 1) +
currentLineRef.current.slice(cursorPosRef.current);
setCurrentLine(newLine);
setCursorPos(cursorPosRef.current - 1);
terminal.write('\b \b'); // delete character before cursor
}
} else if (ev.keyCode === 37) {
// left arrow: move cursor left
if (cursorPosRef.current > 0) {
setCursorPos(cursorPosRef.current - 1);
terminal.write('\x1b[D'); // ANSI escape sequence: move cursor left
}
} else if (ev.keyCode === 39) {
// right arrow: move cursor right
if (cursorPosRef.current < currentLineRef.current.length) {
setCursorPos(cursorPosRef.current + 1);
terminal.write('\x1b[C'); // ANSI escape sequence: move cursor right
}
} else if (printable) {
// printable character: insert at cursor position
const newLine =
currentLineRef.current.slice(0, cursorPosRef.current) +
key +
currentLineRef.current.slice(cursorPosRef.current);
setCurrentLine(newLine);
setCursorPos(cursorPosRef.current + 1);
terminal.write(key);
}
},
[promptText]
);
if (ev.keyCode === 13) {
// Enter key: execute command
terminal.writeln("");
if (currentLineRef.current.trim()) {
terminal.writeln(
`\x1b[90m# Executed: ${currentLineRef.current}\x1b[0m`
);
terminal.writeln(
`\x1b[33m⚠ Interactive mode not fully implemented\x1b[0m`
);
}
setCurrentLine("");
setCursorPos(0);
terminal.write(promptText);
} else if (ev.keyCode === 8) {
// Backspace key: delete character
if (cursorPosRef.current > 0) {
const newLine =
currentLineRef.current.slice(0, cursorPosRef.current - 1) +
currentLineRef.current.slice(cursorPosRef.current);
setCurrentLine(newLine);
setCursorPos(cursorPosRef.current - 1);
terminal.write("\b \b"); // delete character before cursor
}
} else if (ev.keyCode === 37) {
// left arrow: move cursor left
if (cursorPosRef.current > 0) {
setCursorPos(cursorPosRef.current - 1);
terminal.write("\x1b[D"); // ANSI escape sequence: move cursor left
}
} else if (ev.keyCode === 39) {
// right arrow: move cursor right
if (cursorPosRef.current < currentLineRef.current.length) {
setCursorPos(cursorPosRef.current + 1);
terminal.write("\x1b[C"); // ANSI escape sequence: move cursor right
}
} else if (printable) {
// printable character: insert at cursor position
const newLine =
currentLineRef.current.slice(0, cursorPosRef.current) +
key +
currentLineRef.current.slice(cursorPosRef.current);
setCurrentLine(newLine);
setCursorPos(cursorPosRef.current + 1);
terminal.write(key);
}
},
[promptText]
);
// initialize xterm terminal
useEffect(() => {
if (!terminalRef.current || isInitialized.current) return;
console.log('isInitialized.current', isInitialized.current);
// mark as initialized
isInitialized.current = true;
// initialize xterm terminal
useEffect(() => {
if (!terminalRef.current || isInitialized.current) return;
console.log("isInitialized.current", isInitialized.current);
// mark as initialized
isInitialized.current = true;
// create terminal instance
const terminal = new Terminal({
theme: {
background: 'transparent', // transparent background
foreground: '#ffffff', // white foreground
cursor: '#00ff00', // green cursor
cursorAccent: '#00ff00', // cursor accent
},
fontFamily: '"Courier New", Courier, monospace', // monospace font
fontSize: 12, // font size
lineHeight: 1.2, // line height
letterSpacing: 0, // letter spacing
cursorBlink: true, // cursor blink
allowProposedApi: true, // allow proposed API
scrollback: 1000, // scrollback lines
rightClickSelectsWord: true, // right click selects word
smoothScrollDuration: 0, // smooth scroll duration
fastScrollModifier: 'alt', // fast scroll modifier
convertEol: true, // convert end of line
windowsMode: true, // Windows mode
cols: 100, // columns
rows: 30, // rows
});
// create terminal instance
const terminal = new Terminal({
theme: {
background: "transparent", // transparent background
foreground: "#ffffff", // white foreground
cursor: "#00ff00", // green cursor
cursorAccent: "#00ff00", // cursor accent
},
fontFamily: '"Courier New", Courier, monospace', // monospace font
fontSize: 12, // font size
lineHeight: 1.2, // line height
letterSpacing: 0, // letter spacing
cursorBlink: true, // cursor blink
allowProposedApi: true, // allow proposed API
scrollback: 1000, // scrollback lines
rightClickSelectsWord: true, // right click selects word
smoothScrollDuration: 0, // smooth scroll duration
fastScrollModifier: "alt", // fast scroll modifier
convertEol: true, // convert end of line
windowsMode: true, // Windows mode
cols: 100, // columns
rows: 30, // rows
});
// add plugins
const fitAddon = new FitAddon(); // fit addon
const webLinksAddon = new WebLinksAddon(); // web links addon
// add plugins
const fitAddon = new FitAddon(); // fit addon
const webLinksAddon = new WebLinksAddon(); // web links addon
terminal.loadAddon(fitAddon);
terminal.loadAddon(webLinksAddon);
terminal.loadAddon(fitAddon);
terminal.loadAddon(webLinksAddon);
// open terminal
terminal.open(terminalRef.current);
// open terminal
terminal.open(terminalRef.current);
// wait for layout to stabilize and adapt size, then write content
setTimeout(() => {
fitAddon.fit(); // adapt container size
// wait for layout to stabilize and adapt size, then write content
setTimeout(() => {
fitAddon.fit(); // adapt container size
// only show welcome information when needed
if (showWelcome) {
terminal.writeln('\x1b[32m=== Eigent Terminal ===\x1b[0m');
terminal.writeln(`\x1b[32mInstance: ${instanceId}\x1b[0m`);
terminal.writeln('\x1b[32mReady for commands...\x1b[0m');
terminal.writeln('');
}
// only show welcome information when needed
if (showWelcome) {
terminal.writeln("\x1b[32m=== Eigent Terminal ===\x1b[0m");
terminal.writeln(`\x1b[32mInstance: ${instanceId}\x1b[0m`);
terminal.writeln("\x1b[32mReady for commands...\x1b[0m");
terminal.writeln("");
}
// show prompt
// terminal.write(promptText);
}, 300);
// show prompt
// terminal.write(promptText);
}, 300);
// save reference
xtermRef.current = terminal;
fitAddonRef.current = fitAddon;
// save reference
xtermRef.current = terminal;
fitAddonRef.current = fitAddon;
// add keyboard input handling
terminal.onKey(handleKeyInput);
// add keyboard input handling
terminal.onKey(handleKeyInput);
// clean up function
return () => {
terminal.dispose(); // destroy terminal instance
xtermRef.current = null;
isInitialized.current = false;
};
}, [handleKeyInput, promptText, showWelcome, instanceId]);
// clean up function
return () => {
terminal.dispose(); // destroy terminal instance
xtermRef.current = null;
isInitialized.current = false;
};
}, [handleKeyInput, promptText, showWelcome, instanceId]);
// listen to container size change
useEffect(() => {
if (!terminalContainerRef.current || !fitAddonRef.current) return;
// listen to container size change
useEffect(() => {
if (!terminalContainerRef.current || !fitAddonRef.current) return;
// use ResizeObserver to listen to container size change
const resizeObserver = new ResizeObserver((entries) => {
for (const _entry of entries) {
// delay execution of fit to ensure layout stability
setTimeout(() => {
if (fitAddonRef.current) {
fitAddonRef.current.fit();
}
}, 100);
}
});
// use ResizeObserver to listen to container size change
const resizeObserver = new ResizeObserver((entries) => {
for (const entry of entries) {
// delay execution of fit to ensure layout stability
setTimeout(() => {
if (fitAddonRef.current) {
fitAddonRef.current.fit();
}
}, 100);
}
});
resizeObserver.observe(terminalContainerRef.current);
resizeObserver.observe(terminalContainerRef.current);
// listen to window size change
const handleResize = () => {
setTimeout(() => {
if (fitAddonRef.current) {
fitAddonRef.current.fit();
}
}, 150);
};
window.addEventListener('resize', handleResize);
// listen to window size change
const handleResize = () => {
setTimeout(() => {
if (fitAddonRef.current) {
fitAddonRef.current.fit();
}
}, 150);
};
window.addEventListener("resize", handleResize);
// clean up listeners
return () => {
resizeObserver.disconnect();
window.removeEventListener('resize', handleResize);
};
}, []);
// clean up listeners
return () => {
resizeObserver.disconnect();
window.removeEventListener("resize", handleResize);
};
}, []);
// listen to terminal data change and write to xterm
useEffect(() => {
if (!xtermRef.current || !content) return;
const terminalData = content;
const currentLength = terminalData.length;
// listen to terminal data change and write to xterm
useEffect(() => {
if (!xtermRef.current || !content) return;
const terminalData = content;
const currentLength = terminalData.length;
// check if it is the case of component re-initialization
// if lastTerminalLength is 0 but content has data, it means re-initialization
if (lastTerminalLength.current === 0 && currentLength > 0) {
console.log('component re-initialization, skip history data write');
lastTerminalLength.current = currentLength;
return;
}
// check if it is the case of component re-initialization
// if lastTerminalLength is 0 but content has data, it means re-initialization
if (lastTerminalLength.current === 0 && currentLength > 0) {
console.log("component re-initialization, skip history data write");
lastTerminalLength.current = currentLength;
return;
}
// only process new data (incremental update)
if (currentLength > lastTerminalLength.current) {
const newData = terminalData.slice(lastTerminalLength.current);
// only process new data (incremental update)
if (currentLength > lastTerminalLength.current) {
const newData = terminalData.slice(lastTerminalLength.current);
console.log('newData', newData);
newData.forEach((item) => {
if (!xtermRef.current) return;
console.log("newData", newData);
newData.forEach((item) => {
if (!xtermRef.current) return;
// move to line head and clear whole line
xtermRef.current.write('\r');
xtermRef.current.write('\x1b[2K'); // clear whole line
// move to line head and clear whole line
xtermRef.current.write("\r");
xtermRef.current.write("\x1b[2K"); // clear whole line
// process output content
const formattedOutput = item
.replace(/\r?\n$/, '') // remove trailing newline
.replace(/\t/g, ' ') // convert tab to 4 spaces
.replace(/\r/g, ''); // remove carriage return
// process output content
const formattedOutput = item
.replace(/\r?\n$/, "") // remove trailing newline
.replace(/\t/g, " ") // convert tab to 4 spaces
.replace(/\r/g, ""); // remove carriage return
if (formattedOutput.trim()) {
xtermRef.current.writeln(
`\x1b[36m[Eigent]\x1b[0m ${formattedOutput}`
);
} else {
xtermRef.current.writeln('');
}
if (formattedOutput.trim()) {
xtermRef.current.writeln(`\x1b[36m[Eigent]\x1b[0m ${formattedOutput}`);
} else {
xtermRef.current.writeln("");
}
// re-display prompt
xtermRef.current.write(promptText);
// re-display prompt
xtermRef.current.write(promptText);
// re-display current input
if (currentLineRef.current) {
xtermRef.current.write(currentLineRef.current);
// re-display current input
if (currentLineRef.current) {
xtermRef.current.write(currentLineRef.current);
// if cursor is not at the end, move to the correct position
if (cursorPosRef.current < currentLineRef.current.length) {
const moveBack =
currentLineRef.current.length - cursorPosRef.current;
for (let i = 0; i < moveBack; i++) {
xtermRef.current.write('\x1b[D'); // move cursor left
}
}
}
});
// if cursor is not at the end, move to the correct position
if (cursorPosRef.current < currentLineRef.current.length) {
const moveBack =
currentLineRef.current.length - cursorPosRef.current;
for (let i = 0; i < moveBack; i++) {
xtermRef.current.write("\x1b[D"); // move cursor left
}
}
}
});
lastTerminalLength.current = currentLength;
}
}, [content, promptText]);
lastTerminalLength.current = currentLength;
}
}, [content, promptText]);
// reset terminal when switching task
useEffect(() => {
if (!xtermRef.current) return;
// reset terminal when switching task
useEffect(() => {
if (!xtermRef.current) return;
// clear terminal
xtermRef.current.clear();
// clear terminal
xtermRef.current.clear();
// reset state
lastTerminalLength.current = 0;
setCurrentLine('');
setCursorPos(0);
// reset state
lastTerminalLength.current = 0;
setCurrentLine("");
setCursorPos(0);
// delay re-initialization
setTimeout(() => {
if (!xtermRef.current || !fitAddonRef.current) return;
// delay re-initialization
setTimeout(() => {
if (!xtermRef.current || !fitAddonRef.current) return;
// re-adapt size
fitAddonRef.current.fit();
// re-adapt size
fitAddonRef.current.fit();
// only show switch information on main instance
if (showWelcome) {
xtermRef.current.writeln('\x1b[32m=== Eigent Terminal ===\x1b[0m');
xtermRef.current.writeln(`\x1b[32mInstance: ${instanceId}\x1b[0m`);
xtermRef.current.writeln('\x1b[32mTask switched...\x1b[0m');
xtermRef.current.writeln('');
}
// only show switch information on main instance
if (showWelcome) {
xtermRef.current.writeln("\x1b[32m=== Eigent Terminal ===\x1b[0m");
xtermRef.current.writeln(`\x1b[32mInstance: ${instanceId}\x1b[0m`);
xtermRef.current.writeln("\x1b[32mTask switched...\x1b[0m");
xtermRef.current.writeln("");
}
// if there is history data, re-write
if (chatStore.activeTaskId) {
const terminalData = content || [];
if (terminalData.length > 0) {
xtermRef.current.writeln('\x1b[90m--- Previous Output ---\x1b[0m');
terminalData.forEach((item) => {
const formattedOutput = item
.replace(/\r?\n$/, '')
.replace(/\t/g, ' ')
.replace(/\r/g, '');
// if there is history data, re-write
if (chatStore.activeTaskId) {
const terminalData = content || [];
if (terminalData.length > 0) {
xtermRef.current.writeln("\x1b[90m--- Previous Output ---\x1b[0m");
terminalData.forEach((item) => {
const formattedOutput = item
.replace(/\r?\n$/, "")
.replace(/\t/g, " ")
.replace(/\r/g, "");
if (formattedOutput.trim()) {
xtermRef.current?.writeln(
`\x1b[36m[Eigent]\x1b[0m ${formattedOutput}`
);
}
});
xtermRef.current.writeln(
'\x1b[90m--- End Previous Output ---\x1b[0m'
);
xtermRef.current.writeln('');
}
lastTerminalLength.current = terminalData.length;
}
if (formattedOutput.trim()) {
xtermRef.current?.writeln(
`\x1b[36m[Eigent]\x1b[0m ${formattedOutput}`
);
}
});
xtermRef.current.writeln(
"\x1b[90m--- End Previous Output ---\x1b[0m"
);
xtermRef.current.writeln("");
}
lastTerminalLength.current = terminalData.length;
}
// show prompt
xtermRef.current.write(promptText);
}, 200);
}, [chatStore.activeTaskId, showWelcome, instanceId, content]);
// show prompt
xtermRef.current.write(promptText);
}, 200);
}, [chatStore.activeTaskId, showWelcome, instanceId]);
if (!chatStore) {
return <div>Loading...</div>;
}
// render terminal component
return (
<div
ref={terminalContainerRef}
className="w-full h-full flex flex-col rounded-2xl border border-border-subtle-strong border-solid relative overflow-hidden"
style={{ fontFamily: '"Courier New", Courier, monospace' }}
>
{/* background blur effect */}
<div className="absolute inset-0 blur-bg pointer-events-none bg-black-100% rounded-xl"></div>
// render terminal component
return (
<div
ref={terminalContainerRef}
className="relative flex h-full w-full flex-col overflow-hidden rounded-2xl border border-solid border-border-subtle-strong"
style={{ fontFamily: '"Courier New", Courier, monospace' }}
>
{/* background blur effect */}
<div className="blur-bg pointer-events-none absolute inset-0 rounded-xl bg-black-100%"></div>
{/* terminal container */}
<div
ref={terminalRef}
className="absolute inset-0 z-10"
style={{
margin: "16px",
width: "calc(100% - 32px)",
height: "calc(100% - 32px)",
fontFamily: '"Courier New", Courier, monospace',
}}
/>
{/* terminal container */}
<div
ref={terminalRef}
className="absolute inset-0 z-10"
style={{
margin: '16px',
width: 'calc(100% - 32px)',
height: 'calc(100% - 32px)',
fontFamily: '"Courier New", Courier, monospace',
}}
/>
{/* custom style: override xterm.js character spacing */}
<style
dangerouslySetInnerHTML={{
__html: `
{/* custom style: override xterm.js character spacing */}
<style
dangerouslySetInnerHTML={{
__html: `
.xterm span {
letter-spacing: 0.5px !important;
}
`,
}}
/>
</div>
);
}}
/>
</div>
);
}

View file

@ -129,7 +129,7 @@ export default function TerminalAgentWrokSpace() {
className="rounded-full"
>
<ChevronLeft size={16} className="text-text-inverse-primary" />
<span className="text-text-inverse-primary text-sm font-bold leading-13">
<span className="text-sm font-bold leading-13 text-text-inverse-primary">
{t('chat.give-back-to-agent')}
</span>
</Button>
@ -172,7 +172,7 @@ export default function TerminalAgentWrokSpace() {
{agentMap[activeAgent?.type as keyof typeof agentMap]?.name}
</div>
</div>
<div className="text-text-tertiary text-[10px] font-medium leading-17">
<div className="text-[10px] font-medium leading-17 text-text-tertiary">
{
activeAgent?.tasks?.filter(
(task) => task.status && task.status !== 'running'

View file

@ -28,7 +28,9 @@ import { TooltipSimple } from '@/components/ui/tooltip';
import useChatStoreAdapter from '@/hooks/useChatStoreAdapter';
import { share } from '@/lib/share';
import { useAuthStore } from '@/store/authStore';
import { useInstallationUI } from '@/store/installationStore';
import { useSidebarStore } from '@/store/sidebarStore';
import { ChatTaskStatus } from '@/types/constants';
import {
ChevronDown,
ChevronLeft,
@ -45,7 +47,6 @@ import { useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useLocation, useNavigate } from 'react-router-dom';
import { toast } from 'sonner';
import { ChatTaskStatus } from '@/types/constants';
function HeaderWin() {
const { t } = useTranslation();
@ -60,6 +61,10 @@ function HeaderWin() {
const appearance = useAuthStore((state) => state.appearance);
const [endDialogOpen, setEndDialogOpen] = useState(false);
const [endProjectLoading, setEndProjectLoading] = useState(false);
const { isInstalling, installationState } = useInstallationUI();
const _isInstallationActive =
isInstalling || installationState === 'waiting-backend';
useEffect(() => {
const p = window.electronAPI.getPlatform();
setPlatform(p);
@ -91,22 +96,15 @@ function HeaderWin() {
navigate('/');
};
const summaryTask =
chatStore?.tasks[chatStore?.activeTaskId as string]?.summaryTask;
const activeTaskTitle = useMemo(() => {
if (
chatStore?.activeTaskId &&
chatStore.tasks[chatStore.activeTaskId as string]?.summaryTask
) {
return chatStore.tasks[
chatStore.activeTaskId as string
].summaryTask.split('|')[0];
if (chatStore?.activeTaskId && summaryTask) {
return summaryTask.split('|')[0];
}
return t('layout.new-project');
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
chatStore?.activeTaskId,
chatStore?.tasks[chatStore?.activeTaskId as string]?.summaryTask,
t,
]);
}, [chatStore?.activeTaskId, summaryTask, t]);
if (!chatStore) {
return <div>Loading...</div>;

View file

@ -12,202 +12,202 @@
// limitations under the License.
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
import { useState, useEffect } from "react";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import { isHtmlDocument } from "@/lib/htmlFontStyles";
import { isHtmlDocument } from '@/lib/htmlFontStyles';
import { useEffect, useState } from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
export const MarkDown = ({
content,
speed = 15,
onTyping,
enableTypewriter = true, // Whether to enable typewriter effect
pTextSize = "text-xs",
olPadding = "",
content,
speed = 15,
onTyping,
enableTypewriter = true, // Whether to enable typewriter effect
pTextSize = 'text-xs',
olPadding = '',
}: {
content: string;
speed?: number;
onTyping?: () => void;
enableTypewriter?: boolean;
pTextSize?: string;
olPadding?: string;
content: string;
speed?: number;
onTyping?: () => void;
enableTypewriter?: boolean;
pTextSize?: string;
olPadding?: string;
}) => {
const [displayedContent, setDisplayedContent] = useState("");
const [displayedContent, setDisplayedContent] = useState('');
useEffect(() => {
if (!enableTypewriter) {
setDisplayedContent(content);
return;
}
useEffect(() => {
if (!enableTypewriter) {
setDisplayedContent(content);
return;
}
setDisplayedContent("");
let index = 0;
setDisplayedContent('');
let index = 0;
const timer = setInterval(() => {
if (index < content.length) {
setDisplayedContent(content.slice(0, index + 1));
index++;
if (onTyping) {
onTyping();
}
} else {
clearInterval(timer);
}
}, speed);
const timer = setInterval(() => {
if (index < content.length) {
setDisplayedContent(content.slice(0, index + 1));
index++;
if (onTyping) {
onTyping();
}
} else {
clearInterval(timer);
}
}, speed);
return () => clearInterval(timer);
}, [content, speed]);
return () => clearInterval(timer);
}, [content, speed, enableTypewriter, onTyping]);
// process line breaks, convert \n to <br> tag
const processContent = (text: string) => {
return text.replace(/\\n/g, " \n "); // add two spaces before \n, so ReactMarkdown will recognize it as a line break
};
// process line breaks, convert \n to <br> tag
const processContent = (text: string) => {
return text.replace(/\\n/g, ' \n '); // add two spaces before \n, so ReactMarkdown will recognize it as a line break
};
// If content is a pure HTML document, render in a styled pre block
if (isHtmlDocument(content)) {
// Trim leading whitespace from each line for consistent alignment
const formattedHtml = displayedContent
.split('\n')
.map(line => line.trimStart())
.join('\n')
.trim();
return (
<div className="prose prose-sm w-full select-text pointer-events-auto overflow-x-auto markdown-container">
<pre className="bg-code-surface p-2 rounded text-xs font-mono overflow-x-auto whitespace-pre-wrap">
<code>{formattedHtml}</code>
</pre>
</div>
);
}
// If content is a pure HTML document, render in a styled pre block
if (isHtmlDocument(content)) {
// Trim leading whitespace from each line for consistent alignment
const formattedHtml = displayedContent
.split('\n')
.map((line) => line.trimStart())
.join('\n')
.trim();
return (
<div className="prose prose-sm markdown-container pointer-events-auto w-full select-text overflow-x-auto">
<pre className="overflow-x-auto whitespace-pre-wrap rounded bg-code-surface p-2 font-mono text-xs">
<code>{formattedHtml}</code>
</pre>
</div>
);
}
return (
<div className="prose prose-sm w-full select-text pointer-events-auto overflow-x-auto markdown-container">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
h1: ({ children }) => (
<h1 className="text-xs font-bold text-primary mb-1 break-words">
{children}
</h1>
),
h2: ({ children }) => (
<h2 className="text-xs font-semibold text-primary mb-1 break-words">
{children}
</h2>
),
h3: ({ children }) => (
<h3 className="text-xs font-medium text-primary mb-1 break-words">
{children}
</h3>
),
p: ({ children }) => (
<p
className={`m-0 ${pTextSize} font-medium text-xs text-primary leading-10 font-inter whitespace-pre-line break-words`}
>
{children}
</p>
),
ul: ({ children }) => (
<ul
className={`list-disc list-inside text-xs text-primary mb-1 ${olPadding}`}
>
{children}
</ul>
),
// ol: ({ children }) => (
// <ol
// className={`list-decimal list-inside text-xs text-primary mb-1 ${olPadding}`}
// >
// {children}
// </ol>
// ),
li: ({ children }) => (
<li className="mb-1 list-inside break-all">{children}</li>
),
a: ({ children, href }) => (
<a
href={href}
className=" hover:text-text-link-hover underline break-all"
target="_blank"
rel="noopener noreferrer"
>
{children}
</a>
),
code: ({ children }) => (
<code className="bg-code-surface px-1 py-0.5 rounded text-xs font-mono">
{children}
</code>
),
pre: ({ children }) => (
<pre className="bg-code-surface p-2 rounded text-xs font-mono overflow-x-auto whitespace-pre-wrap">
{children}
</pre>
),
blockquote: ({ children }) => (
<blockquote className="border-l-4 border-border-subtle-strong pl-3 italic text-primary text-xs">
{children}
</blockquote>
),
strong: ({ children }) => (
<strong className="font-semibold text-primary text-xs">
{children}
</strong>
),
em: ({ children }) => (
<em className="italic text-primary text-xs">{children}</em>
),
table: ({ children }) => (
<div className="overflow-x-auto w-full max-w-full">
<table
className="w-full mb-4 !table min-w-0"
style={{
borderCollapse: "collapse",
border: "1px solid #d1d5db",
borderSpacing: 0,
}}
>
{children}
</table>
</div>
),
thead: ({ children }) => (
<thead className="!table-header-group bg-code-surface">
{children}
</thead>
),
tbody: ({ children }) => (
<tbody className="!table-row-group">{children}</tbody>
),
tr: ({ children }) => <tr className="!table-row">{children}</tr>,
th: ({ children }) => (
<th
className="text-left font-semibold text-primary text-[10px] !table-cell"
style={{
border: "1px solid #d1d5db",
padding: "2px 5px",
borderCollapse: "collapse",
}}
>
{children}
</th>
),
td: ({ children }) => (
<td
className="text-primary text-[10px] !table-cell"
style={{
border: "1px solid #d1d5db",
padding: "2px 5px",
borderCollapse: "collapse",
}}
>
{children}
</td>
),
}}
>
{processContent(displayedContent)}
</ReactMarkdown>
</div>
);
return (
<div className="prose prose-sm markdown-container pointer-events-auto w-full select-text overflow-x-auto">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
h1: ({ children }) => (
<h1 className="text-primary mb-1 break-words text-xs font-bold">
{children}
</h1>
),
h2: ({ children }) => (
<h2 className="text-primary mb-1 break-words text-xs font-semibold">
{children}
</h2>
),
h3: ({ children }) => (
<h3 className="text-primary mb-1 break-words text-xs font-medium">
{children}
</h3>
),
p: ({ children }) => (
<p
className={`m-0 ${pTextSize} text-primary whitespace-pre-line break-words font-inter text-xs font-medium leading-10`}
>
{children}
</p>
),
ul: ({ children }) => (
<ul
className={`text-primary mb-1 list-inside list-disc text-xs ${olPadding}`}
>
{children}
</ul>
),
// ol: ({ children }) => (
// <ol
// className={`list-decimal list-inside text-xs text-primary mb-1 ${olPadding}`}
// >
// {children}
// </ol>
// ),
li: ({ children }) => (
<li className="mb-1 list-inside break-all">{children}</li>
),
a: ({ children, href }) => (
<a
href={href}
className="break-all underline hover:text-text-link-hover"
target="_blank"
rel="noopener noreferrer"
>
{children}
</a>
),
code: ({ children }) => (
<code className="rounded bg-code-surface px-1 py-0.5 font-mono text-xs">
{children}
</code>
),
pre: ({ children }) => (
<pre className="overflow-x-auto whitespace-pre-wrap rounded bg-code-surface p-2 font-mono text-xs">
{children}
</pre>
),
blockquote: ({ children }) => (
<blockquote className="text-primary border-l-4 border-border-subtle-strong pl-3 text-xs italic">
{children}
</blockquote>
),
strong: ({ children }) => (
<strong className="text-primary text-xs font-semibold">
{children}
</strong>
),
em: ({ children }) => (
<em className="text-primary text-xs italic">{children}</em>
),
table: ({ children }) => (
<div className="w-full max-w-full overflow-x-auto">
<table
className="mb-4 !table w-full min-w-0"
style={{
borderCollapse: 'collapse',
border: '1px solid #d1d5db',
borderSpacing: 0,
}}
>
{children}
</table>
</div>
),
thead: ({ children }) => (
<thead className="!table-header-group bg-code-surface">
{children}
</thead>
),
tbody: ({ children }) => (
<tbody className="!table-row-group">{children}</tbody>
),
tr: ({ children }) => <tr className="!table-row">{children}</tr>,
th: ({ children }) => (
<th
className="text-primary !table-cell text-left text-[10px] font-semibold"
style={{
border: '1px solid #d1d5db',
padding: '2px 5px',
borderCollapse: 'collapse',
}}
>
{children}
</th>
),
td: ({ children }) => (
<td
className="text-primary !table-cell text-[10px]"
style={{
border: '1px solid #d1d5db',
padding: '2px 5px',
borderCollapse: 'collapse',
}}
>
{children}
</td>
),
}}
>
{processContent(displayedContent)}
</ReactMarkdown>
</div>
);
};

View file

@ -67,7 +67,7 @@ export default function ConfirmModal({
className="alert-dialog-wrapper fixed max-w-md rounded-xl shadow-perfect"
>
<div className="p-6">
<span className="text-text-primary mb-2 text-body-lg font-bold">
<span className="mb-2 text-body-lg font-bold text-text-primary">
{title}
</span>
<p className="mb-6 text-label-md text-text-label">{message}</p>

View file

@ -24,13 +24,18 @@ const ProgressInstall = React.forwardRef<
<ProgressPrimitive.Root
ref={ref}
className={cn(
'bg-specialty-progress-surface relative h-3 w-full overflow-hidden rounded-full',
'relative h-3 w-full overflow-hidden rounded-full bg-surface-tertiary',
className
)}
{...props}
>
{/* Shimmer background layer */}
<div className="progress-install-shimmer" />
<ProgressPrimitive.Indicator
className={`h-full w-full flex-1 transition-all`}
className={cn(
'relative z-10 h-full w-full flex-1 rounded-full transition-all'
)}
style={{
transform: `translateX(-${100 - (value || 0)}%)`,
background:

View file

@ -20,7 +20,6 @@ import {
proxyFetchPost,
proxyFetchPut,
} from '@/api/http';
import { capitalizeFirstLetter } from '@/lib';
import { useAuthStore } from '@/store/authStore';
import { useCallback, useEffect, useRef, useState } from 'react';

View file

@ -7,8 +7,9 @@
"eigent-cloud": "Eigent Cloud",
"default": "默认",
"profile": "个人资料",
"account": "账户",
"you-are-currently-signed-in-with": "你当前使用的是 <email>{{email}}</email> 账户",
"you-are-currently-signed-in-with": "你当前使用的是 {{email}} 账户",
"manage": "管理",
"log-out": "退出",
"language": "语言",
@ -102,7 +103,6 @@
"save": "保存",
"confirm-delete": "确认删除",
"are-you-sure-you-want-to-delete": "你确定要删除",
"deleting": "删除中...",
"delete": "删除",
"configure {name} Toolkit": "配置 {{name}} 工具包",
"get-it-from": "获取它来自",
@ -163,7 +163,6 @@
"reset": "重置",
"reset-success": "重置成功!",
"reset-failed": "重置失败!",
"select-default-model": "选择默认模型",
"browser-login": "浏览器登录",
"browser-login-description": "打开 Chrome 浏览器以登录您的账户。您的登录数据将安全地保存在本地配置文件中。",
@ -192,14 +191,6 @@
"all-cookies-deleted": "所有 Cookie 已成功删除。",
"cookie-delete-warning": "注意:删除 Cookie 会使您从相关网站登出。您可能需要重启浏览器才能看到更改生效。",
"network-proxy": "网络代理",
"network-proxy-description": "配置网络请求的代理服务器。如果您需要通过代理访问外部 API这将非常有用。",
"proxy-placeholder": "http://127.0.0.1:7890",
"proxy-saved-restart-required": "代理配置已保存。请重启应用以应用更改。",
"proxy-save-failed": "保存代理配置失败。",
"proxy-invalid-url": "无效的代理 URL。必须以 http://、https://、socks4:// 或 socks5:// 开头。",
"proxy-restart-hint": "需要重启应用以应用代理更改。",
"cloud-not-available-in-local-proxy": "在本地代理模式下无法使用云端版本",
"set-as-default": "设为默认",
"api-key-setting": "API 密钥设置",

View file

@ -4,11 +4,9 @@
"privacy": "隱私",
"models": "模型",
"mcp": "MCP & 工具",
"eigent-cloud": "Eigent Cloud",
"default": "預設",
"profile": "個人資料",
"account": "帳戶",
"you-are-currently-signed-in-with": "您目前使用的是 <email>{{email}}</email> 帳戶",
"you-are-currently-signed-in-with": "您目前使用的是 {{email}} 帳戶",
"manage": "管理",
"log-out": "登出",
"language": "語言",
@ -164,14 +162,6 @@
"reset-success": "重設成功!",
"reset-failed": "重設失敗!",
"select-default-model": "選擇預設模型",
"network-proxy": "網路代理",
"network-proxy-description": "設定網路請求的代理伺服器。如果您需要透過代理存取外部 API這將非常有用。",
"proxy-placeholder": "http://127.0.0.1:7890",
"proxy-saved-restart-required": "代理設定已儲存。請重新啟動應用程式以套用變更。",
"proxy-save-failed": "儲存代理設定失敗。",
"proxy-invalid-url": "無效的代理 URL。必須以 http://、https://、socks4:// 或 socks5:// 開頭。",
"proxy-restart-hint": "需要重新啟動應用程式以套用代理變更。",
"cloud-not-available-in-local-proxy": "在本機代理模式下無法使用雲端版本",
"set-as-default": "設為預設",
"api-key-setting": "API 金鑰設定",
@ -194,6 +184,13 @@
"gpt-5-mini-name": "GPT-5 Mini",
"claude-sonnet-4-5-name": "Claude Sonnet 4-5",
"network-proxy": "網路代理",
"network-proxy-description": "設定網路請求的代理伺服器。如果您需要透過代理存取外部 API這將非常有用。",
"proxy-placeholder": "http://127.0.0.1:7890",
"proxy-saved-restart-required": "代理設定已儲存。請重新啟動應用程式以套用變更。",
"proxy-save-failed": "儲存代理設定失敗。",
"proxy-invalid-url": "無效的代理 URL。必須以 http://、https://、socks4:// 或 socks5:// 開頭。",
"proxy-restart-hint": "需要重新啟動應用程式以套用代理變更。",
"preferred-ide": "偏好 IDE",
"preferred-ide-description": "選擇開啟智能體專案資料夾時使用的應用程式。",
"system-file-manager": "系統檔案管理器"

View file

@ -12,303 +12,308 @@
// limitations under the License.
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
import { ChatTaskStatus } from "@/types/constants";
import ChatBox from "@/components/ChatBox";
import Workflow from "@/components/WorkFlow";
import Folder from "@/components/Folder";
import Terminal from "@/components/Terminal";
import useChatStoreAdapter from "@/hooks/useChatStoreAdapter";
import { useCallback, useEffect, useState } from "react";
import { ReactFlowProvider } from "@xyflow/react";
import BottomBar from "@/components/BottomBar";
import BrowserAgentWorkSpace from "@/components/BrowserAgentWorkSpace";
import TerminalAgentWrokSpace from "@/components/TerminalAgentWrokSpace";
import UpdateElectron from "@/components/update";
import BottomBar from '@/components/BottomBar';
import BrowserAgentWorkSpace from '@/components/BrowserAgentWorkSpace';
import ChatBox from '@/components/ChatBox';
import Folder from '@/components/Folder';
import TerminalAgentWrokSpace from '@/components/TerminalAgentWrokSpace';
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from "@/components/ui/resizable"
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from '@/components/ui/resizable';
import UpdateElectron from '@/components/update';
import Workflow from '@/components/WorkFlow';
import useChatStoreAdapter from '@/hooks/useChatStoreAdapter';
import { ChatTaskStatus } from '@/types/constants';
import { ReactFlowProvider } from '@xyflow/react';
import { useCallback, useEffect, useState } from 'react';
export default function Home() {
//Get Chatstore for the active project's task
const { chatStore, projectStore } = useChatStoreAdapter();
//Get Chatstore for the active project's task
const { chatStore, projectStore } = useChatStoreAdapter();
const [_activeWebviewId, setActiveWebviewId] = useState<string | null>(null);
const [_activeWebviewId, setActiveWebviewId] = useState<string | null>(null);
// Add webview-show listener in useEffect with cleanup
useEffect(() => {
const handleWebviewShow = (_event: any, id: string) => {
setActiveWebviewId(id);
};
// Add webview-show listener in useEffect with cleanup
useEffect(() => {
const handleWebviewShow = (_event: any, id: string) => {
setActiveWebviewId(id);
};
window.ipcRenderer?.on("webview-show", handleWebviewShow);
window.ipcRenderer?.on('webview-show', handleWebviewShow);
// Cleanup: remove listener on unmount
return () => {
window.ipcRenderer?.off("webview-show", handleWebviewShow);
};
}, []); // Empty dependency array means this only runs once
// Cleanup: remove listener on unmount
return () => {
window.ipcRenderer?.off('webview-show', handleWebviewShow);
};
}, []); // Empty dependency array means this only runs once
// Extract complex dependency to a variable
const taskAssigning =
chatStore?.tasks[chatStore?.activeTaskId as string]?.taskAssigning;
// Extract complex dependency to a variable
const taskAssigning =
chatStore?.tasks[chatStore?.activeTaskId as string]?.taskAssigning;
useEffect(() => {
if (!chatStore) return;
useEffect(() => {
if (!chatStore) return;
let taskAssigningArray = [
...(taskAssigning || []),
];
let webviews: { id: string; agent_id: string; index: number }[] = [];
taskAssigningArray.map((item) => {
if (item.type === "browser_agent") {
item.activeWebviewIds?.map((webview, index) => {
webviews.push({ ...webview, agent_id: item.agent_id, index });
});
}
});
let taskAssigningArray = [...(taskAssigning || [])];
let webviews: { id: string; agent_id: string; index: number }[] = [];
taskAssigningArray.map((item) => {
if (item.type === 'browser_agent') {
item.activeWebviewIds?.map((webview, index) => {
webviews.push({ ...webview, agent_id: item.agent_id, index });
});
}
});
if (taskAssigningArray.length === 0) {
return;
}
if (taskAssigningArray.length === 0) {
return;
}
if (webviews.length === 0) {
const browserAgent = taskAssigningArray.find(agent => agent.type === 'browser_agent');
if (browserAgent && browserAgent.activeWebviewIds && browserAgent.activeWebviewIds.length > 0) {
browserAgent.activeWebviewIds.forEach((webview, index) => {
webviews.push({ ...webview, agent_id: browserAgent.agent_id, index });
});
}
}
if (webviews.length === 0) {
const browserAgent = taskAssigningArray.find(
(agent) => agent.type === 'browser_agent'
);
if (
browserAgent &&
browserAgent.activeWebviewIds &&
browserAgent.activeWebviewIds.length > 0
) {
browserAgent.activeWebviewIds.forEach((webview, index) => {
webviews.push({ ...webview, agent_id: browserAgent.agent_id, index });
});
}
}
if (webviews.length === 0) {
return;
}
if (webviews.length === 0) {
return;
}
// capture webview
const captureWebview = async () => {
const activeTask = chatStore.tasks[chatStore.activeTaskId as string];
if (!activeTask || activeTask.status === ChatTaskStatus.FINISHED) {
return;
}
webviews.map((webview) => {
window.ipcRenderer
.invoke("capture-webview", webview.id)
.then((base64: string) => {
const currentTask = chatStore.tasks[chatStore.activeTaskId as string];
if (!currentTask || currentTask.type) return;
let taskAssigning = [
...currentTask.taskAssigning,
];
const browserAgentIndex = taskAssigning.findIndex(
(agent) => agent.agent_id === webview.agent_id
);
// capture webview
const captureWebview = async () => {
const activeTask = chatStore.tasks[chatStore.activeTaskId as string];
if (!activeTask || activeTask.status === ChatTaskStatus.FINISHED) {
return;
}
webviews.map((webview) => {
window.ipcRenderer
.invoke('capture-webview', webview.id)
.then((base64: string) => {
const currentTask =
chatStore.tasks[chatStore.activeTaskId as string];
if (!currentTask || currentTask.type) return;
let taskAssigning = [...currentTask.taskAssigning];
const browserAgentIndex = taskAssigning.findIndex(
(agent) => agent.agent_id === webview.agent_id
);
if (
browserAgentIndex !== -1 &&
base64 !== "data:image/jpeg;base64,"
) {
taskAssigning[browserAgentIndex].activeWebviewIds![
webview.index
].img = base64;
chatStore.setTaskAssigning(
chatStore.activeTaskId as string,
taskAssigning
);
const { processTaskId, url } =
taskAssigning[browserAgentIndex].activeWebviewIds![
webview.index
];
chatStore.setSnapshotsTemp(chatStore.activeTaskId as string, {
api_task_id: chatStore.activeTaskId,
camel_task_id: processTaskId,
browser_url: url,
image_base64: base64,
});
if (
browserAgentIndex !== -1 &&
base64 !== 'data:image/jpeg;base64,'
) {
taskAssigning[browserAgentIndex].activeWebviewIds![
webview.index
].img = base64;
chatStore.setTaskAssigning(
chatStore.activeTaskId as string,
taskAssigning
);
const { processTaskId, url } =
taskAssigning[browserAgentIndex].activeWebviewIds![
webview.index
];
chatStore.setSnapshotsTemp(chatStore.activeTaskId as string, {
api_task_id: chatStore.activeTaskId,
camel_task_id: processTaskId,
browser_url: url,
image_base64: base64,
});
}
// let list: any = [];
// taskAssigning.forEach((item: any) => {
// item.activeWebviewIds.forEach((item2: any) => {
// if (item2.img && item2.url && item2.processTaskId) {
// list.push({
// api_task_id: chatStore.activeTaskId,
// camel_task_id: item2.processTaskId,
// browser_url: item2.url,
// image_base64: item2.img,
// });
// }
// });
// });
// chatStore.setSnapshots(chatStore.activeTaskId as string, list);
})
.catch((error) => {
console.error('capture webview error:', error);
});
});
};
}
// let list: any = [];
// taskAssigning.forEach((item: any) => {
// item.activeWebviewIds.forEach((item2: any) => {
// if (item2.img && item2.url && item2.processTaskId) {
// list.push({
// api_task_id: chatStore.activeTaskId,
// camel_task_id: item2.processTaskId,
// browser_url: item2.url,
// image_base64: item2.img,
// });
// }
// });
// });
// chatStore.setSnapshots(chatStore.activeTaskId as string, list);
})
.catch((error) => {
console.error("capture webview error:", error);
});
});
};
let intervalTimer: NodeJS.Timeout | null = null;
let intervalTimer: NodeJS.Timeout | null = null;
const initialTimer = setTimeout(() => {
captureWebview();
intervalTimer = setInterval(captureWebview, 2000);
}, 2000);
const initialTimer = setTimeout(() => {
captureWebview();
intervalTimer = setInterval(captureWebview, 2000);
}, 2000);
// cleanup function
return () => {
clearTimeout(initialTimer);
if (intervalTimer) {
clearInterval(intervalTimer);
}
};
}, [chatStore, taskAssigning]);
// cleanup function
return () => {
clearTimeout(initialTimer);
if (intervalTimer) {
clearInterval(intervalTimer);
}
};
}, [chatStore, taskAssigning]);
const getSize = useCallback(() => {
const webviewContainer = document.getElementById('webview-container');
if (webviewContainer) {
const rect = webviewContainer.getBoundingClientRect();
window.electronAPI.setSize({
x: rect.left,
y: rect.top,
width: rect.width,
height: rect.height,
});
console.log('setSize', rect);
}
}, []);
const getSize = useCallback(() => {
const webviewContainer = document.getElementById("webview-container");
if (webviewContainer) {
const rect = webviewContainer.getBoundingClientRect();
window.electronAPI.setSize({
x: rect.left,
y: rect.top,
width: rect.width,
height: rect.height,
});
console.log("setSize", rect);
}
}, []);
useEffect(() => {
if (!chatStore) return;
useEffect(() => {
if (!chatStore) return;
if (!chatStore.activeTaskId) {
projectStore?.createProject('new project');
}
if (!chatStore.activeTaskId) {
projectStore?.createProject("new project");
}
const webviewContainer = document.getElementById('webview-container');
if (webviewContainer) {
const resizeObserver = new ResizeObserver(() => {
getSize();
});
resizeObserver.observe(webviewContainer);
const webviewContainer = document.getElementById("webview-container");
if (webviewContainer) {
const resizeObserver = new ResizeObserver(() => {
getSize();
});
resizeObserver.observe(webviewContainer);
return () => {
resizeObserver.disconnect();
};
}
}, [chatStore, projectStore, getSize]);
return () => {
resizeObserver.disconnect();
};
}
}, [chatStore, projectStore, getSize]);
if (!chatStore) {
return <div>Loading...</div>;
}
if (!chatStore) {
return <div>Loading...</div>;
}
return (
<div className="h-full min-h-0 flex flex-row overflow-hidden pt-10 px-2 pb-2">
<ReactFlowProvider>
<div className="flex-1 min-w-0 min-h-0 flex items-center justify-center bg-surface-secondary border-solid border-border-tertiary rounded-2xl gap-2 relative overflow-hidden">
<ResizablePanelGroup direction="horizontal">
<ResizablePanel defaultSize={30} minSize={20}>
<ChatBox />
</ResizablePanel>
<ResizableHandle withHandle={true} className="custom-resizable-handle" />
<ResizablePanel>
{chatStore.tasks[chatStore.activeTaskId as string]
?.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(
(agent) =>
agent.agent_id ===
chatStore.tasks[chatStore.activeTaskId as string]
.activeWorkSpace
)?.type === "browser_agent" && (
<div className="w-full h-[calc(100vh-104px)] flex-1 flex animate-in fade-in-0 slide-in-from-right-2 duration-300">
<BrowserAgentWorkSpace />
</div>
)}
{chatStore.tasks[chatStore.activeTaskId as string]
?.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">
{/*filter blur */}
<div className="absolute inset-0 pointer-events-none bg-transparent rounded-xl"></div>
<div className="w-full h-full relative z-10">
<Workflow
taskAssigning={
chatStore.tasks[chatStore.activeTaskId as string]
?.taskAssigning || []
}
/>
</div>
</div>
</div>
)}
{chatStore.tasks[
chatStore.activeTaskId as string
]?.taskAssigning?.find(
(agent) =>
agent.agent_id ===
chatStore.tasks[chatStore.activeTaskId as string]
.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" && (
<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-border-subtle-strong border-solid relative">
{/*filter blur */}
<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 />
</div>
</div>
</div>
)}
{chatStore.tasks[
chatStore.activeTaskId as string
]?.taskAssigning?.find(
(agent) =>
agent.agent_id ===
chatStore.tasks[chatStore.activeTaskId as string]
.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-border-subtle-strong border-solid relative">
{/*filter blur */}
<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(
(agent) =>
agent.agent_id ===
chatStore.tasks[chatStore.activeTaskId as string]
.activeWorkSpace
)}
/>
</div>
</div>
</div>
)}
<BottomBar />
</div>
)}
</ResizablePanel>
{/* Fixed sidebar on the right
return (
<div className="flex h-full min-h-0 flex-row overflow-hidden px-2 pb-2 pt-10">
<ReactFlowProvider>
<div className="relative flex min-h-0 min-w-0 flex-1 items-center justify-center gap-2 overflow-hidden rounded-2xl border-solid border-border-tertiary bg-surface-secondary">
<ResizablePanelGroup direction="horizontal">
<ResizablePanel defaultSize={30} minSize={20}>
<ChatBox />
</ResizablePanel>
<ResizableHandle
withHandle={true}
className="custom-resizable-handle"
/>
<ResizablePanel>
{chatStore.tasks[chatStore.activeTaskId as string]
?.activeWorkSpace && (
<div className="flex h-full w-full flex-1 flex-col pr-2 duration-300 animate-in fade-in-0 slide-in-from-right-2">
{chatStore.tasks[
chatStore.activeTaskId as string
]?.taskAssigning?.find(
(agent) =>
agent.agent_id ===
chatStore.tasks[chatStore.activeTaskId as string]
.activeWorkSpace
)?.type === 'browser_agent' && (
<div className="flex h-[calc(100vh-104px)] w-full flex-1 duration-300 animate-in fade-in-0 slide-in-from-right-2">
<BrowserAgentWorkSpace />
</div>
)}
{chatStore.tasks[chatStore.activeTaskId as string]
?.activeWorkSpace === 'workflow' && (
<div className="flex h-full w-full flex-1 items-center justify-center duration-300 animate-in fade-in-0 slide-in-from-right-2">
<div className="relative flex h-full w-full flex-col rounded-2xl border border-solid border-transparent p-2">
{/*filter blur */}
<div className="pointer-events-none absolute inset-0 rounded-xl bg-transparent"></div>
<div className="relative z-10 h-full w-full">
<Workflow
taskAssigning={
chatStore.tasks[chatStore.activeTaskId as string]
?.taskAssigning || []
}
/>
</div>
</div>
</div>
)}
{chatStore.tasks[
chatStore.activeTaskId as string
]?.taskAssigning?.find(
(agent) =>
agent.agent_id ===
chatStore.tasks[chatStore.activeTaskId as string]
.activeWorkSpace
)?.type === 'developer_agent' && (
<div className="flex h-[calc(100vh-104px)] w-full flex-1 duration-300 animate-in fade-in-0 slide-in-from-right-2">
<TerminalAgentWrokSpace></TerminalAgentWrokSpace>
{/* <Terminal content={[]} /> */}
</div>
)}
{chatStore.tasks[chatStore.activeTaskId as string]
.activeWorkSpace === 'documentWorkSpace' && (
<div className="flex h-[calc(100vh-104px)] w-full flex-1 items-center justify-center duration-300 animate-in fade-in-0 slide-in-from-right-2">
<div className="relative flex h-[calc(100vh-104px)] w-full flex-col rounded-2xl border border-solid border-border-subtle-strong">
{/*filter blur */}
<div className="blur-bg bg-white-50 pointer-events-none absolute inset-0 rounded-xl"></div>
<div className="relative z-10 h-full w-full">
<Folder />
</div>
</div>
</div>
)}
{chatStore.tasks[
chatStore.activeTaskId as string
]?.taskAssigning?.find(
(agent) =>
agent.agent_id ===
chatStore.tasks[chatStore.activeTaskId as string]
.activeWorkSpace
)?.type === 'document_agent' && (
<div className="flex h-[calc(100vh-104px)] w-full flex-1 items-center justify-center duration-300 animate-in fade-in-0 slide-in-from-right-2">
<div className="relative flex h-[calc(100vh-104px)] w-full flex-col rounded-2xl border border-solid border-border-subtle-strong">
{/*filter blur */}
<div className="blur-bg bg-white-50 pointer-events-none absolute inset-0 rounded-xl"></div>
<div className="relative z-10 h-full w-full">
<Folder
data={chatStore.tasks[
chatStore.activeTaskId as string
]?.taskAssigning?.find(
(agent) =>
agent.agent_id ===
chatStore.tasks[
chatStore.activeTaskId as string
].activeWorkSpace
)}
/>
</div>
</div>
</div>
)}
<BottomBar />
</div>
)}
</ResizablePanel>
{/* Fixed sidebar on the right
<div className="h-full z-30">
<SideBar />
</div>*/}
</ResizablePanelGroup>
</div>
</ReactFlowProvider>
<UpdateElectron />
</div>
);
</ResizablePanelGroup>
</div>
</ReactFlowProvider>
<UpdateElectron />
</div>
);
}

View file

@ -12,7 +12,6 @@
// limitations under the License.
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
import loginGif from '@/assets/login.gif';
import { Button } from '@/components/ui/button';
import { useAuthStore } from '@/store/authStore';
import { useStackApp } from '@stackframe/react';
@ -30,6 +29,9 @@ import WindowControls from '@/components/WindowControls';
import { hasStackKeys } from '@/lib';
import { useTranslation } from 'react-i18next';
import background from '@/assets/background.png';
import eigentLogo from '@/assets/logo/eigent_icon.png';
const HAS_STACK_KEYS = hasStackKeys();
let lock = false;
export default function Login() {
@ -336,20 +338,6 @@ export default function Login() {
ref={titlebarRef}
style={{ WebkitAppRegion: 'drag' } as React.CSSProperties}
>
{/* Left spacer for macOS */}
<div
className={`${
platform === 'darwin' ? 'w-[70px]' : 'w-0'
} flex items-center justify-center`}
style={{ WebkitAppRegion: 'no-drag' } as React.CSSProperties}
>
{platform === 'darwin' && (
<span className="text-label-md font-bold text-text-heading">
Eigent
</span>
)}
</div>
{/* Center drag region */}
<div
className="flex h-full flex-1 items-center"
@ -374,12 +362,22 @@ export default function Login() {
</div>
{/* Main content - image extends to top, form has padding */}
<div className={`flex h-full items-center justify-center gap-2 p-2`}>
<div className="flex h-full items-center justify-center rounded-3xl bg-white-100%">
<img src={loginGif} className="h-full rounded-3xl object-cover" />
</div>
<div className="flex h-full flex-1 flex-col items-center justify-center pt-11">
<div className="flex w-80 flex-1 flex-col items-center justify-center">
<div
className={`flex h-full items-center justify-center gap-2 px-2 pb-2 pt-10`}
>
<div
className="flex h-full min-h-0 w-full flex-col items-center justify-center overflow-hidden rounded-2xl border-solid border-border-tertiary bg-surface-secondary px-2 pb-2"
style={{
backgroundImage: `url(${background})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
}}
>
<div className="relative flex w-80 flex-1 flex-col items-center justify-center pt-8">
<img
src={eigentLogo}
className="absolute left-1/2 top-10 h-16 w-16 -translate-x-1/2"
/>
<div className="mb-4 flex items-end justify-between self-stretch">
<div className="text-heading-lg font-bold text-text-heading">
{t('layout.login')}

View file

@ -34,7 +34,7 @@ export default function Setting() {
const version = useAppVersion();
const { appearance } = useAuthStore();
const { t } = useTranslation();
const logoSrc = appearance === 'dark' ? logoWhite : logoBlack;
const _logoSrc = appearance === 'dark' ? logoWhite : logoBlack;
// Setting menu configuration
const settingMenus = [
{

View file

@ -12,113 +12,131 @@
// limitations under the License.
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Circle } from "lucide-react";
import { useEffect, useState } from "react";
import { proxyFetchGet, proxyFetchPost } from "@/api/http";
import { useTranslation } from "react-i18next";
import { proxyFetchGet, proxyFetchPost } from '@/api/http';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Circle } from 'lucide-react';
import { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
interface ConfigItem {
name: string;
env_vars: string[];
name: string;
env_vars: string[];
}
export default function SettingAPI() {
const { t } = useTranslation();
const [items, setItems] = useState<ConfigItem[]>([]);
const [envValues, setEnvValues] = useState<Record<string, string>>({});
const [loading, setLoading] = useState<Record<string, boolean>>({});
const [errors, setErrors] = useState<Record<string, string>>({});
const { t } = useTranslation();
const [items, setItems] = useState<ConfigItem[]>([]);
const [envValues, setEnvValues] = useState<Record<string, string>>({});
const [loading, setLoading] = useState<Record<string, boolean>>({});
const [errors, setErrors] = useState<Record<string, string>>({});
useEffect(() => {
proxyFetchGet("/api/config/info").then((res) => {
const configs = Object.entries(res || {})
.map(([name, v]: [string, any]) => ({ name, env_vars: v.env_vars }))
.filter((item) => Array.isArray(item.env_vars) && item.env_vars.length > 0);
setItems(configs);
});
proxyFetchGet("/api/configs").then((res) => {
if (Array.isArray(res)) {
const envMap: Record<string, string> = {};
res.forEach((item: any) => {
if (item.config_name && item.config_value) {
envMap[item.config_name] = item.config_value;
}
});
setEnvValues(envMap);
}
});
}, []);
useEffect(() => {
proxyFetchGet('/api/config/info').then((res) => {
const configs = Object.entries(res || {})
.map(([name, v]: [string, any]) => ({ name, env_vars: v.env_vars }))
.filter(
(item) => Array.isArray(item.env_vars) && item.env_vars.length > 0
);
setItems(configs);
});
proxyFetchGet('/api/configs').then((res) => {
if (Array.isArray(res)) {
const envMap: Record<string, string> = {};
res.forEach((item: any) => {
if (item.config_name && item.config_value) {
envMap[item.config_name] = item.config_value;
}
});
setEnvValues(envMap);
}
});
}, []);
const handleInputChange = (env: string, value: string) => {
setEnvValues((prev) => ({ ...prev, [env]: value }));
};
const handleInputChange = (env: string, value: string) => {
setEnvValues((prev) => ({ ...prev, [env]: value }));
};
const handleVerify = async (configGroup: string, env: string) => {
const value = envValues[env] || "";
if (!value.trim()) {
setErrors((prev) => ({ ...prev, [env]: t("layout.env-should-not-empty") }));
return;
} else {
setErrors((prev) => ({ ...prev, [env]: "" }));
}
setLoading((prev) => ({ ...prev, [env]: true }));
try {
await proxyFetchPost("/api/configs", {
config_name: env,
config_value: value,
config_group: configGroup,
});
} catch (e) {
} finally {
setLoading((prev) => ({ ...prev, [env]: false }));
}
};
const handleVerify = async (configGroup: string, env: string) => {
const value = envValues[env] || '';
if (!value.trim()) {
setErrors((prev) => ({
...prev,
[env]: t('layout.env-should-not-empty'),
}));
return;
} else {
setErrors((prev) => ({ ...prev, [env]: '' }));
}
setLoading((prev) => ({ ...prev, [env]: true }));
try {
await proxyFetchPost('/api/configs', {
config_name: env,
config_value: value,
config_group: configGroup,
});
} catch (e) {
console.error('Failed to verify config:', e);
} finally {
setLoading((prev) => ({ ...prev, [env]: false }));
}
};
return (
<div className="space-y-8">
{items.map((item) => (
<div key={item.name} className="px-6 py-4 bg-bg-surface-tertiary rounded-2xl">
<div>
<div className="text-base font-bold leading-12 text-text-primary">{item.name}</div>
</div>
<div className="mt-md">
<div>
{item.env_vars.map((env) => (
<div key={env} className="mt-md">
<div className="flex items-center gap-2">
<Input
id={env}
placeholder={env}
className="w-full"
value={envValues[env] || ""}
onChange={(e) => {
handleInputChange(env, e.target.value);
if (errors[env]) setErrors((prev) => ({ ...prev, [env]: "" }));
}}
/>
<Button
className="shadow-none px-sm py-xs bg-bg-fill-disabled"
onClick={() => handleVerify(item.name, env)}
disabled={loading[env]}
>
<span className="text-sm leading-13 text-text-inverse-primary">
{loading[env] ? t("layout.loading") : t("layout.verify")}
</span>
<Circle className="w-4 h-4 text-icon-inverse-primary" />
</Button>
</div>
<div className="text-xs leading-17 text-text-secondary mt-1.5">{env}</div>
{errors[env] && (
<span className="text-xs text-text-error mt-1">{errors[env]}</span>
)}
</div>
))}
</div>
</div>
</div>
))}
</div>
);
return (
<div className="space-y-8">
{items.map((item) => (
<div
key={item.name}
className="bg-bg-surface-tertiary rounded-2xl px-6 py-4"
>
<div>
<div className="text-base font-bold leading-12 text-text-primary">
{item.name}
</div>
</div>
<div className="mt-md">
<div>
{item.env_vars.map((env) => (
<div key={env} className="mt-md">
<div className="flex items-center gap-2">
<Input
id={env}
placeholder={env}
className="w-full"
value={envValues[env] || ''}
onChange={(e) => {
handleInputChange(env, e.target.value);
if (errors[env])
setErrors((prev) => ({ ...prev, [env]: '' }));
}}
/>
<Button
className="bg-bg-fill-disabled px-sm py-xs shadow-none"
onClick={() => handleVerify(item.name, env)}
disabled={loading[env]}
>
<span className="text-sm leading-13 text-text-inverse-primary">
{loading[env]
? t('layout.loading')
: t('layout.verify')}
</span>
<Circle className="text-icon-inverse-primary h-4 w-4" />
</Button>
</div>
<div className="mt-1.5 text-xs leading-17 text-text-secondary">
{env}
</div>
{errors[env] && (
<span className="mt-1 text-xs text-text-error">
{errors[env]}
</span>
)}
</div>
))}
</div>
</div>
</div>
))}
</div>
);
}

File diff suppressed because it is too large Load diff

View file

@ -12,415 +12,415 @@
// limitations under the License.
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
import { useEffect, useState, useRef, useCallback } from "react";
import { Button } from "@/components/ui/button";
import {
Select,
SelectTrigger,
SelectContent,
SelectItem,
SelectValue,
} from "@/components/ui/select";
import {
Tooltip,
TooltipContent,
TooltipSimple,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { MCPEnvDialog } from "./components/MCPEnvDialog";
import { Plus, Store, CircleAlert, ArrowLeft, ChevronLeft } from "lucide-react";
import { proxyFetchDelete, proxyFetchGet, proxyFetchPost } from "@/api/http";
import { Input } from "@/components/ui/input";
import githubIcon from "@/assets/github.svg";
import { useAuthStore } from "@/store/authStore";
import SearchInput from "@/components/SearchInput";
import { useTranslation } from "react-i18next";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
import { proxyFetchDelete, proxyFetchGet, proxyFetchPost } from '@/api/http';
import githubIcon from '@/assets/github.svg';
import SearchInput from '@/components/SearchInput';
import { Button } from '@/components/ui/button';
import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
import { TooltipSimple } from '@/components/ui/tooltip';
import { useAuthStore } from '@/store/authStore';
import { ChevronLeft, CircleAlert, Store } from 'lucide-react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { MCPEnvDialog } from './components/MCPEnvDialog';
interface MCPItem {
id: number;
name: string;
key: string;
description: string;
status: number | string;
category?: { name: string };
home_page?: string;
install_command?: {
command: string;
args: string[];
env?: Record<string, string>;
};
homepage?: string;
id: number;
name: string;
key: string;
description: string;
status: number | string;
category?: { name: string };
home_page?: string;
install_command?: {
command: string;
args: string[];
env?: Record<string, string>;
};
homepage?: string;
}
interface EnvValue {
value: string;
required: boolean;
tip: string;
interface _EnvValue {
value: string;
required: boolean;
tip: string;
}
const PAGE_SIZE = 10;
const STICKY_Z = 20;
const _PAGE_SIZE = 10;
const _STICKY_Z = 20;
function useDebounce<T>(value: T, delay: number): T {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const handler = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(handler);
}, [value, delay]);
return debounced;
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const handler = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(handler);
}, [value, delay]);
return debounced;
}
// map category name to svg file name
const categoryIconMap: Record<string, string> = {
anthropic: "Anthropic",
community: "Community",
official: "Official",
camel: "Camel",
anthropic: 'Anthropic',
community: 'Community',
official: 'Official',
camel: 'Camel',
};
// load all svg files dynamically
const svgIcons = import.meta.glob("@/assets/mcp/*.svg", {
eager: true,
query: "?url",
import: "default",
const svgIcons = import.meta.glob('@/assets/mcp/*.svg', {
eager: true,
query: '?url',
import: 'default',
});
type MCPMarketProps = {
onBack?: () => void;
keyword?: string;
onBack?: () => void;
keyword?: string;
};
export default function MCPMarket({ onBack, keyword: externalKeyword }: MCPMarketProps) {
const { t } = useTranslation();
const { checkAgentTool } = useAuthStore();
const [items, setItems] = useState<MCPItem[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState("");
const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(true);
const [keyword, setKeyword] = useState("");
const effectiveKeyword = externalKeyword !== undefined ? externalKeyword : keyword;
const debouncedKeyword = useDebounce(effectiveKeyword, 400);
const loader = useRef<HTMLDivElement | null>(null);
const [installing, setInstalling] = useState<{ [id: number]: boolean }>({});
const [installed, setInstalled] = useState<{ [id: number]: boolean }>({});
const [installedIds, setInstalledIds] = useState<number[]>([]);
const [mcpCategory, setMcpCategory] = useState<
{ id: number; name: string }[]
>([]);
export default function MCPMarket({
onBack,
keyword: externalKeyword,
}: MCPMarketProps) {
const { t } = useTranslation();
const { checkAgentTool } = useAuthStore();
const [items, setItems] = useState<MCPItem[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState('');
const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(true);
const [keyword, setKeyword] = useState('');
const effectiveKeyword =
externalKeyword !== undefined ? externalKeyword : keyword;
const debouncedKeyword = useDebounce(effectiveKeyword, 400);
const loader = useRef<HTMLDivElement | null>(null);
const [installing, setInstalling] = useState<{ [id: number]: boolean }>({});
const [installed, setInstalled] = useState<{ [id: number]: boolean }>({});
const [installedIds, setInstalledIds] = useState<number[]>([]);
const [mcpCategory, setMcpCategory] = useState<
{ id: number; name: string }[]
>([]);
// environment variable configuration
const [showEnvConfig, setShowEnvConfig] = useState(false);
const [activeMcp, setActiveMcp] = useState<MCPItem | null>(null);
// environment variable configuration
const [showEnvConfig, setShowEnvConfig] = useState(false);
const [activeMcp, setActiveMcp] = useState<MCPItem | null>(null);
const [categoryId, setCategoryId] = useState<number | undefined>(undefined);
const effectiveCategoryId = categoryId;
const [userInstallMcp, setUserInstallMcp] = useState<any | undefined>([]);
// get installed MCP list
useEffect(() => {
proxyFetchGet("/api/mcp/users").then((res) => {
let ids: number[] = [];
if (Array.isArray(res)) {
setUserInstallMcp(res);
ids = res.map((item: any) => item.mcp_id);
} else if (Array.isArray(res.items)) {
setUserInstallMcp(res.items);
ids = res.items.map((item: any) => item.mcp_id);
}
setInstalledIds(ids);
});
}, []);
const [categoryId, setCategoryId] = useState<number | undefined>(undefined);
const effectiveCategoryId = categoryId;
const [userInstallMcp, setUserInstallMcp] = useState<any | undefined>([]);
// get installed MCP list
useEffect(() => {
proxyFetchGet('/api/mcp/users').then((res) => {
let ids: number[] = [];
if (Array.isArray(res)) {
setUserInstallMcp(res);
ids = res.map((item: any) => item.mcp_id);
} else if (Array.isArray(res.items)) {
setUserInstallMcp(res.items);
ids = res.items.map((item: any) => item.mcp_id);
}
setInstalledIds(ids);
});
}, []);
// get MCP categories
useEffect(() => {
proxyFetchGet("/api/mcp/categories").then((res) => {
if (Array.isArray(res)) {
setMcpCategory(res);
}
});
}, []);
// get MCP categories
useEffect(() => {
proxyFetchGet('/api/mcp/categories').then((res) => {
if (Array.isArray(res)) {
setMcpCategory(res);
}
});
}, []);
// load data
const loadData = useCallback(
async (pageNum: number, kw: string, catId?: number, pageSize = 20) => {
setIsLoading(true);
setError("");
try {
const params: any = { page: pageNum, size: pageSize, keyword: kw };
if (catId) params.category_id = catId;
const res = await proxyFetchGet("/api/mcps", params);
if (res && Array.isArray(res.items)) {
// frontend deduplication
const all: MCPItem[] =
pageNum === 1 ? res.items : [...items, ...res.items];
const unique: MCPItem[] = Array.from(
new Map(all.map((i: MCPItem) => [i.id, i])).values()
);
setItems(unique);
setHasMore(res.items.length === pageSize);
} else {
if (pageNum === 1) setItems([]);
setHasMore(false);
}
} catch (err: any) {
setError(err?.message || "Load failed");
} finally {
setIsLoading(false);
}
},
[items]
);
// load data
const loadData = useCallback(
async (pageNum: number, kw: string, catId?: number, pageSize = 20) => {
setIsLoading(true);
setError('');
try {
const params: any = { page: pageNum, size: pageSize, keyword: kw };
if (catId) params.category_id = catId;
const res = await proxyFetchGet('/api/mcps', params);
if (res && Array.isArray(res.items)) {
// frontend deduplication
const all: MCPItem[] =
pageNum === 1 ? res.items : [...items, ...res.items];
const unique: MCPItem[] = Array.from(
new Map(all.map((i: MCPItem) => [i.id, i])).values()
);
setItems(unique);
setHasMore(res.items.length === pageSize);
} else {
if (pageNum === 1) setItems([]);
setHasMore(false);
}
} catch (err: any) {
setError(err?.message || 'Load failed');
} finally {
setIsLoading(false);
}
},
[items]
);
useEffect(() => {
setPage(1);
loadData(1, debouncedKeyword, effectiveCategoryId);
// eslint-disable-next-line
}, [debouncedKeyword, effectiveCategoryId]);
useEffect(() => {
setPage(1);
loadData(1, debouncedKeyword, effectiveCategoryId);
// eslint-disable-next-line
}, [debouncedKeyword, effectiveCategoryId]);
useEffect(() => {
if (page > 1) loadData(page, debouncedKeyword, effectiveCategoryId);
// eslint-disable-next-line
}, [page]);
useEffect(() => {
if (page > 1) loadData(page, debouncedKeyword, effectiveCategoryId);
// eslint-disable-next-line
}, [page]);
useEffect(() => {
if (!hasMore || isLoading) return;
const node = loader.current;
if (!node) return;
const observer = new window.IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) {
setPage((p) => (isLoading || !hasMore ? p : p + 1));
}
},
{ root: null, rootMargin: "0px", threshold: 0.1 }
);
observer.observe(node);
return () => {
observer.disconnect();
};
}, [hasMore, isLoading]);
useEffect(() => {
if (!hasMore || isLoading) return;
const node = loader.current;
if (!node) return;
const observer = new window.IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) {
setPage((p) => (isLoading || !hasMore ? p : p + 1));
}
},
{ root: null, rootMargin: '0px', threshold: 0.1 }
);
observer.observe(node);
return () => {
observer.disconnect();
};
}, [hasMore, isLoading]);
const checkEnv = (id: number) => {
const mcp = items.find((mcp) => mcp.id === id);
if (mcp && Object.keys(mcp?.install_command?.env || {}).length > 0) {
setActiveMcp(mcp);
setShowEnvConfig(true);
} else {
installMcp(id);
}
};
const onConnect = (mcp: MCPItem) => {
console.log(mcp);
setItems((prev) =>
prev.map((item) => (item.id === mcp.id ? { ...item, ...mcp } : item))
);
installMcp(mcp.id);
onClose();
};
const onClose = () => {
setShowEnvConfig(false);
setActiveMcp(null);
};
const installMcp = async (id: number) => {
setInstalling((prev) => ({ ...prev, [id]: true }));
try {
const mcpItem = items.find((item) => item.id === id);
const res = await proxyFetchPost("/api/mcp/install?mcp_id=" + id);
if (res) {
console.log(res);
setUserInstallMcp((prev: any) => [...prev, res]);
}
setInstalled((prev) => ({ ...prev, [id]: true }));
setInstalledIds((prev) => [...prev, id]);
// notify main process
if (window.ipcRenderer && mcpItem) {
await window.ipcRenderer.invoke(
"mcp-install",
mcpItem.key,
mcpItem.install_command
);
}
} catch (e) {
} finally {
setInstalling((prev) => ({ ...prev, [id]: false }));
}
};
const checkEnv = (id: number) => {
const mcp = items.find((mcp) => mcp.id === id);
if (mcp && Object.keys(mcp?.install_command?.env || {}).length > 0) {
setActiveMcp(mcp);
setShowEnvConfig(true);
} else {
installMcp(id);
}
};
const onConnect = (mcp: MCPItem) => {
console.log(mcp);
setItems((prev) =>
prev.map((item) => (item.id === mcp.id ? { ...item, ...mcp } : item))
);
installMcp(mcp.id);
onClose();
};
const onClose = () => {
setShowEnvConfig(false);
setActiveMcp(null);
};
const installMcp = async (id: number) => {
setInstalling((prev) => ({ ...prev, [id]: true }));
try {
const mcpItem = items.find((item) => item.id === id);
const res = await proxyFetchPost('/api/mcp/install?mcp_id=' + id);
if (res) {
console.log(res);
setUserInstallMcp((prev: any) => [...prev, res]);
}
setInstalled((prev) => ({ ...prev, [id]: true }));
setInstalledIds((prev) => [...prev, id]);
// notify main process
if (window.ipcRenderer && mcpItem) {
await window.ipcRenderer.invoke(
'mcp-install',
mcpItem.key,
mcpItem.install_command
);
}
} catch (e) {
console.error('Error installing MCP:', e);
} finally {
setInstalling((prev) => ({ ...prev, [id]: false }));
}
};
const handleBack = () => {
if (onBack) onBack();
else window.history.back();
};
const handleBack = () => {
if (onBack) onBack();
else window.history.back();
};
const handleDelete = async (deleteTarget: MCPItem) => {
if (!deleteTarget) return;
try {
checkAgentTool(deleteTarget.name);
console.log(userInstallMcp, deleteTarget);
const id = userInstallMcp.find(
(item: any) => item.mcp_id === deleteTarget.id
)?.id;
console.log("deleteTarget", deleteTarget);
await proxyFetchDelete(`/api/mcp/users/${id}`);
// notify main process
if (window.ipcRenderer) {
await window.ipcRenderer.invoke("mcp-remove", deleteTarget.key);
}
setInstalledIds((prev) =>
prev.filter((item) => item !== deleteTarget.id)
);
setInstalled((prev) => ({ ...prev, [deleteTarget.id]: false }));
loadData(1, debouncedKeyword, categoryId, page * 20);
} catch (e) {
console.log(e);
}
};
return (
<div className="h-full flex flex-col items-center ">
{externalKeyword === undefined && (
<>
<div className="text-body flex items-center justify-between sticky top-0 z-[20] py-2 mb-0 w-full max-w-4xl">
<Button
variant="ghost"
size="sm"
onClick={handleBack}
className="mr-2"
>
<ChevronLeft className="w-6 h-6" />
</Button>
<span className="text-base font-bold leading-12 text-text-primary">
{t("setting.mcp-market")}
</span>
</div>
<div className="w-40 max-w-4xl">
<SearchInput
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
/>
</div>
</>
)}
const handleDelete = async (deleteTarget: MCPItem) => {
if (!deleteTarget) return;
try {
checkAgentTool(deleteTarget.name);
console.log(userInstallMcp, deleteTarget);
const id = userInstallMcp.find(
(item: any) => item.mcp_id === deleteTarget.id
)?.id;
console.log('deleteTarget', deleteTarget);
await proxyFetchDelete(`/api/mcp/users/${id}`);
// notify main process
if (window.ipcRenderer) {
await window.ipcRenderer.invoke('mcp-remove', deleteTarget.key);
}
setInstalledIds((prev) =>
prev.filter((item) => item !== deleteTarget.id)
);
setInstalled((prev) => ({ ...prev, [deleteTarget.id]: false }));
loadData(1, debouncedKeyword, categoryId, page * 20);
} catch (e) {
console.log(e);
}
};
return (
<div className="flex h-full flex-col items-center">
{externalKeyword === undefined && (
<>
<div className="text-body sticky top-0 z-[20] mb-0 flex w-full max-w-4xl items-center justify-between py-2">
<Button
variant="ghost"
size="sm"
onClick={handleBack}
className="mr-2"
>
<ChevronLeft className="h-6 w-6" />
</Button>
<span className="text-base font-bold leading-12 text-text-primary">
{t('setting.mcp-market')}
</span>
</div>
<div className="w-40 max-w-4xl">
<SearchInput
value={keyword}
onChange={(e) => setKeyword(e.target.value)}
/>
</div>
</>
)}
{/* Category toggle row */}
<div className="w-full flex py-2">
<ToggleGroup
type="single"
value={categoryId ? String(categoryId) : "all"}
onValueChange={(val) => setCategoryId(!val || val === "all" ? undefined : Number(val))}
className="flex flex-wrap"
>
<ToggleGroupItem value="all">
{t("setting.all")}
</ToggleGroupItem>
{mcpCategory.map((cat) => (
<ToggleGroupItem key={cat.id} value={String(cat.id)}>
{cat.name}
</ToggleGroupItem>
))}
</ToggleGroup>
</div>
{/* Category toggle row */}
<div className="flex w-full py-2">
<ToggleGroup
type="single"
value={categoryId ? String(categoryId) : 'all'}
onValueChange={(val) =>
setCategoryId(!val || val === 'all' ? undefined : Number(val))
}
className="flex flex-wrap"
>
<ToggleGroupItem value="all">{t('setting.all')}</ToggleGroupItem>
{mcpCategory.map((cat) => (
<ToggleGroupItem key={cat.id} value={String(cat.id)}>
{cat.name}
</ToggleGroupItem>
))}
</ToggleGroup>
</div>
{/* list */}
<MCPEnvDialog
showEnvConfig={showEnvConfig}
onClose={onClose}
onConnect={onConnect}
activeMcp={activeMcp}
></MCPEnvDialog>
<div className="flex flex-col gap-4 w-full pt-4">
{isLoading && items.length === 0 && (
<div className="text-center py-8 text-text-muted">{t("setting.loading")}</div>
)}
{error && <div className="text-center py-8 text-text-error">{error}</div>}
{!isLoading && !error && items.length === 0 && (
<div className="text-center py-8 text-text-muted">{t("setting.no-mcp-services")}</div>
)}
{items.map((item) => (
<div
key={item.id}
className="p-4 bg-surface-secondary rounded-2xl flex items-center"
>
{/* Left: Icon */}
<div className="flex items-center mr-4">
{(() => {
const catName = item.category?.name;
const iconKey = catName ? categoryIconMap[catName] : undefined;
const iconUrl = iconKey
? (svgIcons[`/src/assets/mcp/${iconKey}.svg`] as string)
: undefined;
return iconUrl ? (
<img src={iconUrl} alt={catName} className="w-9 h-11" />
) : (
<Store className="w-9 h-11 text-icon-primary" />
);
})()}
</div>
<div className="flex-1 min-w-0 flex flex-col justify-center">
<div className="flex items-center gap-xs w-full pb-1">
<div className="flex items-center gap-xs flex-1">
<span className="text-base leading-9 font-bold text-text-primary truncate ">
{item.name}
</span>
<TooltipSimple content={item.description}>
<CircleAlert className="w-4 h-4 text-icon-secondary" />
</TooltipSimple>
</div>
<Button
variant={
!installedIds.includes(item.id) ? "primary" : "secondary"
}
size="sm"
onClick={() =>
installedIds.includes(item.id)
? handleDelete(item)
: checkEnv(item.id)
}
>
{installedIds.includes(item.id)
? t("setting.uninstall")
: installing[item.id]
? t("setting.installing")
: installed[item.id]
? t("setting.uninstall")
: t("setting.install")}
</Button>
</div>
{item.home_page &&
item.home_page.startsWith("https://github.com/") && (
<div className="flex items-center">
<img
src={githubIcon}
alt="github"
style={{
width: 14.7,
height: 14.7,
marginRight: 4,
display: "inline-block",
verticalAlign: "middle",
}}
/>
<span className="self-stretch items-center justify-center text-xs font-medium leading-3">
{(() => {
const parts = item.home_page.split("/");
return parts.length > 4 ? parts[4] : item.home_page;
})()}
</span>
</div>
)}
<div className="text-sm text-text-muted mt-1 break-words whitespace-pre-line">
{item.description}
</div>
</div>
</div>
))}
<div ref={loader} />
{isLoading && items.length > 0 && (
<div className="text-center py-4 text-text-muted">{t("setting.loading-more")}</div>
)}
{!hasMore && items.length > 0 && (
<div className="text-center py-4 text-text-muted">
{t("setting.no-more-mcp-servers")}
</div>
)}
</div>
</div>
);
{/* list */}
<MCPEnvDialog
showEnvConfig={showEnvConfig}
onClose={onClose}
onConnect={onConnect}
activeMcp={activeMcp}
></MCPEnvDialog>
<div className="flex w-full flex-col gap-4 pt-4">
{isLoading && items.length === 0 && (
<div className="py-8 text-center text-text-muted">
{t('setting.loading')}
</div>
)}
{error && (
<div className="py-8 text-center text-text-error">{error}</div>
)}
{!isLoading && !error && items.length === 0 && (
<div className="py-8 text-center text-text-muted">
{t('setting.no-mcp-services')}
</div>
)}
{items.map((item) => (
<div
key={item.id}
className="flex items-center rounded-2xl bg-surface-secondary p-4"
>
{/* Left: Icon */}
<div className="mr-4 flex items-center">
{(() => {
const catName = item.category?.name;
const iconKey = catName ? categoryIconMap[catName] : undefined;
const iconUrl = iconKey
? (svgIcons[`/src/assets/mcp/${iconKey}.svg`] as string)
: undefined;
return iconUrl ? (
<img src={iconUrl} alt={catName} className="h-11 w-9" />
) : (
<Store className="h-11 w-9 text-icon-primary" />
);
})()}
</div>
<div className="flex min-w-0 flex-1 flex-col justify-center">
<div className="flex w-full items-center gap-xs pb-1">
<div className="flex flex-1 items-center gap-xs">
<span className="truncate text-base font-bold leading-9 text-text-primary">
{item.name}
</span>
<TooltipSimple content={item.description}>
<CircleAlert className="h-4 w-4 text-icon-secondary" />
</TooltipSimple>
</div>
<Button
variant={
!installedIds.includes(item.id) ? 'primary' : 'secondary'
}
size="sm"
onClick={() =>
installedIds.includes(item.id)
? handleDelete(item)
: checkEnv(item.id)
}
>
{installedIds.includes(item.id)
? t('setting.uninstall')
: installing[item.id]
? t('setting.installing')
: installed[item.id]
? t('setting.uninstall')
: t('setting.install')}
</Button>
</div>
{item.home_page &&
item.home_page.startsWith('https://github.com/') && (
<div className="flex items-center">
<img
src={githubIcon}
alt="github"
style={{
width: 14.7,
height: 14.7,
marginRight: 4,
display: 'inline-block',
verticalAlign: 'middle',
}}
/>
<span className="items-center justify-center self-stretch text-xs font-medium leading-3">
{(() => {
const parts = item.home_page.split('/');
return parts.length > 4 ? parts[4] : item.home_page;
})()}
</span>
</div>
)}
<div className="mt-1 whitespace-pre-line break-words text-sm text-text-muted">
{item.description}
</div>
</div>
</div>
))}
<div ref={loader} />
{isLoading && items.length > 0 && (
<div className="py-4 text-center text-text-muted">
{t('setting.loading-more')}
</div>
)}
{!hasMore && items.length > 0 && (
<div className="py-4 text-center text-text-muted">
{t('setting.no-more-mcp-servers')}
</div>
)}
</div>
</div>
);
}

View file

@ -98,7 +98,7 @@ type SidebarTab =
export default function SettingModels() {
const { modelType, cloud_model_type, setModelType, setCloudModelType } =
useAuthStore();
const navigate = useNavigate();
const _navigate = useNavigate();
const { t } = useTranslation();
const getValidateMessage = (res: any) =>
res?.message ??
@ -106,7 +106,7 @@ export default function SettingModels() {
res?.detail?.error?.message ??
res?.error?.message ??
t('setting.validate-failed');
const [items, setItems] = useState<Provider[]>(
const [items, _setItems] = useState<Provider[]>(
INIT_PROVODERS.filter((p) => p.id !== 'local')
);
const [form, setForm] = useState(() =>
@ -139,7 +139,7 @@ export default function SettingModels() {
apiHost: '',
}))
);
const [collapsed, setCollapsed] = useState(false);
const [_collapsed, _setCollapsed] = useState(false);
// Sidebar selected tab - default to cloud
const [selectedTab, setSelectedTab] = useState<SidebarTab>('cloud');
@ -266,6 +266,7 @@ export default function SettingModels() {
setCloudPrefer(false);
}
} catch (e) {
console.error('Error fetching providers:', e);
// ignore error
}
})();
@ -274,7 +275,7 @@ export default function SettingModels() {
fetchSubscription();
updateCredits();
}
}, []);
}, [items, modelType]);
// Get current default model display text
const getDefaultModelDisplayText = (): string => {
@ -760,6 +761,7 @@ export default function SettingModels() {
setForm((f) => f.map((fi, i) => ({ ...fi, prefer: i === idx }))); // Only one prefer allowed
setLocalPrefer(false);
} catch (e) {
console.error('Error switching model:', e);
// Optional: add error message
}
};
@ -793,6 +795,7 @@ export default function SettingModels() {
setLocalPrefer(true);
setCloudPrefer(false);
} catch (e) {
console.error('Error switching local model:', e);
// Optional: add error message
}
};
@ -814,6 +817,7 @@ export default function SettingModels() {
setActiveModelIdx(null);
toast.success(t('setting.reset-success'));
} catch (e) {
console.error('Error resetting local model:', e);
toast.error(t('setting.reset-failed'));
}
};
@ -852,6 +856,7 @@ export default function SettingModels() {
}
toast.success(t('setting.reset-success'));
} catch (e) {
console.error('Error deleting model:', e);
toast.error(t('setting.reset-failed'));
}
};

View file

@ -17,18 +17,21 @@ import { Button } from '@/components/ui/button';
import { Switch } from '@/components/ui/switch';
import { useAuthStore } from '@/store/authStore';
import { ChevronDown } from 'lucide-react';
import { useEffect, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
export default function SettingPrivacy() {
const { email } = useAuthStore();
const [_privacy, setPrivacy] = useState(false);
const { t } = useTranslation();
const API_FIELDS = [
'take_screenshot',
'access_local_software',
'access_your_address',
'password_storage',
];
const API_FIELDS = useMemo(
() => [
'take_screenshot',
'access_local_software',
'access_your_address',
'password_storage',
],
[]
);
const [settings, setSettings] = useState([
{
title: t('setting.allow-agent-to-take-screenshots'),
@ -71,7 +74,7 @@ export default function SettingPrivacy() {
setPrivacy(!hasFalse);
})
.catch((err) => console.error('Failed to fetch settings:', err));
}, []);
}, [API_FIELDS]);
const handleTurnOnAll = (type: boolean) => {
const newSettings = settings.map((item) => ({
@ -90,7 +93,7 @@ export default function SettingPrivacy() {
proxyFetchPut('/api/user/privacy', requestData);
};
const handleToggle = (index: number) => {
const _handleToggle = (index: number) => {
setSettings((prev) => {
const newSettings = [...prev];
newSettings[index] = {

View file

@ -116,11 +116,11 @@ export default function CookieManager() {
<div className="rounded-2xl bg-surface-secondary px-6 py-4">
<div className="mb-4 flex items-center justify-between">
<div>
<div className="text-text-primary flex items-center gap-2 text-base font-bold leading-12">
<div className="flex items-center gap-2 text-base font-bold leading-12 text-text-primary">
<Cookie className="h-5 w-5" />
{t('setting.cookie-manager')}
</div>
<div className="text-text-secondary mt-1 text-sm leading-13">
<div className="mt-1 text-sm leading-13 text-text-secondary">
{t('setting.cookie-manager-description')}
</div>
</div>
@ -153,7 +153,7 @@ export default function CookieManager() {
{/* Search Bar */}
<div className="relative mb-4">
<Search className="text-text-tertiary absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 transform" />
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 transform text-text-tertiary" />
<Input
type="text"
placeholder={t('setting.search-domains')}
@ -166,12 +166,12 @@ export default function CookieManager() {
{/* Cookie List */}
<div className="space-y-2">
{isLoading ? (
<div className="text-text-secondary py-8 text-center">
<div className="py-8 text-center text-text-secondary">
<RefreshCw className="mx-auto mb-2 h-6 w-6 animate-spin" />
{t('setting.loading-cookies')}
</div>
) : filteredDomains.length === 0 ? (
<div className="text-text-secondary py-8 text-center">
<div className="py-8 text-center text-text-secondary">
<Cookie className="mx-auto mb-3 h-12 w-12 opacity-30" />
<div className="mb-1 text-base font-medium">
{domains.length === 0
@ -191,10 +191,10 @@ export default function CookieManager() {
className="flex items-center justify-between rounded-lg border border-border-primary bg-surface-primary p-3 transition-colors hover:border-border-secondary"
>
<div className="min-w-0 flex-1">
<div className="text-text-primary truncate font-medium">
<div className="truncate font-medium text-text-primary">
{item.domain}
</div>
<div className="text-text-tertiary mt-1 flex items-center gap-3 text-xs">
<div className="mt-1 flex items-center gap-3 text-xs text-text-tertiary">
<span>
{t('setting.cookies-count', { count: item.cookie_count })}
</span>

View file

@ -40,7 +40,7 @@ export default function MCPListItem({
<div className="mb-4 flex items-center justify-between gap-4 rounded-2xl bg-surface-secondary p-4">
<div className="flex items-center gap-xs">
<div className="mx-xs h-3 w-3 rounded-full bg-green-500"></div>
<div className="text-text-primary text-base font-bold leading-9">
<div className="text-base font-bold leading-9 text-text-primary">
{item.mcp_name}
</div>
<div className="flex items-center">

View file

@ -12,20 +12,22 @@
// limitations under the License.
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
import loginGif from '@/assets/login.gif';
import { Button } from '@/components/ui/button';
import { useAuthStore } from '@/store/authStore';
import { useStackApp } from '@stackframe/react';
import { useCallback, useEffect, useState } from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { Input } from '@/components/ui/input';
import { proxyFetchPost } from '@/api/http';
import background from '@/assets/background.png';
import eyeOff from '@/assets/eye-off.svg';
import eye from '@/assets/eye.svg';
import github2 from '@/assets/github2.svg';
import google from '@/assets/google.svg';
import eigentLogo from '@/assets/logo/eigent_icon.png';
import WindowControls from '@/components/WindowControls';
import { hasStackKeys } from '@/lib';
import { useTranslation } from 'react-i18next';
@ -52,6 +54,8 @@ export default function SignUp() {
});
const [isLoading, setIsLoading] = useState(false);
const [generalError, setGeneralError] = useState('');
const titlebarRef = useRef<HTMLDivElement | null>(null);
const [platform, setPlatform] = useState<string>('');
const validateEmail = (email: string) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
@ -262,134 +266,188 @@ export default function SignUp() {
};
}, [handleAuthCode]);
useEffect(() => {
const p = window.electronAPI.getPlatform();
setPlatform(p);
if (platform === 'darwin') {
titlebarRef.current?.classList.add('mac');
}
}, [platform]);
return (
<div className={`flex h-full items-center justify-center gap-2 p-2`}>
<div className="flex h-[calc(800px-16px)] items-center justify-center rounded-3xl bg-white-100%">
<img src={loginGif} className="h-full rounded-3xl object-cover" />
<div className="relative flex h-full flex-col overflow-hidden">
{/* Titlebar with drag region and window controls */}
<div
className="absolute left-0 right-0 top-0 z-50 flex !h-9 items-center justify-between py-1 pl-2"
id="signup-titlebar"
ref={titlebarRef}
style={{ WebkitAppRegion: 'drag' } as React.CSSProperties}
>
{/* Center drag region */}
<div
className="flex h-full flex-1 items-center"
style={{ WebkitAppRegion: 'drag' } as React.CSSProperties}
>
<div className="h-10 flex-1" />
</div>
{/* Right window controls */}
<div
style={
{
WebkitAppRegion: 'no-drag',
pointerEvents: 'auto',
} as React.CSSProperties
}
onMouseDown={(e) => e.stopPropagation()}
onClick={(e) => e.stopPropagation()}
>
<WindowControls />
</div>
</div>
<div className="flex h-full flex-1 flex-col items-center justify-center">
<div className="flex w-80 flex-1 flex-col items-center justify-center">
<div className="mb-4 flex items-end justify-between self-stretch">
<div className="text-heading-lg font-bold text-text-heading">
{t('layout.sign-up')}
{/* Main content - image extends to top, form has padding */}
<div
className={`flex h-full items-center justify-center gap-2 px-2 pb-2 pt-10`}
>
<div
className="flex h-full min-h-0 w-full flex-col items-center justify-center overflow-hidden rounded-2xl border-solid border-border-tertiary bg-surface-secondary px-2 pb-2"
style={{
backgroundImage: `url(${background})`,
backgroundSize: 'cover',
backgroundPosition: 'center',
}}
>
<div className="relative flex w-80 flex-1 flex-col items-center justify-center pt-8">
<img
src={eigentLogo}
className="absolute left-1/2 top-10 h-16 w-16 -translate-x-1/2"
/>
<div className="mb-4 flex items-end justify-between self-stretch">
<div className="text-heading-lg font-bold text-text-heading">
{t('layout.sign-up')}
</div>
<Button
variant="ghost"
size="sm"
onClick={() => navigate('/login')}
>
{t('layout.login')}
</Button>
</div>
{HAS_STACK_KEYS && (
<div className="w-full pt-6">
<Button
variant="primary"
size="lg"
onClick={() => handleReloadBtn('google')}
className="mb-4 w-full justify-center rounded-[24px] text-center font-inter text-[15px] font-bold leading-[22px] text-[#F5F5F5] transition-all duration-300 ease-in-out"
disabled={isLoading}
>
<img src={google} className="h-5 w-5" />
<span className="ml-2">
{t('layout.continue-with-google-sign-up')}
</span>
</Button>
<Button
variant="primary"
size="lg"
onClick={() => handleReloadBtn('github')}
className="mb-4 w-full justify-center rounded-[24px] text-center font-inter text-[15px] font-bold leading-[22px] text-[#F5F5F5] transition-all duration-300 ease-in-out"
disabled={isLoading}
>
<img src={github2} className="h-5 w-5" />
<span className="ml-2">
{t('layout.continue-with-github-sign-up')}
</span>
</Button>
</div>
)}
{HAS_STACK_KEYS && (
<div className="mb-6 mt-2 w-full text-center font-inter text-[15px] font-medium leading-[22px] text-[#222]">
{t('layout.or')}
</div>
)}
<div className="flex w-full flex-col gap-4">
{generalError && (
<p className="mb-4 mt-1 text-label-md text-text-cuation">
{generalError}
</p>
)}
<div className="relative mb-4 flex w-full flex-col gap-4">
<Input
id="email"
type="email"
size="default"
title={t('layout.email')}
placeholder={t('layout.enter-your-email')}
required
value={formData.email}
onChange={(e) => handleInputChange('email', e.target.value)}
state={errors.email ? 'error' : undefined}
note={errors.email}
/>
<Input
id="password"
title={t('layout.password')}
size="default"
type={hidePassword ? 'password' : 'text'}
required
placeholder={t('layout.enter-your-password')}
value={formData.password}
onChange={(e) =>
handleInputChange('password', e.target.value)
}
state={errors.password ? 'error' : undefined}
note={errors.password}
backIcon={<img src={hidePassword ? eye : eyeOff} />}
onBackIconClick={() => setHidePassword(!hidePassword)}
/>
<Input
id="invite_code"
title={t('layout.invitation-code-optional')}
size="default"
type="text"
placeholder={t('layout.enter-your-invite-code')}
value={formData.invite_code}
onChange={(e) =>
handleInputChange('invite_code', e.target.value)
}
state={errors.invite_code ? 'error' : undefined}
note={errors.invite_code}
/>
</div>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => navigate('/login')}
onClick={handleRegister}
size="md"
variant="primary"
type="submit"
className="w-full rounded-full"
disabled={isLoading}
>
{t('layout.login')}
<span className="flex-1">
{isLoading ? t('layout.signing-up') : t('layout.sign-up')}
</span>
</Button>
</div>
{HAS_STACK_KEYS && (
<div className="w-full pt-6">
<Button
variant="primary"
size="lg"
onClick={() => handleReloadBtn('google')}
className="mb-4 w-full justify-center rounded-[24px] text-center font-inter text-[15px] font-bold leading-[22px] text-[#F5F5F5] transition-all duration-300 ease-in-out"
disabled={isLoading}
>
<img src={google} className="h-5 w-5" />
<span className="ml-2">
{t('layout.continue-with-google-sign-up')}
</span>
</Button>
<Button
variant="primary"
size="lg"
onClick={() => handleReloadBtn('github')}
className="mb-4 w-full justify-center rounded-[24px] text-center font-inter text-[15px] font-bold leading-[22px] text-[#F5F5F5] transition-all duration-300 ease-in-out"
disabled={isLoading}
>
<img src={github2} className="h-5 w-5" />
<span className="ml-2">
{t('layout.continue-with-github-sign-up')}
</span>
</Button>
</div>
)}
{HAS_STACK_KEYS && (
<div className="mb-6 mt-2 w-full text-center font-inter text-[15px] font-medium leading-[22px] text-[#222]">
{t('layout.or')}
</div>
)}
<div className="flex w-full flex-col gap-4">
{generalError && (
<p className="mb-4 mt-1 text-label-md text-text-cuation">
{generalError}
</p>
)}
<div className="relative mb-4 flex w-full flex-col gap-4">
<Input
id="email"
type="email"
size="default"
title={t('layout.email')}
placeholder={t('layout.enter-your-email')}
required
value={formData.email}
onChange={(e) => handleInputChange('email', e.target.value)}
state={errors.email ? 'error' : undefined}
note={errors.email}
/>
<Input
id="password"
title={t('layout.password')}
size="default"
type={hidePassword ? 'password' : 'text'}
required
placeholder={t('layout.enter-your-password')}
value={formData.password}
onChange={(e) => handleInputChange('password', e.target.value)}
state={errors.password ? 'error' : undefined}
note={errors.password}
backIcon={<img src={hidePassword ? eye : eyeOff} />}
onBackIconClick={() => setHidePassword(!hidePassword)}
/>
<Input
id="invite_code"
title={t('layout.invitation-code-optional')}
size="default"
type="text"
placeholder={t('layout.enter-your-invite-code')}
value={formData.invite_code}
onChange={(e) =>
handleInputChange('invite_code', e.target.value)
}
state={errors.invite_code ? 'error' : undefined}
note={errors.invite_code}
/>
</div>
</div>
<Button
onClick={handleRegister}
size="md"
variant="primary"
type="submit"
className="w-full rounded-full"
disabled={isLoading}
variant="ghost"
size="xs"
onClick={() =>
window.open(
'https://www.eigent.ai/privacy-policy',
'_blank',
'noopener,noreferrer'
)
}
>
<span className="flex-1">
{isLoading ? t('layout.signing-up') : t('layout.sign-up')}
</span>
{t('layout.privacy-policy')}
</Button>
</div>
<Button
variant="ghost"
size="xs"
onClick={() =>
window.open(
'https://www.eigent.ai/privacy-policy',
'_blank',
'noopener,noreferrer'
)
}
>
{t('layout.privacy-policy')}
</Button>
</div>
</div>
);

View file

@ -13,9 +13,9 @@
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
import { generateUniqueId } from '@/lib';
import { ChatTaskStatus } from '@/types/constants';
import { create } from 'zustand';
import { createChatStoreInstance, VanillaChatStore } from './chatStore';
import { ChatTaskStatus } from '@/types/constants';
export enum ProjectType {
NORMAL = 'normal',

View file

@ -555,3 +555,33 @@ code {
[data-theme='dark'] .folder-component-content table tr:nth-child(2n) {
background-color: rgba(110, 118, 129, 0.1) !important;
}
/* ProgressInstall shimmer background */
.progress-install-shimmer {
position: absolute;
inset: 0;
z-index: 0;
background: linear-gradient(
120deg,
transparent 0%,
color-mix(in srgb, var(--colors-primary-4) 8%, transparent) 25%,
color-mix(in srgb, var(--colors-primary-4) 20%, transparent) 50%,
color-mix(in srgb, var(--colors-primary-4) 8%, transparent) 75%,
transparent 100%
);
background-size: 200% 100%;
background-position: 200% 0;
animation: progress-install-shimmer 3s ease-in-out infinite;
pointer-events: none;
border-radius: var(--borderRadius-lg);
will-change: background-position;
}
@keyframes progress-install-shimmer {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}

View file

@ -12,7 +12,12 @@
// limitations under the License.
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
import type { AgentStepType, AgentMessageStatusType, TaskStatusType, ChatTaskStatusType, AgentStatusType } from './constants';
import type {
AgentMessageStatusType,
AgentStatusType,
AgentStepType,
TaskStatusType,
} from './constants';
// Global type definitions for ChatBox component

View file

@ -16,32 +16,32 @@
* SSE step values received from the backend in AgentMessage.step.
*/
export const AgentStep = {
CONFIRMED: 'confirmed',
NEW_TASK_STATE: 'new_task_state',
END: 'end',
WAIT_CONFIRM: 'wait_confirm',
DECOMPOSE_TEXT: 'decompose_text',
TO_SUB_TASKS: 'to_sub_tasks',
CREATE_AGENT: 'create_agent',
TASK_STATE: 'task_state',
ACTIVATE_AGENT: 'activate_agent',
DEACTIVATE_AGENT: 'deactivate_agent',
ASSIGN_TASK: 'assign_task',
ACTIVATE_TOOLKIT: 'activate_toolkit',
DEACTIVATE_TOOLKIT: 'deactivate_toolkit',
TERMINAL: 'terminal',
WRITE_FILE: 'write_file',
BUDGET_NOT_ENOUGH: 'budget_not_enough',
CONTEXT_TOO_LONG: 'context_too_long',
ERROR: 'error',
ADD_TASK: 'add_task',
REMOVE_TASK: 'remove_task',
NOTICE: 'notice',
ASK: 'ask',
SYNC: 'sync',
NOTICE_CARD: 'notice_card',
FAILED: 'failed',
AGENT_SUMMARY_END: 'agent_summary_end',
CONFIRMED: 'confirmed',
NEW_TASK_STATE: 'new_task_state',
END: 'end',
WAIT_CONFIRM: 'wait_confirm',
DECOMPOSE_TEXT: 'decompose_text',
TO_SUB_TASKS: 'to_sub_tasks',
CREATE_AGENT: 'create_agent',
TASK_STATE: 'task_state',
ACTIVATE_AGENT: 'activate_agent',
DEACTIVATE_AGENT: 'deactivate_agent',
ASSIGN_TASK: 'assign_task',
ACTIVATE_TOOLKIT: 'activate_toolkit',
DEACTIVATE_TOOLKIT: 'deactivate_toolkit',
TERMINAL: 'terminal',
WRITE_FILE: 'write_file',
BUDGET_NOT_ENOUGH: 'budget_not_enough',
CONTEXT_TOO_LONG: 'context_too_long',
ERROR: 'error',
ADD_TASK: 'add_task',
REMOVE_TASK: 'remove_task',
NOTICE: 'notice',
ASK: 'ask',
SYNC: 'sync',
NOTICE_CARD: 'notice_card',
FAILED: 'failed',
AGENT_SUMMARY_END: 'agent_summary_end',
} as const;
export type AgentStepType = (typeof AgentStep)[keyof typeof AgentStep];
@ -50,24 +50,25 @@ export type AgentStepType = (typeof AgentStep)[keyof typeof AgentStep];
* Status values on AgentMessage.status (SSE message lifecycle).
*/
export const AgentMessageStatus = {
RUNNING: 'running',
FILLED: 'filled',
COMPLETED: 'completed',
RUNNING: 'running',
FILLED: 'filled',
COMPLETED: 'completed',
} as const;
export type AgentMessageStatusType = (typeof AgentMessageStatus)[keyof typeof AgentMessageStatus];
export type AgentMessageStatusType =
(typeof AgentMessageStatus)[keyof typeof AgentMessageStatus];
/**
* Status values for TaskInfo (individual sub-task progress).
*/
export const TaskStatus = {
COMPLETED: 'completed',
FAILED: 'failed',
SKIPPED: 'skipped',
WAITING: 'waiting',
RUNNING: 'running',
BLOCKED: 'blocked',
EMPTY: '',
COMPLETED: 'completed',
FAILED: 'failed',
SKIPPED: 'skipped',
WAITING: 'waiting',
RUNNING: 'running',
BLOCKED: 'blocked',
EMPTY: '',
} as const;
export type TaskStatusType = (typeof TaskStatus)[keyof typeof TaskStatus];
@ -76,22 +77,24 @@ export type TaskStatusType = (typeof TaskStatus)[keyof typeof TaskStatus];
* Top-level task status in the ChatStore Task interface.
*/
export const ChatTaskStatus = {
RUNNING: 'running',
FINISHED: 'finished',
PENDING: 'pending',
PAUSE: 'pause',
RUNNING: 'running',
FINISHED: 'finished',
PENDING: 'pending',
PAUSE: 'pause',
} as const;
export type ChatTaskStatusType = (typeof ChatTaskStatus)[keyof typeof ChatTaskStatus];
export type ChatTaskStatusType =
(typeof ChatTaskStatus)[keyof typeof ChatTaskStatus];
/**
* Status values for individual agent lifecycle (toolkit operations, agent progress).
*/
export const AgentStatusValue = {
PENDING: 'pending',
RUNNING: 'running',
COMPLETED: 'completed',
FAILED: 'failed',
PENDING: 'pending',
RUNNING: 'running',
COMPLETED: 'completed',
FAILED: 'failed',
} as const;
export type AgentStatusType = (typeof AgentStatusValue)[keyof typeof AgentStatusValue];
export type AgentStatusType =
(typeof AgentStatusValue)[keyof typeof AgentStatusValue];

File diff suppressed because it is too large Load diff