feat(app): update workspace shell, nav, and integrations

Refresh IntegrationList, session rows, TopBar, and recent sessions. Remove recent-session delete from Workspace in favor of handling elsewhere.

Made-with: Cursor
This commit is contained in:
Douglas 2026-04-24 00:17:41 +01:00
parent 49f3b16c6d
commit 3f840fff88
5 changed files with 146 additions and 136 deletions

View file

@ -14,10 +14,10 @@
import { fetchGet, fetchPost } from '@/api/http';
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import {
Tooltip,
TooltipContent,
TooltipSimple,
TooltipTrigger,
} from '@/components/ui/tooltip';
import { CircleAlert, Settings2 } from 'lucide-react';
@ -36,6 +36,7 @@ import {
} from '@/hooks/useIntegrationManagement';
import { getProxyBaseURL } from '@/lib';
import { OAuth } from '@/lib/oauth';
import { cn } from '@/lib/utils';
import { MCPEnvDialog } from '@/pages/Connectors/components/MCPEnvDialog';
import React, { useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
@ -65,6 +66,11 @@ interface IntegrationListProps {
installedKeys?: string[];
oauth?: OAuth | null;
translationNamespace?: 'layout' | 'setting'; // For translation keys
className?: string;
/** Select mode: show a leading checkbox; use isIntegrationSelected + onToggleIntegration */
selectWithCheckbox?: boolean;
isIntegrationSelected?: (item: IntegrationItem) => boolean;
onToggleIntegration?: (item: IntegrationItem, selected: boolean) => void;
}
export default function IntegrationList({
@ -84,6 +90,10 @@ export default function IntegrationList({
installedKeys: _installedKeys = [],
oauth: _oauth,
translationNamespace = variant === 'select' ? 'layout' : 'setting',
className: rootClassName,
selectWithCheckbox = false,
isIntegrationSelected,
onToggleIntegration,
}: IntegrationListProps) {
const { t } = useTranslation();
const [showEnvConfig, setShowEnvConfig] = useState(false);
@ -294,15 +304,15 @@ export default function IntegrationList({
: 'flex flex-col w-full items-start justify-start gap-4';
const itemClassName = isSelectMode
? 'cursor-pointer hover:bg-ds-bg-neutral-default-hover px-3 py-2 flex justify-between'
? 'cursor-pointer gap-2 rounded-lg bg-ds-bg-neutral-subtle-default px-3 py-2 min-h-0 flex w-full items-center justify-between hover:bg-ds-bg-neutral-default-hover'
: 'w-full px-6 py-4 bg-ds-bg-neutral-subtle-default rounded-2xl';
const titleClassName = isSelectMode
? 'text-base leading-snug font-bold text-ds-text-brand-default-default'
? 'min-w-0 flex-1 text-sm font-bold leading-5 text-ds-text-neutral-default-default sm:text-base line-clamp-2 break-words'
: 'text-label-lg font-bold text-ds-text-neutral-default-default';
return (
<div className={containerClassName}>
<div className={cn(containerClassName, rootClassName)}>
<MCPEnvDialog
showEnvConfig={showEnvConfig}
onClose={onClose}
@ -312,6 +322,12 @@ export default function IntegrationList({
{sortedItems.map((item) => {
const isInstalled = !!installed[item.key];
const isComingSoon = COMING_SOON_ITEMS.includes(item.name);
const envCount = item.env_vars?.length ?? 0;
const selectedForTool =
selectWithCheckbox && isIntegrationSelected
? isIntegrationSelected(item)
: false;
const checkboxDisabled = isComingSoon || (!isInstalled && envCount > 0);
return (
<div key={item.key} className="w-full">
@ -320,45 +336,48 @@ export default function IntegrationList({
onClick={
isSelectMode
? () => {
if (!isComingSoon) {
if (item.env_vars.length === 0 || isInstalled) {
// Ensure toolkit field is passed and normalized for known cases
const normalizedToolkit =
item.name === 'Notion'
? 'notion_mcp_toolkit'
: item.toolkit;
addOption?.(
{ ...item, toolkit: normalizedToolkit },
true
);
if (isComingSoon) return;
if (selectWithCheckbox && onToggleIntegration) {
if (isInstalled) {
onToggleIntegration(item, !selectedForTool);
} else if (envCount === 0) {
onToggleIntegration(item, !selectedForTool);
} else {
handleInstall(item);
}
return;
}
if (envCount === 0 || isInstalled) {
const normalizedToolkit =
item.name === 'Notion'
? 'notion_mcp_toolkit'
: item.toolkit;
addOption?.(
{ ...item, toolkit: normalizedToolkit },
true
);
} else {
handleInstall(item);
}
}
: undefined
}
>
{isSelectMode ? (
<div className="gap-xs flex items-center">
{(isSelectMode || showStatusDot) && (
<img
src={ellipseIcon}
alt="icon"
className="mr-2 h-3 w-3"
style={{
filter: isInstalled
? 'grayscale(0%) brightness(0) saturate(100%) invert(41%) sepia(99%) saturate(749%) hue-rotate(81deg) brightness(95%) contrast(92%)'
: 'none',
<div className="gap-2 min-w-0 min-h-0 flex flex-1 items-center">
{selectWithCheckbox && (
<Checkbox
disabled={checkboxDisabled}
checked={selectedForTool}
onCheckedChange={(c) => {
if (c === 'indeterminate') return;
onToggleIntegration?.(item, c === true);
}}
onClick={(e) => e.stopPropagation()}
aria-label={item.name}
/>
)}
<div className={titleClassName}>{item.name}</div>
<div className="flex items-center">
<TooltipSimple content={item.desc}>
<CircleAlert className="h-4 w-4 text-ds-icon-neutral-muted-default" />
</TooltipSimple>
</div>
<span className={titleClassName}>{item.name}</span>
</div>
) : (
<div className="gap-xs flex w-full flex-row items-center justify-between">
@ -433,7 +452,7 @@ export default function IntegrationList({
</div>
</div>
)}
{isSelectMode && item.env_vars.length !== 0 && (
{isSelectMode && envCount !== 0 && (
<Button
disabled={isComingSoon}
variant={isInstalled ? 'secondary' : 'primary'}

View file

@ -49,6 +49,8 @@ export interface NavListSessionRowsProps {
* neutral hover fill; sidebar keeps the subtle fill.
*/
panelListHover?: boolean;
/** When false, hide the trailing ⋯ row menu (workspace recent: time only). */
showRowMenu?: boolean;
}
/**
@ -65,6 +67,7 @@ export function NavListSessionRows({
folded,
maxItems,
panelListHover = false,
showRowMenu = true,
}: NavListSessionRowsProps) {
const { t } = useTranslation();
const deleteLabel = t('layout.sessions-delete-session', {
@ -115,7 +118,8 @@ export function NavListSessionRows({
<div key={session.id} className="min-w-0">
<div
className={cn(
'group/session-item min-w-0 h-8 gap-1 rounded-xl pl-3 pr-1 relative flex w-full items-center overflow-hidden',
'group/session-item min-w-0 h-8 gap-1 rounded-xl pl-3 relative flex w-full items-center overflow-hidden',
showRowMenu ? 'pr-1' : 'pr-3',
'transition-colors duration-150',
active
? panelListHover
@ -148,58 +152,62 @@ export function NavListSessionRows({
) : null}
</button>
<span
className={cn(
'top-0 bottom-0 right-7 w-14 pointer-events-none absolute z-[1] bg-gradient-to-l to-transparent',
active
? panelListHover
? 'from-ds-bg-neutral-muted-default group-hover/session-item:from-ds-bg-neutral-default-default'
: 'from-ds-bg-neutral-subtle-default'
: [
'from-transparent',
panelListHover
? 'group-hover/session-item:from-ds-bg-neutral-default-default'
: 'group-hover/session-item:from-ds-bg-neutral-subtle-default',
]
)}
aria-hidden
/>
{showRowMenu ? (
<>
<span
className={cn(
'top-0 bottom-0 right-7 w-14 pointer-events-none absolute z-[1] bg-gradient-to-l to-transparent',
active
? panelListHover
? 'from-ds-bg-neutral-muted-default group-hover/session-item:from-ds-bg-neutral-default-default'
: 'from-ds-bg-neutral-subtle-default'
: [
'from-transparent',
panelListHover
? 'group-hover/session-item:from-ds-bg-neutral-default-default'
: 'group-hover/session-item:from-ds-bg-neutral-subtle-default',
]
)}
aria-hidden
/>
<div className="relative z-[2] shrink-0">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type="button"
className={cn(
'no-drag h-7 w-7 rounded-lg text-ds-icon-neutral-muted-default flex shrink-0 items-center justify-center transition-opacity duration-150 outline-none',
active
? panelListHover
? 'bg-transparent opacity-100'
: 'bg-ds-bg-neutral-subtle-default hover:bg-ds-bg-neutral-subtle-default opacity-100'
: [
'md:opacity-0 bg-transparent opacity-100',
'md:group-hover/session-item:opacity-100',
'md:group-focus-within/session-item:opacity-100',
'data-[state=open]:opacity-100',
],
'focus-visible:ring-ds-ring-neutral-subtle-default focus-visible:ring-2 focus-visible:outline-none'
)}
aria-label={sessionMenuAria}
onClick={(e) => e.stopPropagation()}
>
<MoreHorizontal className="h-4 w-4" aria-hidden />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="min-w-[9rem]">
<DropdownMenuItem
className="text-ds-text-error-default-default focus:text-ds-text-error-default-default cursor-pointer"
onClick={() => onDeleteSession?.(session.id)}
>
{deleteLabel}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="relative z-[2] shrink-0">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button
type="button"
className={cn(
'no-drag h-7 w-7 rounded-lg text-ds-icon-neutral-muted-default flex shrink-0 items-center justify-center transition-opacity duration-150 outline-none',
active
? panelListHover
? 'bg-transparent opacity-100'
: 'bg-ds-bg-neutral-subtle-default hover:bg-ds-bg-neutral-subtle-default opacity-100'
: [
'md:opacity-0 bg-transparent opacity-100',
'md:group-hover/session-item:opacity-100',
'md:group-focus-within/session-item:opacity-100',
'data-[state=open]:opacity-100',
],
'focus-visible:ring-ds-ring-neutral-subtle-default focus-visible:ring-2 focus-visible:outline-none'
)}
aria-label={sessionMenuAria}
onClick={(e) => e.stopPropagation()}
>
<MoreHorizontal className="h-4 w-4" aria-hidden />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="min-w-[9rem]">
<DropdownMenuItem
className="text-ds-text-error-default-default focus:text-ds-text-error-default-default cursor-pointer"
onClick={() => onDeleteSession?.(session.id)}
>
{deleteLabel}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</>
) : null}
</div>
</div>
);

View file

@ -184,7 +184,7 @@ function HeaderWin() {
return (
<div
className={`drag left-0 right-0 top-0 !h-9 py-1 absolute z-50 flex items-center justify-between ${
className={`drag left-0 right-0 top-0 !h-10 py-1 absolute z-50 flex items-center justify-between ${
platform === 'darwin' ? 'pr-[2px] pl-[68px]' : 'pl-2'
}`}
id="titlebar"
@ -195,14 +195,14 @@ function HeaderWin() {
src={appearance === 'dark' ? eigentAppIconWhite : eigentAppIconBlack}
alt="Eigent"
className="h-6 w-6 mt-[2px] select-none"
width={12}
height={12}
width={16}
height={16}
draggable={false}
/>
</div>
<div className="drag min-w-0 flex h-full flex-1 items-center justify-between">
<div className="relative z-50 flex h-full items-center">
<div className="no-drag gap-2 pl-2 flex items-center">
<div className="no-drag gap-2 pl-1 flex items-center">
<div className="flex items-center">
{isHistoryRoute ? (
<TooltipSimple
@ -212,16 +212,14 @@ function HeaderWin() {
>
<Button
variant="ghost"
size="icon"
size="sm"
buttonContent="icon-only"
className="no-drag rounded-full"
onClick={() => navigate('/')}
aria-label={t('layout.projects')}
aria-current="page"
>
<FolderOpen
className="h-4 w-4 text-ds-icon-neutral-default-default"
aria-hidden
/>
<FolderOpen aria-hidden />
</Button>
</TooltipSimple>
) : (
@ -232,16 +230,14 @@ function HeaderWin() {
>
<Button
variant="ghost"
size="icon"
size="sm"
buttonContent="icon-only"
className="no-drag rounded-full"
onClick={() => navigate('/history')}
aria-label={t('layout.dashboard')}
aria-current="page"
>
<House
className="h-4 w-4 text-ds-icon-neutral-default-default"
aria-hidden
/>
<House aria-hidden />
</Button>
</TooltipSimple>
)}
@ -252,16 +248,14 @@ function HeaderWin() {
>
<Button
variant="ghost"
size="icon"
size="sm"
buttonContent="icon-only"
className="no-drag rounded-full"
disabled={!canGoBack}
onClick={() => navigate(-1)}
aria-label={t('layout.back')}
>
<ChevronLeft
className="h-4 w-4 text-ds-icon-neutral-default-default"
aria-hidden
/>
<ChevronLeft aria-hidden />
</Button>
</TooltipSimple>
<TooltipSimple
@ -271,16 +265,14 @@ function HeaderWin() {
>
<Button
variant="ghost"
size="icon"
size="sm"
buttonContent="icon-only"
className="no-drag rounded-full"
disabled={!canGoForward}
onClick={() => navigate(1)}
aria-label={t('layout.forward')}
>
<ChevronRight
className="h-4 w-4 text-ds-icon-neutral-default-default"
aria-hidden
/>
<ChevronRight aria-hidden />
</Button>
</TooltipSimple>
</div>
@ -335,9 +327,11 @@ function HeaderWin() {
onClick={() =>
handleShare(chatStore.activeTaskId as string)
}
variant="ghost"
size="icon"
className="no-drag bg-ds-bg-information-subtle-default !text-ds-text-information-strong-default rounded-full"
variant="secondary"
size="sm"
buttonContent="icon-only"
buttonRadius="full"
tone="information"
aria-label={t('layout.share')}
>
<Share aria-hidden />
@ -352,14 +346,15 @@ function HeaderWin() {
<Button
type="button"
variant="ghost"
size="icon"
size="sm"
className="no-drag rounded-full"
aria-label={t('layout.notifications')}
aria-expanded={notificationPanelOpen}
aria-controls="notification-panel"
onClick={() => setNotificationPanelOpen((open) => !open)}
buttonContent="icon-only"
>
<Bell className="h-4 w-4" aria-hidden />
<Bell aria-hidden />
</Button>
</TooltipSimple>
<TooltipSimple
@ -370,12 +365,13 @@ function HeaderWin() {
<Button
type="button"
variant="ghost"
size="icon"
size="sm"
className="no-drag rounded-full"
aria-label={t('layout.support')}
onClick={() => setReportBugOpen(true)}
buttonContent="icon-only"
>
<CircleHelp className="h-4 w-4" aria-hidden />
<CircleHelp aria-hidden />
</Button>
</TooltipSimple>
<TooltipSimple
@ -386,13 +382,15 @@ function HeaderWin() {
<Button
onClick={getReferFriendsLink}
variant="ghost"
size="icon"
size="sm"
className="no-drag rounded-full"
buttonContent="icon-only"
>
<img
src={appearance === 'dark' ? giftWhiteIcon : giftIcon}
alt="gift-icon"
className="h-4 w-4"
width={16}
height={16}
/>
</Button>
</TooltipSimple>
@ -404,10 +402,11 @@ function HeaderWin() {
<Button
onClick={() => navigate('/history?tab=settings')}
variant="ghost"
size="icon"
buttonContent="icon-only"
size="sm"
className="no-drag rounded-full"
>
<Settings className="h-4 w-4" />
<Settings aria-hidden />
</Button>
</TooltipSimple>
</>

View file

@ -62,7 +62,6 @@ export interface WorkspaceRecentSessionsProps {
activeTaskId: string | null;
onSelectSession: (sessionId: string) => void;
onOpenAllSessions: () => void;
onDeleteSession: (sessionId: string) => void;
}
/**
@ -74,7 +73,6 @@ export function WorkspaceRecentSessions({
activeTaskId,
onSelectSession,
onOpenAllSessions,
onDeleteSession,
}: WorkspaceRecentSessionsProps) {
const { t } = useTranslation();
@ -135,10 +133,10 @@ export function WorkspaceRecentSessions({
sessions={sessions}
activeSessionId={activeTaskId}
onSessionClick={onSelectSession}
onDeleteSession={onDeleteSession}
folded={false}
maxItems={NAV_LIST_SESSIONS_RECENT_MAX}
panelListHover
showRowMenu={false}
/>
</div>
</div>

View file

@ -87,19 +87,6 @@ export default function Workspace() {
useState(false);
const textareaRef = useRef<HTMLDivElement>(null);
const handleWorkspaceRecentDeleteSession = useCallback(
(sessionId: string) => {
if (!chatStore) return;
if (!window.confirm(t('layout.delete-task-confirmation'))) return;
const wasActive = chatStore.activeTaskId === sessionId;
chatStore.removeTask(sessionId);
if (wasActive) {
setActiveWorkspaceTab('workforce');
}
},
[chatStore, setActiveWorkspaceTab, t]
);
useEffect(() => {
if (!workspaceWorkWithPanelOpen) return;
const onKeyDown = (e: KeyboardEvent) => {
@ -427,7 +414,6 @@ export default function Workspace() {
setActiveWorkspaceTab('session');
}}
onOpenAllSessions={() => setActiveWorkspaceTab('sessions')}
onDeleteSession={handleWorkspaceRecentDeleteSession}
/>
)}
</div>