Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 83 additions & 21 deletions src/web-ui/src/app/components/NavPanel/MainNav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,18 @@
* MainNav — default workspace navigation sidebar.
*
* Layout (top to bottom):
* 1. Top: New sessions | Assistant | Extensions (expand → Agents | Skills)
* 2. Assistant sessions, Workspace
* 3. Bottom: MiniApp
* 1. Workspace file search
* 2. Top: New sessions | Assistant | Extensions (expand → Agents | Skills)
* 3. Assistant sessions, Workspace
* 4. Bottom: MiniApp
*
* When a scene-nav transition is active (`isDeparting=true`), items receive
* positional CSS classes for the split-open animation effect.
*/

import React, { useCallback, useState, useMemo, useEffect, useRef } from 'react';
import { createPortal } from 'react-dom';
import { Plus, FolderOpen, FolderPlus, History, Check, BotMessageSquare, Users, Puzzle, Blocks, ChevronDown } from 'lucide-react';
import { Plus, FolderOpen, FolderPlus, History, Check, BotMessageSquare, Users, Puzzle, Blocks, ChevronDown, Search } from 'lucide-react';
import { Tooltip } from '@/component-library';
import { useApp } from '../../hooks/useApp';
import { useSceneManager } from '../../hooks/useSceneManager';
Expand All @@ -30,9 +31,16 @@ import { flowChatManager } from '@/flow_chat/services/FlowChatManager';
import { workspaceManager } from '@/infrastructure/services/business/workspaceManager';
import { useWorkspaceContext } from '@/infrastructure/contexts/WorkspaceContext';
import { createLogger } from '@/shared/utils/logger';
import { notificationService } from '@/shared/notification-system';
import { WorkspaceKind, isRemoteWorkspace } from '@/shared/types';
import {
findReusableEmptySessionId,
flowChatSessionConfigForWorkspace,
pickWorkspaceForProjectChatSession,
} from '@/app/utils/projectSessionWorkspace';
import { useSSHRemoteContext, SSHConnectionDialog, RemoteFileBrowser } from '@/features/ssh-remote';
import { useSessionModeStore } from '../../stores/sessionModeStore';
import NavSearchDialog from './NavSearchDialog';

import './NavPanel.scss';

Expand Down Expand Up @@ -68,6 +76,7 @@ const MainNav: React.FC<MainNavProps> = ({
recentWorkspaces,
openedWorkspacesList,
assistantWorkspacesList,
normalWorkspacesList,
switchWorkspace,
setActiveWorkspace,
} = useWorkspaceContext();
Expand All @@ -88,6 +97,7 @@ const MainNav: React.FC<MainNavProps> = ({
const [workspaceMenuClosing, setWorkspaceMenuClosing] = useState(false);
const [workspaceMenuPos, setWorkspaceMenuPos] = useState({ top: 0, left: 0 });
const [isExtensionsOpen, setIsExtensionsOpen] = useState(false);
const [searchOpen, setSearchOpen] = useState(false);

const toggleSection = useCallback((id: string) => {
setExpandedSections(prev => {
Expand Down Expand Up @@ -137,28 +147,59 @@ const MainNav: React.FC<MainNavProps> = ({
});
}, [openedWorkspacesList]);

const handleCreateSession = useCallback(async (mode?: 'agentic' | 'Cowork' | 'Claw') => {
openScene('session');
switchLeftPanelTab('sessions');
try {
await flowChatManager.createChatSession(
{},
mode ?? (isAssistantWorkspaceActive ? 'Claw' : 'agentic')
);
} catch (err) {
log.error('Failed to create session', err);
}
}, [openScene, switchLeftPanelTab, isAssistantWorkspaceActive]);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
e.preventDefault();
setSearchOpen(v => !v);
}
};
document.addEventListener('keydown', handleKeyDown);
return () => document.removeEventListener('keydown', handleKeyDown);
}, []);

const handleCreateProjectSession = useCallback(
async (mode: 'agentic' | 'Cowork') => {
const target = pickWorkspaceForProjectChatSession(currentWorkspace, normalWorkspacesList);
if (!target) {
notificationService.warning(t('nav.sessions.needProjectWorkspaceForSession'), { duration: 4500 });
return;
}
openScene('session');
switchLeftPanelTab('sessions');
try {
if (target.id !== currentWorkspace?.id) {
await setActiveWorkspace(target.id);
}
const reusableId = findReusableEmptySessionId(target, mode);
if (reusableId) {
await flowChatManager.switchChatSession(reusableId);
return;
}
await flowChatManager.createChatSession(flowChatSessionConfigForWorkspace(target), mode);
} catch (err) {
log.error('Failed to create session', err);
}
},
[
currentWorkspace,
normalWorkspacesList,
openScene,
setActiveWorkspace,
switchLeftPanelTab,
t,
]
);

const handleCreateCodeSession = useCallback(() => {
setSessionMode('code');
void handleCreateSession('agentic');
}, [handleCreateSession, setSessionMode]);
void handleCreateProjectSession('agentic');
}, [handleCreateProjectSession, setSessionMode]);

const handleCreateCoworkSession = useCallback(() => {
setSessionMode('cowork');
void handleCreateSession('Cowork');
}, [handleCreateSession, setSessionMode]);
void handleCreateProjectSession('Cowork');
}, [handleCreateProjectSession, setSessionMode]);

const handleOpenProject = useCallback(async () => {
try {
Expand Down Expand Up @@ -338,6 +379,27 @@ const MainNav: React.FC<MainNavProps> = ({
const extensionsLabel = t('nav.sections.extensions');
return (
<>
{/* ── Workspace search ───────────────────────── */}
<div className="bitfun-nav-panel__brand-header">
<div className="bitfun-nav-panel__brand-search">
<Tooltip content={t('nav.search.triggerTooltip')} placement="right" followCursor>
<button
type="button"
className="bitfun-nav-panel__search-trigger"
onClick={() => setSearchOpen(true)}
aria-label={t('nav.search.triggerTooltip')}
>
<Search size={13} />
<span className="bitfun-nav-panel__search-trigger__label">
{t('nav.search.triggerPlaceholder')}
</span>
<span className="bitfun-nav-panel__search-trigger__kbd">⌘K</span>
</button>
</Tooltip>
<NavSearchDialog open={searchOpen} onClose={() => setSearchOpen(false)} />
</div>
</div>

{/* ── Top action strip ────────────────────────── */}
<div className="bitfun-nav-panel__top-actions">
<Tooltip content={createCodeTooltip} placement="right" followCursor>
Expand Down Expand Up @@ -462,7 +524,7 @@ const MainNav: React.FC<MainNavProps> = ({
/>
<div className={`bitfun-nav-panel__collapsible${expandedSections.has('assistant-sessions') ? '' : ' is-collapsed'}`}>
<div className="bitfun-nav-panel__collapsible-inner">
<div className="bitfun-nav-panel__items">
<div className="bitfun-nav-panel__items bitfun-nav-panel__items--session-blocks">
{assistantWorkspacesList.map(workspace => {
const assistantDisplayName =
workspace.workspaceKind === WorkspaceKind.Assistant
Expand Down
92 changes: 86 additions & 6 deletions src/web-ui/src/app/components/NavPanel/NavPanel.scss
Original file line number Diff line number Diff line change
Expand Up @@ -520,7 +520,9 @@ $_section-header-height: 24px;
flex: 1 1 auto;
overflow-y: auto;
overflow-x: hidden;
padding: $size-gap-5 0 $size-gap-3 0;
// No padding-top: separation from top-actions is only .bitfun-nav-panel__top-actions padding-bottom.
// Top padding here stacked with that and read as “extra” gap above the first section header.
padding: 0 0 $size-gap-3 0;

&::-webkit-scrollbar { width: 3px; }
&::-webkit-scrollbar-track { background: transparent; }
Expand Down Expand Up @@ -994,6 +996,11 @@ $_section-header-height: 24px;
flex-direction: column;
padding: 2px $size-gap-2;
gap: 2px;

// Multiple SessionsSection siblings: match vertical rhythm of __workspace-list (gap 0 + 2px row padding).
&--session-blocks {
gap: 0;
}
}

button.bitfun-nav-panel__inline-item {
Expand All @@ -1020,10 +1027,14 @@ $_section-header-height: 24px;
// Item slots depart with the split-open animation used by scene navs.

&__section {
margin-bottom: $size-gap-5;
margin-bottom: $size-gap-5 * 0.75;
transition: transform $_depart-duration $_depart-easing,
opacity $_depart-duration $_depart-easing;

&:last-child {
margin-bottom: $size-gap-5;
}

&.is-departing-up {
transform: translateY(-$_depart-distance);
opacity: 0;
Expand Down Expand Up @@ -1703,6 +1714,51 @@ $_section-header-height: 24px;
filter: none;
}
}

.bitfun-nav-panel__top-action-icon-circle {
transition: none;
}

.bitfun-nav-panel__top-action-btn:hover .bitfun-nav-panel__top-action-icon-circle {
transform: scale(1);
}

.bitfun-nav-panel__top-action-btn:not(.bitfun-nav-panel__top-action-btn--sub) .bitfun-nav-panel__top-action-icon-slot {
transition: none;
}

.bitfun-nav-panel__top-action-btn:not(.bitfun-nav-panel__top-action-btn--sub):hover .bitfun-nav-panel__top-action-icon-slot {
transform: scale(1);
}
}

// ── Nav workspace search ─────────────────────────

.bitfun-nav-panel__brand-header {
display: flex;
flex-direction: column;
padding: $size-gap-2 $size-gap-2 $size-gap-1;
flex-shrink: 0;
}

.bitfun-nav-panel__brand-search {
width: 100%;
min-width: 0;

// Legacy GlobalSearch overrides kept for reference but no longer rendered.
// Trigger button styles live in NavSearchDialog.scss.

.bitfun-global-search--nav-panel .bitfun-global-search__option {
width: 22px;
height: 22px;
min-width: 22px;
padding: 0;

svg {
width: 11px;
height: 11px;
}
}
}

// ── Top action strip ─────────────────────────────
Expand All @@ -1711,14 +1767,18 @@ $_section-header-height: 24px;
display: flex;
flex-direction: column;
gap: $size-gap-1;
padding: $size-gap-2 $size-gap-2 $size-gap-4;
padding: $size-gap-2 $size-gap-2 $size-gap-4 * 0.75;
flex-shrink: 0;
}

.bitfun-nav-panel__top-action-expand {
display: flex;
flex-direction: column;
gap: 1px;
// Divider below 扩展 (main row + optional Agents / Skills sublist) → scrollable sections
padding-bottom: $size-gap-2;
margin-bottom: $size-gap-1;
border-bottom: 1px solid var(--border-subtle);
}

.bitfun-nav-panel__top-action-sublist {
Expand Down Expand Up @@ -1777,6 +1837,17 @@ $_section-header-height: 24px;
justify-content: center;
}

// 助理行:前导 icon 悬停微微放大(与 __top-action-icon-circle 一致;排除 --sub 的 Agents / Skills)
&:not(.bitfun-nav-panel__top-action-btn--sub) .bitfun-nav-panel__top-action-icon-slot {
transform: scale(1);
transform-origin: center;
transition: transform $motion-fast $easing-standard;
}

&:not(.bitfun-nav-panel__top-action-btn--sub):hover .bitfun-nav-panel__top-action-icon-slot {
transform: scale(1.07);
}

&:hover {
background: var(--element-bg-soft);
color: var(--color-text-primary);
Expand All @@ -1795,7 +1866,7 @@ $_section-header-height: 24px;
}

&--sub {
padding-left: 28px;
padding-left: 18px;
}
}

Expand Down Expand Up @@ -1851,9 +1922,18 @@ $_section-header-height: 24px;
justify-content: center;
border-radius: 50%;
background: var(--element-bg-medium);
transition: background $motion-fast $easing-standard;
color: var(--color-text-muted);
transform: scale(1);
transform-origin: center;
transition: background $motion-fast $easing-standard,
color $motion-fast $easing-standard,
box-shadow $motion-fast $easing-standard,
transform $motion-fast $easing-standard;

.bitfun-nav-panel__top-action-btn:hover & {
background: color-mix(in srgb, var(--color-primary) 18%, var(--element-bg-medium));
background: var(--element-bg-strong);
color: var(--color-primary);
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--color-primary) 28%, transparent);
transform: scale(1.07);
}
}
Loading
Loading