update the layout movement for topbar, worker and workspace item

This commit is contained in:
Douglas 2026-05-06 19:17:11 +01:00
parent d454cd26f2
commit f9ad2fcef2
10 changed files with 170 additions and 173 deletions

View file

@ -395,6 +395,13 @@ export function AddWorker({
const activeWorkerModelOptions = workerModelOptions[workerModelMode];
useEffect(() => {
if (!dialogOpen || !edit || !workerInfo) return;
setWorkerName(workerInfo.workerInfo?.name || '');
setWorkerDescription(workerInfo.workerInfo?.description || '');
setSelectedTools(workerInfo.workerInfo?.selectedTools || []);
}, [dialogOpen, edit, workerInfo]);
useEffect(() => {
if (!showModelConfig) return;
const options = activeWorkerModelOptions;
@ -673,7 +680,7 @@ export function AddWorker({
{showEnvConfig ? (
// environment configuration interface
<>
<DialogContentSection className="gap-3 p-md flex flex-col">
<DialogContentSection className="scrollbar-always-visible gap-3 p-md flex flex-col overflow-y-auto">
<div className="gap-md flex items-center">
{getCategoryIcon(activeMcp?.category?.name)}
<div>
@ -772,7 +779,7 @@ export function AddWorker({
) : (
// default add interface
<>
<DialogContentSection className="gap-3 p-md flex flex-col">
<DialogContentSection className="scrollbar-always-visible gap-3 p-md flex flex-col overflow-y-auto">
<div className="gap-4 flex flex-col">
<div className="gap-sm flex items-center">
<div className="h-16 w-16 flex items-center justify-center">

View file

@ -12,12 +12,6 @@
// limitations under the License.
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
import { Button } from '@/components/ui/button';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
import { TooltipSimple } from '@/components/ui/tooltip';
import { cn } from '@/lib/utils';
import type { WebSocketConnectionStatus } from '@/store/triggerStore';
@ -61,67 +55,49 @@ export function triggerListenerLeadIconClass(
return 'text-ds-icon-status-error-default';
case 'disconnected':
default:
return 'text-ds-icon-neutral-muted-default';
return '!text-ds-icon-status-error-default';
}
}
export interface NavTabReconnectSuffixProps {
wsConnectionStatus: WebSocketConnectionStatus;
reconnectHint: string;
reconnectButtonLabel: string;
onReconnect: () => void;
}
/** Optional right control for {@link NavTab} `layout="split"` (e.g. triggers reconnect). */
/** Reconnect button for the triggers tab — direct click, no dropdown. */
export function NavTabReconnectSuffix({
wsConnectionStatus,
reconnectHint,
reconnectButtonLabel,
onReconnect,
}: NavTabReconnectSuffixProps) {
return (
<Popover>
<PopoverTrigger asChild>
<button
type="button"
<TooltipSimple
content="Reconnect to trigger listener"
side="top"
sideOffset={8}
delayDuration={300}
>
<button
type="button"
className={cn(
'no-drag h-8 w-8 rounded-xl text-ds-icon-neutral-muted-default hover:bg-ds-bg-neutral-strong-default flex shrink-0 items-center justify-center transition-colors outline-none',
'focus-visible:ring-ds-ring-neutral-subtle-default focus-visible:z-10 focus-visible:ring-2 focus-visible:outline-none'
)}
aria-label="Reconnect to trigger listener"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onReconnect();
}}
>
<RefreshCw
className={cn(
'no-drag h-8 w-8 rounded-xl text-ds-icon-neutral-muted-default hover:bg-ds-bg-neutral-subtle-default flex shrink-0 items-center justify-center transition-colors outline-none',
'focus-visible:ring-ds-ring-neutral-subtle-default focus-visible:z-10 focus-visible:ring-2 focus-visible:outline-none'
'h-3.5 w-3.5',
wsConnectionStatus === 'connecting' && 'animate-spin'
)}
aria-label={reconnectHint}
>
<RefreshCw
className={cn(
'h-3.5 w-3.5',
wsConnectionStatus === 'connecting' && 'animate-spin'
)}
aria-hidden
/>
</button>
</PopoverTrigger>
<PopoverContent className="w-64 p-4" side="right" align="start">
<div className="gap-3 flex flex-col">
<p className="text-body-sm text-ds-text-neutral-default-default">
{reconnectHint}
</p>
<Button
variant="primary"
size="sm"
className="w-full items-center justify-center"
onClick={onReconnect}
>
<RefreshCw
className={cn(
'mr-2 h-4 w-4',
wsConnectionStatus === 'connecting' && 'animate-spin'
)}
aria-hidden
/>
{reconnectButtonLabel}
</Button>
</div>
</PopoverContent>
</Popover>
aria-hidden
/>
</button>
</TooltipSimple>
);
}
@ -146,6 +122,8 @@ export interface NavTabProps {
suffix?: ReactNode;
/** Split only: extra control after `suffix`; shown when the tab row is hovered (or focused within). */
endAction?: ReactNode;
/** Override the max-width reveal class on the endAction wrapper (default: `group-hover:max-w-10`). */
endActionMaxWidthClass?: string;
tooltip: string;
/** When true, tooltips are hidden (labels are visible in the fixed-width sidebar). */
tooltipEnabledWhenCollapsed?: boolean;
@ -233,6 +211,7 @@ export function NavTab({
mainButtonClassName,
folded = false,
endAction,
endActionMaxWidthClass,
}: NavTabProps) {
const inner = tabMainInner({
leading,
@ -293,8 +272,11 @@ export function NavTab({
<div
className={cn(
'max-w-0 ease-out flex shrink-0 items-center justify-end overflow-hidden opacity-0 transition-[max-width,opacity] duration-150',
'group-hover:max-w-10 pointer-events-none group-hover:pointer-events-auto group-hover:opacity-100',
'focus-within:max-w-10 focus-within:pointer-events-auto focus-within:opacity-100'
'pointer-events-none opacity-0',
'group-hover:pointer-events-auto group-hover:opacity-100',
endActionMaxWidthClass ??
'group-hover:max-w-10 focus-within:max-w-10',
'focus-within:pointer-events-auto focus-within:opacity-100'
)}
>
{endAction}

View file

@ -70,8 +70,6 @@ export default function ProjectPageSidebar({
const wsConnectionStatus = useTriggerStore((s) => s.wsConnectionStatus);
const triggerReconnect = useTriggerStore((s) => s.triggerReconnect);
const triggersListenerConnected = wsConnectionStatus === 'connected';
const showTriggersDisconnectedTag =
wsConnectionStatus === 'disconnected' || wsConnectionStatus === 'unhealthy';
const projectStore = useProjectStore();
const activeProjectId = projectStore.activeProjectId;
const folderTabHasUnviewedFiles =
@ -346,50 +344,38 @@ export default function ProjectPageSidebar({
)
}
label="Scheduled"
trailing={
showTriggersDisconnectedTag ? (
<span className="rounded-md px-1.5 font-medium leading-tight bg-ds-bg-neutral-default-default text-ds-text-status-error-strong-default max-w-[5.5rem] shrink-0 truncate py-px text-[10px]">
{t('layout.triggers-disconnected')}
</span>
) : undefined
}
showNotificationDot={unviewedTabs.has('triggers')}
notificationDotTone="attention"
notificationDotClassName="h-2 w-2"
suffix={
wsConnectionStatus !== 'connected' ? (
endAction={
triggersListenerConnected ? (
<Button
type="button"
variant="ghost"
size="sm"
buttonContent="icon-only"
className={cn(
'no-drag mr-1 rounded-xl hover:bg-ds-bg-neutral-strong-default shrink-0',
'focus-visible:ring-ds-border-neutral-default-default focus-visible:z-10 focus-visible:ring-2 focus-visible:outline-none'
)}
aria-label={t('triggers.add-trigger')}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
requestOpenTriggerAddDialog();
}}
>
<Plus
className="h-4 w-4 text-ds-icon-neutral-muted-default"
aria-hidden
/>
</Button>
) : (
<NavTabReconnectSuffix
wsConnectionStatus={wsConnectionStatus}
reconnectHint={t('layout.triggers-reconnect-hint')}
reconnectButtonLabel={t(
'layout.triggers-listener-reconnect'
)}
onReconnect={triggerReconnect}
/>
) : undefined
}
endAction={
<Button
type="button"
variant="ghost"
size="sm"
buttonContent="icon-only"
className={cn(
'no-drag mr-1 rounded-xl hover:bg-ds-bg-neutral-strong-default shrink-0',
'focus-visible:ring-ds-border-neutral-default-default focus-visible:z-10 focus-visible:ring-2 focus-visible:outline-none'
)}
aria-label={t('triggers.add-trigger')}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
requestOpenTriggerAddDialog();
}}
>
<Plus
className="h-4 w-4 text-ds-icon-neutral-muted-default"
aria-hidden
/>
</Button>
)
}
tooltip={triggersTabTooltip}
tooltipEnabledWhenCollapsed={!projectSidebarFolded}

View file

@ -320,6 +320,47 @@ function HeaderWin() {
>
{/* Leading: home ↔ dashboard / new project */}
<div className="no-drag flex shrink-0 items-center justify-center">
{isHistoryRoute ? (
<div className="no-drag h-[28px] w-[28px] shrink-0" aria-hidden />
) : (
<TooltipSimple
content={
projectSidebarFolded
? t('layout.expand-project-sidebar', {
defaultValue: 'Expand sidebar',
})
: t('layout.fold-project-sidebar', {
defaultValue: 'Fold sidebar',
})
}
side="bottom"
align="center"
>
<Button
variant="ghost"
size="sm"
buttonContent="icon-only"
className="no-drag shrink-0 rounded-full"
onClick={() => toggleProjectSidebarFolded()}
aria-pressed={!projectSidebarFolded}
aria-label={
projectSidebarFolded
? t('layout.expand-project-sidebar', {
defaultValue: 'Expand sidebar',
})
: t('layout.fold-project-sidebar', {
defaultValue: 'Fold sidebar',
})
}
>
{projectSidebarFolded ? (
<PanelLeft className="h-4 w-4" aria-hidden />
) : (
<PanelLeftClose className="h-4 w-4" aria-hidden />
)}
</Button>
</TooltipSimple>
)}
<TooltipSimple
content={
isHistoryRoute ? t('layout.new-project') : t('layout.dashboard')
@ -417,43 +458,6 @@ function HeaderWin() {
>
<div className="min-w-0 min-h-0 relative z-50 flex h-full items-center">
<div className="no-drag min-w-0 flex items-center">
<TooltipSimple
content={
projectSidebarFolded
? t('layout.expand-project-sidebar', {
defaultValue: 'Expand sidebar',
})
: t('layout.fold-project-sidebar', {
defaultValue: 'Fold sidebar',
})
}
side="bottom"
align="center"
>
<Button
variant="ghost"
size="sm"
buttonContent="icon-only"
className="no-drag shrink-0 rounded-full"
onClick={() => toggleProjectSidebarFolded()}
aria-pressed={!projectSidebarFolded}
aria-label={
projectSidebarFolded
? t('layout.expand-project-sidebar', {
defaultValue: 'Expand sidebar',
})
: t('layout.fold-project-sidebar', {
defaultValue: 'Fold sidebar',
})
}
>
{projectSidebarFolded ? (
<PanelLeft className="h-4 w-4" aria-hidden />
) : (
<PanelLeftClose className="h-4 w-4" aria-hidden />
)}
</Button>
</TooltipSimple>
<TooltipSimple
content={t('layout.back')}
side="bottom"

View file

@ -45,7 +45,7 @@ import {
FileText,
Globe,
Image,
Info,
Pencil,
Trash2,
} from 'lucide-react';
import { type ReactNode, useState } from 'react';
@ -112,9 +112,10 @@ export function FoldedAgentCard({
borderless?: boolean;
/** Compact icon card: click opens a menu instead of calling `onSelect` directly. */
compactContextMenu?: {
onDetail: () => void;
onEdit: () => void;
onDuplicate: () => void;
onDelete: () => void;
editEnabled?: boolean;
duplicateEnabled?: boolean;
deleteEnabled?: boolean;
};
@ -205,16 +206,19 @@ export function FoldedAgentCard({
<DropdownMenuContent align="start" side="bottom" sideOffset={8}>
<DropdownMenuItem
className="gap-2 cursor-pointer"
disabled={compactContextMenu.editEnabled === false}
onSelect={(e) => {
e.preventDefault();
compactContextMenu.onDetail();
if (compactContextMenu.editEnabled !== false) {
compactContextMenu.onEdit();
}
}}
>
<Info
<Pencil
className="h-4 w-4 text-ds-icon-neutral-default-default shrink-0"
aria-hidden
/>
{t('workforce.detail', { defaultValue: 'Detail' })}
{t('workforce.edit', { defaultValue: 'Edit' })}
</DropdownMenuItem>
<DropdownMenuItem
className="gap-2 cursor-pointer"

View file

@ -25,7 +25,7 @@ export interface WorkforceAgentListProps {
sortedAgents: Agent[];
activeAgentId: string | undefined;
onSelectAgent: (agentId: string) => void;
onAgentDetailFromMenu: (agentId: string) => void;
onEditWorkerFromMenu: (agent: Agent) => void;
onDuplicateUserAgent: (agent: Agent) => void;
onDeleteUserAgent: (agentId: string) => void;
onAddWorker: () => void;
@ -38,7 +38,7 @@ export function WorkforceAgentList({
sortedAgents,
activeAgentId,
onSelectAgent,
onAgentDetailFromMenu,
onEditWorkerFromMenu,
onDuplicateUserAgent,
onDeleteUserAgent,
onAddWorker,
@ -65,9 +65,10 @@ export function WorkforceAgentList({
onSelect={() => onSelectAgent(agent.agent_id)}
showUserAgentOverflow={false}
compactContextMenu={{
onDetail: () => onAgentDetailFromMenu(agent.agent_id),
onEdit: () => onEditWorkerFromMenu(agent),
onDuplicate: () => onDuplicateUserAgent(agent),
onDelete: () => onDeleteUserAgent(agent.agent_id),
editEnabled: !isBaseWorkflowAgent(agent),
duplicateEnabled: !isBaseWorkflowAgent(agent),
deleteEnabled: !isBaseWorkflowAgent(agent),
}}

View file

@ -30,6 +30,7 @@ import { usePageTabStore } from '@/store/pageTabStore';
import type { ProjectGroup } from '@/types/history';
import {
ChevronDown,
FolderIcon,
FolderKanban,
FolderOpen,
PlusCircle,
@ -158,17 +159,15 @@ export function WorkspaceProjectPicker() {
size="md"
buttonContent="text"
buttonRadius="full"
className="no-drag bg-ds-bg-neutral-subtle-default px-3 py-1 font-semibold shadow-soft hover:bg-ds-bg-neutral-default-hover inline-flex h-auto w-fit max-w-[300px] justify-between"
className="no-drag bg-ds-bg-neutral-subtle-default shadow-workspace-project-picker px-3 py-1 font-semibold hover:bg-ds-bg-neutral-default-hover inline-flex h-auto w-fit max-w-[300px] min-w-[180px] justify-between"
aria-expanded={menuOpen}
aria-haspopup="menu"
>
<FolderIcon className="size-4 shrink-0" aria-hidden />
<span className="text-ds-text-neutral-default-default min-w-0 text-label-sm truncate">
{activeTaskTitle}
</span>
<ChevronDown
className="text-ds-icon-neutral-muted-default h-2.5 w-2.5 shrink-0 opacity-80"
aria-hidden
/>
<ChevronDown className="shrink-0 opacity-80" aria-hidden />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent

View file

@ -82,6 +82,9 @@ export default function Workspace() {
const { hasModel } = useModelConfigCheck();
const [useCloudModelInDev, setUseCloudModelInDev] = useState(false);
const [addWorkerDialogOpen, setAddWorkerDialogOpen] = useState(false);
const [editingWorkerAgent, setEditingWorkerAgent] = useState<Agent | null>(
null
);
const [workspaceWorkWithPanelOpen, setWorkspaceWorkWithPanelOpen] =
useState(false);
const textareaRef = useRef<HTMLDivElement>(null);
@ -212,13 +215,9 @@ export default function Workspace() {
[chatStore, host]
);
const onAgentDetailFromMenu = useCallback(
(agentId: string) => {
onSelectAgent(agentId);
setActiveWorkspaceTab('session');
},
[onSelectAgent, setActiveWorkspaceTab]
);
const onEditWorkerFromMenu = useCallback((agent: Agent) => {
setEditingWorkerAgent(agent);
}, []);
const onDuplicateUserAgent = useCallback(
(agent: Agent) => {
@ -274,8 +273,8 @@ export default function Workspace() {
});
return (
<div className="relative flex h-full min-h-0 w-full flex-col">
<div className="relative z-50 flex h-[44px] w-full shrink-0 flex-row items-center justify-start px-3">
<div className="min-h-0 relative flex h-full w-full flex-col">
<div className="px-3 relative z-50 flex h-[44px] w-full shrink-0 flex-row items-center justify-start">
<TooltipSimple content={workWithPanelToggleLabel} delayDuration={300}>
<Button
type="button"
@ -285,21 +284,21 @@ export default function Workspace() {
onClick={() => setWorkspaceWorkWithPanelOpen((open) => !open)}
aria-expanded={workspaceWorkWithPanelOpen}
aria-controls="workspace-work-with-panel"
className="no-drag shrink-0 text-ds-text-neutral-muted-default hover:bg-ds-bg-neutral-strong-default"
className="no-drag text-ds-text-neutral-muted-default hover:bg-ds-bg-neutral-strong-default shrink-0"
aria-label={workWithPanelToggleLabel}
>
<Cast className="h-4 w-4" aria-hidden />
</Button>
</TooltipSimple>
</div>
<div className="relative z-0 flex min-h-0 w-full flex-1 flex-col items-stretch overflow-hidden">
<div className="flex min-h-0 w-full flex-1 flex-col px-3">
<div className="min-h-0 relative z-0 flex w-full flex-1 flex-col items-stretch overflow-hidden">
<div className="min-h-0 px-3 flex w-full flex-1 flex-col">
<div className="mx-auto flex w-full max-w-[600px] shrink-0 flex-col">
<div className="flex min-h-[50vh] w-full min-w-0 flex-col justify-end">
<div className="min-w-0 flex min-h-[50vh] w-full flex-col justify-end">
<div className="mb-8 flex w-full justify-center">
<WorkspaceProjectPicker />
</div>
<span className="mb-8 w-full text-center text-heading-lg font-bold text-ds-text-neutral-default-default">
<span className="mb-8 text-heading-lg font-bold text-ds-text-neutral-default-default w-full text-center">
{sessionSidePanelMode === SessionMode.SINGLE_AGENT
? t('layout.workspace-cowork-single-agent', {
defaultValue: 'Cowork with Single Agent',
@ -308,7 +307,7 @@ export default function Workspace() {
defaultValue: 'Cowork with Workforce',
})}
</span>
<div className="mb-8 flex w-full justify-center px-5">
<div className="mb-8 px-5 flex w-full justify-center">
{sessionSidePanelMode === SessionMode.SINGLE_AGENT ? (
<SingleAgentList />
) : (
@ -316,7 +315,7 @@ export default function Workspace() {
sortedAgents={sortedAgents}
activeAgentId={activeAgentId}
onSelectAgent={onSelectAgent}
onAgentDetailFromMenu={onAgentDetailFromMenu}
onEditWorkerFromMenu={onEditWorkerFromMenu}
onDuplicateUserAgent={onDuplicateUserAgent}
onDeleteUserAgent={onDeleteUserAgent}
onAddWorker={() => setAddWorkerDialogOpen(true)}
@ -364,11 +363,21 @@ export default function Workspace() {
isOpen={addWorkerDialogOpen}
onOpenChange={setAddWorkerDialogOpen}
/>
{editingWorkerAgent && (
<AddWorker
edit
workerInfo={editingWorkerAgent}
isOpen={true}
onOpenChange={(open) => {
if (!open) setEditingWorkerAgent(null);
}}
/>
)}
</div>
</div>
<div
className="flex min-h-0 w-full flex-1 flex-col overflow-y-auto pt-6"
className="min-h-0 pt-6 flex w-full flex-1 flex-col overflow-y-auto"
id="workspace-bottom-group"
>
{showWorkspaceExamplePrompts ? (
@ -395,7 +404,7 @@ export default function Workspace() {
<>
<button
type="button"
className="absolute inset-0 z-40 cursor-default bg-transparent backdrop-blur-[1px]"
className="inset-0 absolute z-40 cursor-default bg-transparent backdrop-blur-[1px]"
aria-label={t('layout.workspace-work-with-dismiss-overlay', {
defaultValue: 'Dismiss',
})}
@ -406,10 +415,10 @@ export default function Workspace() {
role="dialog"
aria-modal="true"
aria-labelledby="workspace-work-with-heading"
className="absolute left-0 top-8 z-50 flex max-h-[calc(100%-2.75rem)] w-[300px] flex-col overflow-y-auto duration-200 ease-out animate-in fade-in-0 slide-in-from-left-2"
className="left-0 top-8 ease-out animate-in fade-in-0 slide-in-from-left-2 absolute z-50 flex max-h-[calc(100%-2.75rem)] w-[300px] flex-col overflow-y-auto duration-200"
>
<div className="flex flex-col gap-3 p-3">
<div className="flex min-w-0 flex-col rounded-xl border border-solid border-ds-border-neutral-subtle-default bg-ds-bg-neutral-subtle-default p-3">
<div className="gap-3 p-3 flex flex-col">
<div className="min-w-0 rounded-xl border-ds-border-neutral-subtle-default bg-ds-bg-neutral-subtle-default p-3 flex flex-col border border-solid">
<span
id="workspace-work-with-heading"
className="text-body-sm font-semibold text-ds-text-neutral-default-default"
@ -418,7 +427,7 @@ export default function Workspace() {
defaultValue: 'Work with',
})}
</span>
<div className="mt-3 flex flex-col gap-1">
<div className="mt-3 gap-1 flex flex-col">
<Button
type="button"
variant="ghost"
@ -426,10 +435,10 @@ export default function Workspace() {
emphasis="default"
size="sm"
buttonContent="text"
className="no-drag justify-start gap-2"
className="no-drag gap-2 justify-start"
>
<MonitorSmartphone
className="h-4 w-4 shrink-0 text-ds-text-neutral-muted-default"
className="h-4 w-4 text-ds-text-neutral-muted-default shrink-0"
aria-hidden
/>
{t('layout.workspace-work-with-remote-control', {
@ -443,7 +452,7 @@ export default function Workspace() {
emphasis="default"
size="sm"
buttonContent="text"
className="no-drag justify-start gap-2"
className="no-drag gap-2 justify-start"
aria-label={t('layout.channels-telegram', {
defaultValue: 'Telegram',
})}
@ -465,7 +474,7 @@ export default function Workspace() {
emphasis="default"
size="sm"
buttonContent="text"
className="no-drag justify-start gap-2"
className="no-drag gap-2 justify-start"
aria-label={t('layout.channels-lark', {
defaultValue: 'Lark',
})}
@ -473,7 +482,7 @@ export default function Workspace() {
<img
src={larkIcon}
alt=""
className="h-4 w-4 shrink-0 rounded-lg object-contain"
className="h-4 w-4 rounded-lg shrink-0 object-contain"
aria-hidden
/>
{t('layout.channels-lark', { defaultValue: 'Lark' })}
@ -485,7 +494,7 @@ export default function Workspace() {
emphasis="default"
size="sm"
buttonContent="text"
className="no-drag justify-start gap-2"
className="no-drag gap-2 justify-start"
aria-label={t('layout.channels-whatsapp', {
defaultValue: 'WhatsApp',
})}

View file

@ -90,6 +90,10 @@
-moz-osx-font-smoothing: grayscale;
color-scheme: light dark;
--shadow-workspace-project-picker:
inset 0 0 0 1px var(--ds-bg-neutral-subtle-default),
0 0 4px var(--ds-bg-neutral-strong-default);
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;

View file

@ -748,6 +748,7 @@ module.exports = {
soft: 'var(--shadow-soft)',
'blur-effect': 'var(--shadow-blur-effect)',
'button-shadow': 'var(--shadow-button)',
'workspace-project-picker': 'var(--shadow-workspace-project-picker)',
},
spacing: {
xs: 'var(--spacing-xs, 4px)',