+ );
+}
diff --git a/src/components/Workspace/WorkspaceDispatch.tsx b/src/components/Workspace/WorkspaceDispatch.tsx
new file mode 100644
index 000000000..acf9c92f3
--- /dev/null
+++ b/src/components/Workspace/WorkspaceDispatch.tsx
@@ -0,0 +1,191 @@
+// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
+
+import larkIcon from '@/assets/icon/lark.png';
+import telegramIcon from '@/assets/icon/telegram.svg';
+import whatsappIcon from '@/assets/icon/whatsapp.svg';
+import { Badge } from '@/components/ui/badge';
+import { Button } from '@/components/ui/button';
+import type { WebSocketConnectionStatus } from '@/store/triggerStore';
+import { useTriggerStore } from '@/store/triggerStore';
+import { Copy, MonitorSmartphone } from 'lucide-react';
+import type { ReactNode } from 'react';
+import { useTranslation } from 'react-i18next';
+import { toast } from 'sonner';
+
+function statusDotClass(status: WebSocketConnectionStatus): string {
+ switch (status) {
+ case 'connected':
+ return 'bg-green-500';
+ case 'connecting':
+ return 'bg-yellow-400 animate-pulse';
+ case 'unhealthy':
+ case 'disconnected':
+ default:
+ return 'bg-red-500';
+ }
+}
+
+interface DispatchChannelCardProps {
+ name: string;
+ icon?: string;
+ leading?: ReactNode;
+ disabled?: boolean;
+ connectionStatus?: WebSocketConnectionStatus;
+ badgeText?: string;
+ action?: {
+ label: string;
+ icon?: ReactNode;
+ onClick: () => void;
+ };
+}
+
+function DispatchChannelCard({
+ name,
+ icon,
+ leading,
+ disabled,
+ connectionStatus,
+ badgeText,
+ action,
+}: DispatchChannelCardProps) {
+ return (
+
+ );
+}
diff --git a/src/components/Workspace/WorkspaceInstructionMd.tsx b/src/components/Workspace/WorkspaceInstructionMd.tsx
new file mode 100644
index 000000000..ccafa1532
--- /dev/null
+++ b/src/components/Workspace/WorkspaceInstructionMd.tsx
@@ -0,0 +1,66 @@
+// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
+
+import { useEffect, useState } from 'react';
+import { useTranslation } from 'react-i18next';
+
+interface WorkspaceInstructionMdProps {
+ projectId: string;
+}
+
+function storageKey(projectId: string) {
+ return `eigent-instructions-md-${projectId}`;
+}
+
+export function WorkspaceInstructionMd({
+ projectId,
+}: WorkspaceInstructionMdProps) {
+ const { t } = useTranslation();
+ const [content, setContent] = useState(
+ () => localStorage.getItem(storageKey(projectId)) ?? ''
+ );
+
+ useEffect(() => {
+ localStorage.setItem(storageKey(projectId), content);
+ }, [content, projectId]);
+
+ useEffect(() => {
+ const onSave = (e: Event) => {
+ const custom = e as CustomEvent<{ projectId?: string }>;
+ if (!custom.detail?.projectId || custom.detail.projectId !== projectId)
+ return;
+ localStorage.setItem(storageKey(projectId), content);
+ };
+
+ window.addEventListener('workspace-instruction-md-save', onSave);
+ return () => {
+ window.removeEventListener('workspace-instruction-md-save', onSave);
+ };
+ }, [content, projectId]);
+
+ return (
+
+
+ );
+}
diff --git a/src/components/Workspace/index.tsx b/src/components/Workspace/index.tsx
index 491016a9f..73026d765 100644
--- a/src/components/Workspace/index.tsx
+++ b/src/components/Workspace/index.tsx
@@ -23,17 +23,23 @@ import { BASE_WORKFLOW_AGENTS } from '@/components/WorkFlow/baseWorkers';
import { isBaseWorkflowAgent } from '@/components/Workspace/FoldedAgentCard';
import { SingleAgentList } from '@/components/Workspace/SingleAgentList';
import { WorkforceAgentList } from '@/components/Workspace/WorkforceAgentList';
+import { WorkspaceAllSessions } from '@/components/Workspace/WorkspaceAllSessions';
+import { WorkspaceCoworkPanel } from '@/components/Workspace/WorkspaceCoworkPanel';
+import { WorkspaceDispatch } from '@/components/Workspace/WorkspaceDispatch';
import { WorkspaceExamplePrompts } from '@/components/Workspace/WorkspaceExamplePrompts';
+import { WorkspaceInstructionMd } from '@/components/Workspace/WorkspaceInstructionMd';
import { WorkspaceProjectPicker } from '@/components/Workspace/WorkspaceProjectPicker';
import { WorkspaceRecentSessions } from '@/components/Workspace/WorkspaceRecentSessions';
import useChatStoreAdapter from '@/hooks/useChatStoreAdapter';
import { useModelConfigCheck } from '@/hooks/useModelConfigCheck';
import { useHost } from '@/host';
+import { cn } from '@/lib/utils';
import { useAuthStore, useWorkerList } from '@/store/authStore';
import { usePageTabStore } from '@/store/pageTabStore';
import { useProjectStore } from '@/store/projectStore';
import { SessionMode } from '@/types/constants';
-import { Cast, MonitorSmartphone } from 'lucide-react';
+import { AnimatePresence, motion } from 'framer-motion';
+import { ArrowLeft, Cast, MonitorSmartphone, ScrollText } from 'lucide-react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
@@ -41,6 +47,15 @@ import { toast } from 'sonner';
const EMPTY_TASK_ASSIGNING: Agent[] = [];
+const MEMORY_STORAGE_KEY = 'eigent-sidebar-instructions-memory-on';
+
+function readMemoryInitial(): boolean {
+ if (typeof window === 'undefined') return true;
+ const v = window.localStorage.getItem(MEMORY_STORAGE_KEY);
+ if (v === null) return true;
+ return v === 'true';
+}
+
/**
* Workspace tab: project landing with a centered task input.
* After the user starts a task, it switches to the session tab.
@@ -69,6 +84,10 @@ export default function Workspace() {
return true;
}, [activeProject, customAgentFolderPath, isEmptyProject]);
const setActiveWorkspaceTab = usePageTabStore((s) => s.setActiveWorkspaceTab);
+ const activeWorkspaceTab = usePageTabStore((s) => s.activeWorkspaceTab);
+ const workspaceChatFocusRequestId = usePageTabStore(
+ (s) => s.workspaceChatFocusRequestId
+ );
const sessionSidePanelMode = usePageTabStore(
(s) => s.sessionSidePanelMode ?? SessionMode.WORKFORCE
);
@@ -85,18 +104,47 @@ export default function Workspace() {
const [editingWorkerAgent, setEditingWorkerAgent] = useState(
null
);
- const [workspaceWorkWithPanelOpen, setWorkspaceWorkWithPanelOpen] =
- useState(false);
+ const [leftPanelTab, setLeftPanelTab] = useState<
+ 'instructions' | 'workWith' | null
+ >(null);
+ type WorkspaceSubPage = 'all-sessions' | 'instruction-md' | 'dispatch' | null;
+ const [workspaceSubPage, setWorkspaceSubPage] =
+ useState(null);
+ const SUB_PAGE_TITLES: Record, string> = {
+ 'all-sessions': t('layout.sessions-full-title', {
+ defaultValue: 'All sessions',
+ }),
+ 'instruction-md': t('layout.instructions-rules-tone', {
+ defaultValue: 'Rules & Tone',
+ }),
+ dispatch: t('layout.workspace-work-with-title', {
+ defaultValue: 'Dispatch',
+ }),
+ };
+ const [memoryOn, setMemoryOn] = useState(readMemoryInitial);
const textareaRef = useRef(null);
useEffect(() => {
- if (!workspaceWorkWithPanelOpen) return;
+ window.localStorage.setItem(MEMORY_STORAGE_KEY, String(memoryOn));
+ }, [memoryOn]);
+
+ useEffect(() => {
+ if (workspaceChatFocusRequestId === 0) return;
+ if (activeWorkspaceTab !== 'workforce') return;
+ const focusTimer = window.setTimeout(() => {
+ textareaRef.current?.focus();
+ }, 180);
+ return () => window.clearTimeout(focusTimer);
+ }, [workspaceChatFocusRequestId, activeWorkspaceTab]);
+
+ useEffect(() => {
+ if (!leftPanelTab) return;
const onKeyDown = (e: KeyboardEvent) => {
- if (e.key === 'Escape') setWorkspaceWorkWithPanelOpen(false);
+ if (e.key === 'Escape') setLeftPanelTab(null);
};
window.addEventListener('keydown', onKeyDown);
return () => window.removeEventListener('keydown', onKeyDown);
- }, [workspaceWorkWithPanelOpen]);
+ }, [leftPanelTab]);
useEffect(() => {
if (
@@ -264,257 +312,404 @@ export default function Workspace() {
const activeAgentId = chatStore.tasks[chatStore.activeTaskId]?.activeAgent;
- const workWithPanelToggleLabel = workspaceWorkWithPanelOpen
- ? t('layout.workspace-work-with-panel-hide', {
- defaultValue: 'Hide Work with panel',
- })
- : t('layout.workspace-work-with-panel-show', {
- defaultValue: 'Show Work with panel',
- });
+ const instructionsLabel = t('layout.instructions', {
+ defaultValue: 'Instructions',
+ });
+ const workWithLabel = t('layout.workspace-work-with-title', {
+ defaultValue: 'Work with',
+ });
return (
-