diff --git a/src/web-ui/public/panda_1.png b/src/web-ui/public/panda_1.png index 3ef50c28..ba747906 100644 Binary files a/src/web-ui/public/panda_1.png and b/src/web-ui/public/panda_1.png differ diff --git a/src/web-ui/public/panda_2.png b/src/web-ui/public/panda_2.png index d78a2865..ec03889c 100644 Binary files a/src/web-ui/public/panda_2.png and b/src/web-ui/public/panda_2.png differ diff --git a/src/web-ui/public/panda_full_1.png b/src/web-ui/public/panda_full_1.png index d718c67e..82282c11 100644 Binary files a/src/web-ui/public/panda_full_1.png and b/src/web-ui/public/panda_full_1.png differ diff --git a/src/web-ui/public/panda_full_2.png b/src/web-ui/public/panda_full_2.png index 450940b1..67c21051 100644 Binary files a/src/web-ui/public/panda_full_2.png and b/src/web-ui/public/panda_full_2.png differ diff --git a/src/web-ui/src/app/components/NavPanel/MainNav.tsx b/src/web-ui/src/app/components/NavPanel/MainNav.tsx index 02264d5d..82e32054 100644 --- a/src/web-ui/src/app/components/NavPanel/MainNav.tsx +++ b/src/web-ui/src/app/components/NavPanel/MainNav.tsx @@ -13,7 +13,7 @@ import React, { useCallback, useState, useMemo, useEffect, useRef } from 'react'; import { createPortal } from 'react-dom'; -import { Plus, FolderOpen, FolderPlus, History, Check, Bot, Code2, Users } from 'lucide-react'; +import { Plus, FolderOpen, FolderPlus, History, Check, Bot } from 'lucide-react'; import { Tooltip } from '@/component-library'; import { useApp } from '../../hooks/useApp'; import { useSceneManager } from '../../hooks/useSceneManager'; @@ -32,6 +32,7 @@ import { useSceneStore } from '../../stores/sceneStore'; import { useMyAgentStore } from '../../scenes/my-agent/myAgentStore'; import { useMiniAppCatalogSync } from '../../scenes/miniapps/hooks/useMiniAppCatalogSync'; import { flowChatStore } from '@/flow_chat/store/FlowChatStore'; +import { compareSessionsForDisplay } from '@/flow_chat/utils/sessionOrdering'; import { flowChatManager } from '@/flow_chat/services/FlowChatManager'; import { configManager } from '@/infrastructure/config/services/ConfigManager'; import { workspaceManager } from '@/infrastructure/services/business/workspaceManager'; @@ -40,11 +41,13 @@ import { createLogger } from '@/shared/utils/logger'; import { WorkspaceKind } from '@/shared/types'; const DEFAULT_MODE_CONFIG_KEY = 'app.session_config.default_mode'; +const NAV_DISPLAY_MODE_STORAGE_KEY = 'bitfun.nav.displayMode'; import './NavPanel.scss'; const log = createLogger('MainNav'); type DepartDir = 'up' | 'anchor' | 'down' | null; +type NavDisplayMode = 'pro' | 'assistant'; /** * Build a flat ordered list of (sectionId, itemTab) tuples so we can @@ -67,6 +70,13 @@ function getAnchorIndex(anchorId: SceneTabId | null): number { return FLAT_ITEMS.findIndex(i => i.navSceneId === anchorId); } +function getInitialNavDisplayMode(): NavDisplayMode { + if (typeof window === 'undefined') return 'pro'; + return window.localStorage.getItem(NAV_DISPLAY_MODE_STORAGE_KEY) === 'assistant' + ? 'assistant' + : 'pro'; +} + interface MainNavProps { isDeparting?: boolean; anchorNavSceneId?: SceneTabId | null; @@ -138,11 +148,6 @@ const MainNav: React.FC = ({ const [workspaceMenuClosing, setWorkspaceMenuClosing] = useState(false); const [workspaceMenuPos, setWorkspaceMenuPos] = useState({ top: 0, left: 0 }); - const modeDropdownButtonRef = useRef(null); - const modeDropdownRef = useRef(null); - const [modeDropdownOpen, setModeDropdownOpen] = useState(false); - const [modeDropdownClosing, setModeDropdownClosing] = useState(false); - const [modeDropdownPos, setModeDropdownPos] = useState({ top: 0, left: 0 }); const getSectionLabel = useCallback( (sectionId: string, fallbackLabel: string | null) => { @@ -208,18 +213,39 @@ const MainNav: React.FC = ({ ); const [defaultSessionMode, setDefaultSessionMode] = useState<'code' | 'cowork'>('code'); + const [navDisplayMode, setNavDisplayMode] = useState(getInitialNavDisplayMode); + const [isModeSwitching, setIsModeSwitching] = useState(false); + const [modeLogoSrc, setModeLogoSrc] = useState('/panda_1.png'); + const [modeLogoHoverSrc, setModeLogoHoverSrc] = useState('/panda_2.png'); + const modeSwitchTimerRef = useRef(null); + const modeSwitchSwapTimerRef = useRef(null); useEffect(() => { configManager.getConfig<'code' | 'cowork'>(DEFAULT_MODE_CONFIG_KEY).then(mode => { - if (mode === 'code' || mode === 'cowork') setDefaultSessionMode(mode); + if (mode === 'code' || mode === 'cowork') { + setDefaultSessionMode(mode); + setSessionMode(mode); + } }).catch(() => {}); const unwatch = configManager.watch(DEFAULT_MODE_CONFIG_KEY, () => { configManager.getConfig<'code' | 'cowork'>(DEFAULT_MODE_CONFIG_KEY).then(mode => { - if (mode === 'code' || mode === 'cowork') setDefaultSessionMode(mode); + if (mode === 'code' || mode === 'cowork') { + setDefaultSessionMode(mode); + setSessionMode(mode); + } }).catch(() => {}); }); return () => unwatch(); + }, [setSessionMode]); + + useEffect(() => () => { + if (modeSwitchTimerRef.current !== null) { + window.clearTimeout(modeSwitchTimerRef.current); + } + if (modeSwitchSwapTimerRef.current !== null) { + window.clearTimeout(modeSwitchSwapTimerRef.current); + } }, []); useEffect(() => { @@ -228,52 +254,16 @@ const MainNav: React.FC = ({ }); }, [openedWorkspacesList]); - const closeModeDropdown = useCallback(() => { - setModeDropdownClosing(true); - window.setTimeout(() => { - setModeDropdownOpen(false); - setModeDropdownClosing(false); - }, 150); - }, []); - const openModeDropdown = useCallback(() => { - const rect = modeDropdownButtonRef.current?.getBoundingClientRect(); - if (!rect) return; - setModeDropdownPos({ - top: rect.bottom + 4, - left: rect.left, - }); - setModeDropdownOpen(true); - setModeDropdownClosing(false); - }, []); - const toggleModeDropdown = useCallback(() => { - if (modeDropdownOpen) { - closeModeDropdown(); - return; - } - openModeDropdown(); - }, [closeModeDropdown, openModeDropdown, modeDropdownOpen]); - - const handleSetDefaultMode = useCallback(async (mode: 'code' | 'cowork') => { - closeModeDropdown(); - setDefaultSessionMode(mode); - setSessionMode(mode); - try { - await configManager.setConfig(DEFAULT_MODE_CONFIG_KEY, mode); - } catch (err) { - log.error('Failed to save default session mode', err); - } - }, [closeModeDropdown, setSessionMode]); const handleCreateAssistantWorkspace = useCallback(async () => { - closeModeDropdown(); try { await workspaceManager.createAssistantWorkspace(); } catch (err) { log.error('Failed to create assistant workspace', err); } - }, [closeModeDropdown]); + }, []); const handleItemClick = useCallback( (tab: PanelType, item: NavItemConfig) => { @@ -289,23 +279,58 @@ const MainNav: React.FC = ({ [switchLeftPanelTab, openScene, openNavScene] ); - const handleCreateSession = useCallback(async () => { + const handleCreateSession = useCallback(async (mode?: 'agentic' | 'Cowork' | 'Claw') => { openScene('session'); switchLeftPanelTab('sessions'); try { await flowChatManager.createChatSession( {}, - isAssistantWorkspaceActive + mode ?? ( + isAssistantWorkspaceActive ? 'Claw' : defaultSessionMode === 'cowork' ? 'Cowork' : 'agentic' + ) ); } catch (err) { log.error('Failed to create session', err); } }, [openScene, switchLeftPanelTab, defaultSessionMode, isAssistantWorkspaceActive]); + const handleCreateCodeSession = useCallback(() => { + setSessionMode('code'); + void handleCreateSession('agentic'); + }, [handleCreateSession, setSessionMode]); + + const handleCreateCoworkSession = useCallback(() => { + setSessionMode('cowork'); + void handleCreateSession('Cowork'); + }, [handleCreateSession, setSessionMode]); + + const handleCreateAssistantSession = useCallback(async () => { + const targetAssistantWorkspace = + isAssistantWorkspaceActive && currentWorkspace?.workspaceKind === WorkspaceKind.Assistant + ? currentWorkspace + : defaultAssistantWorkspace; + + if (targetAssistantWorkspace && !isAssistantWorkspaceActive) { + try { + await setActiveWorkspace(targetAssistantWorkspace.id); + } catch (error) { + log.warn('Failed to activate assistant workspace before creating session', { error }); + } + } + + await handleCreateSession('Claw'); + }, [ + currentWorkspace, + defaultAssistantWorkspace, + handleCreateSession, + isAssistantWorkspaceActive, + setActiveWorkspace, + ]); + const handleOpenProject = useCallback(async () => { try { const { open } = await import('@tauri-apps/plugin-dialog'); @@ -358,28 +383,6 @@ const MainNav: React.FC = ({ }; }, [closeWorkspaceMenu, workspaceMenuOpen]); - useEffect(() => { - if (!modeDropdownOpen) return; - - const handleClickOutside = (event: MouseEvent) => { - const target = event.target as Node | null; - if (!target) return; - if (modeDropdownButtonRef.current?.contains(target)) return; - if (modeDropdownRef.current?.contains(target)) return; - closeModeDropdown(); - }; - - const handleEscape = (event: KeyboardEvent) => { - if (event.key === 'Escape') closeModeDropdown(); - }; - - document.addEventListener('mousedown', handleClickOutside); - document.addEventListener('keydown', handleEscape); - return () => { - document.removeEventListener('mousedown', handleClickOutside); - document.removeEventListener('keydown', handleEscape); - }; - }, [closeModeDropdown, modeDropdownOpen]); const handleOpenProfile = useCallback(() => { const targetAssistantWorkspace = @@ -410,6 +413,119 @@ const MainNav: React.FC = ({ switchLeftPanelTab, ]); + const handleOpenProModeSession = useCallback(async () => { + // 找到项目工作区(非 assistant 类型) + const projectWorkspaces = openedWorkspacesList.filter( + w => w.workspaceKind !== WorkspaceKind.Assistant + ); + + const targetWorkspace = + currentWorkspace?.workspaceKind !== WorkspaceKind.Assistant + ? currentWorkspace + : projectWorkspaces[0] ?? null; + + // 若当前激活的是 assistant workspace,先切回项目工作区 + if (targetWorkspace && currentWorkspace?.id !== targetWorkspace.id) { + await setActiveWorkspace(targetWorkspace.id).catch(() => {}); + } + + const workspacePath = targetWorkspace?.rootPath; + const state = flowChatStore.getState(); + + if (workspacePath) { + const workspaceSessions = Array.from(state.sessions.values()) + .filter(s => + (s.workspacePath || workspacePath) === workspacePath && + !s.parentSessionId + ) + .sort(compareSessionsForDisplay); + + if (workspaceSessions.length > 0) { + const firstSession = workspaceSessions[0]; + if (firstSession.isHistorical) { + await flowChatStore.loadSessionHistory(firstSession.sessionId, workspacePath); + } + flowChatStore.switchSession(firstSession.sessionId); + openScene('session'); + switchLeftPanelTab('sessions'); + return; + } + } + + // 没有已有会话,显式传入 workspacePath 创建 Code 会话,避免被 assistant workspace 覆盖 + openScene('session'); + switchLeftPanelTab('sessions'); + await flowChatManager.createChatSession({ workspacePath: workspacePath || undefined }, 'agentic'); + }, [currentWorkspace, openedWorkspacesList, openScene, setActiveWorkspace, switchLeftPanelTab]); + + const handleOpenAssistantModeSession = useCallback(async () => { + const targetAssistantWorkspace = + isAssistantWorkspaceActive && currentWorkspace?.workspaceKind === WorkspaceKind.Assistant + ? currentWorkspace + : defaultAssistantWorkspace; + + if (targetAssistantWorkspace && !isAssistantWorkspaceActive) { + await setActiveWorkspace(targetAssistantWorkspace.id).catch(() => {}); + } + + const workspacePath = targetAssistantWorkspace?.rootPath; + const state = flowChatStore.getState(); + + if (workspacePath) { + const workspaceSessions = Array.from(state.sessions.values()) + .filter(s => + (s.workspacePath || workspacePath) === workspacePath && + !s.parentSessionId + ) + .sort(compareSessionsForDisplay); + + if (workspaceSessions.length > 0) { + const firstSession = workspaceSessions[0]; + if (firstSession.isHistorical) { + await flowChatStore.loadSessionHistory(firstSession.sessionId, workspacePath); + } + flowChatStore.switchSession(firstSession.sessionId); + openScene('session'); + switchLeftPanelTab('sessions'); + return; + } + } + + // 没有已有会话,新建 Claw 会话 + await handleCreateSession('Claw'); + }, [currentWorkspace, defaultAssistantWorkspace, handleCreateSession, isAssistantWorkspaceActive, openScene, setActiveWorkspace, switchLeftPanelTab]); + + const handleToggleNavDisplayMode = useCallback(() => { + // 防止动画进行中重复触发 + if (modeSwitchTimerRef.current !== null) return; + + setIsModeSwitching(true); + + // 点击时同步计算目标模式,避免 timeout 闭包中读取到过期值 + const nextMode: NavDisplayMode = navDisplayMode === 'pro' ? 'assistant' : 'pro'; + + // 200ms(clip-path 收缩到最小圆点):只切换 nav 显示状态,不触发任何场景/会话操作 + if (modeSwitchSwapTimerRef.current !== null) { + window.clearTimeout(modeSwitchSwapTimerRef.current); + } + modeSwitchSwapTimerRef.current = window.setTimeout(() => { + setNavDisplayMode(nextMode); + window.localStorage.setItem(NAV_DISPLAY_MODE_STORAGE_KEY, nextMode); + modeSwitchSwapTimerRef.current = null; + }, 200); + + // 480ms(动画完全结束):再切场景和会话,避免 tab 文字在动画期间闪动 + modeSwitchTimerRef.current = window.setTimeout(() => { + setIsModeSwitching(false); + modeSwitchTimerRef.current = null; + if (nextMode === 'assistant') { + void handleOpenAssistantModeSession(); + } else { + void handleOpenProModeSession(); + } + }, 480); + }, [navDisplayMode, handleOpenAssistantModeSession, handleOpenProModeSession]); + let flatCounter = 0; const workspaceMenuPortal = workspaceMenuOpen ? createPortal( @@ -474,113 +590,123 @@ const MainNav: React.FC = ({ document.body ) : null; - const ModeIcon = isAssistantWorkspaceActive - ? Bot - : defaultSessionMode === 'cowork' - ? Users - : Code2; const personaTooltip = t('nav.items.persona'); - const createSessionTooltip = isAssistantWorkspaceActive - ? t('nav.sessions.newClawSession') - : defaultSessionMode === 'cowork' - ? t('nav.sessions.newCoworkSession') - : t('nav.sessions.newCodeSession'); - const createModeTooltip = isAssistantWorkspaceActive - ? t('nav.workspaces.actions.newAssistant') - : defaultSessionMode === 'cowork' - ? t('nav.sessions.newCoworkSessionDefault') - : t('nav.sessions.newCodeSessionDefault'); + const createSessionTooltip = t('nav.sessions.newClawSession'); const createAssistantTooltip = t('nav.workspaces.actions.newAssistant'); const openProjectTooltip = t('header.openProject'); + const createCodeTooltip = t('nav.sessions.newCodeSession'); + const createCoworkTooltip = t('nav.sessions.newCoworkSession'); + const isAssistantNavMode = navDisplayMode === 'assistant'; + const navModeLabel = isAssistantNavMode + ? t('nav.displayModes.assistant') + : t('nav.displayModes.pro'); + const navModeHint = isAssistantNavMode + ? t('nav.displayModes.switchToPro') + : t('nav.displayModes.switchToAssistant'); + const navModeDesc = isAssistantNavMode + ? t('nav.displayModes.assistantDesc') + : t('nav.displayModes.proDesc'); + const navSections = useMemo( + () => NAV_SECTIONS.filter(section => isAssistantNavMode ? section.id === 'assistants' : section.id === 'workspace'), + [isAssistantNavMode] + ); + const myAgentEntryLabel = t('nav.actions.openMyAgent'); - const modeDropdownPortal = modeDropdownOpen ? createPortal( -
- {isAssistantWorkspaceActive ? ( - - ) : ( - <> - - - - )} -
, - document.body - ) : null; return ( <>
- +
- - - - - - + {isAssistantNavMode ? ( + + + + ) : ( + <> + + + + + + + + )}
-
- {NAV_SECTIONS.map(section => { +
+ {navSections.map(section => { const isSectionOpen = expandedSections.has(section.id); const isCollapsible = !!section.collapsible; const showItems = !isCollapsible || isSectionOpen; @@ -679,6 +805,22 @@ const MainNav: React.FC = ({ })}
+ {isAssistantNavMode && ( +
+ + + +
+ )} +
= ({
{workspaceMenuPortal} - {modeDropdownPortal} ); }; diff --git a/src/web-ui/src/app/components/NavPanel/NavPanel.scss b/src/web-ui/src/app/components/NavPanel/NavPanel.scss index 9928d767..706e876b 100644 --- a/src/web-ui/src/app/components/NavPanel/NavPanel.scss +++ b/src/web-ui/src/app/components/NavPanel/NavPanel.scss @@ -115,35 +115,37 @@ $_section-header-height: 24px; &__workspace-toolbar { display: flex; - align-items: center; + flex-direction: column; + align-items: stretch; gap: $size-gap-2; - margin: 0 $size-gap-2 $size-gap-1; + margin: $size-gap-2 $size-gap-2 $size-gap-2; padding: 0 $size-gap-1; overflow: visible; } - &__workspace-bot { - width: 52px; - height: 52px; - padding: 0; - border: none; - border-radius: 50%; + &__mode-switch { + width: 100%; + min-height: 56px; + padding: 10px 12px; + border: 1px solid color-mix(in srgb, var(--color-primary) 16%, transparent); + border-radius: 12px; display: inline-flex; align-items: center; - justify-content: center; - flex-shrink: 0; - background: transparent; - color: var(--color-text-secondary); - position: relative; - z-index: 1; - overflow: hidden; + gap: 10px; + background: color-mix(in srgb, var(--element-bg-soft) 88%, transparent); + color: var(--color-text-primary); + text-align: left; cursor: pointer; transition: transform $motion-fast $easing-standard, - box-shadow $motion-fast $easing-standard; + box-shadow $motion-fast $easing-standard, + background $motion-fast $easing-standard, + border-color $motion-fast $easing-standard; &:hover { transform: translateY(-1px); - box-shadow: 0 0 0 1px color-mix(in srgb, var(--color-primary) 28%, transparent); + background: color-mix(in srgb, var(--element-bg-medium) 88%, transparent); + border-color: color-mix(in srgb, var(--color-primary) 34%, transparent); + box-shadow: 0 10px 24px rgba(0, 0, 0, 0.16); } &:focus-visible { @@ -151,30 +153,142 @@ $_section-header-height: 24px; outline-offset: 2px; } - img { - width: 100%; - height: 100%; - object-fit: cover; - display: block; - position: absolute; - top: 0; - left: 0; + &.is-assistant { + border-color: color-mix(in srgb, var(--color-accent-500) 30%, transparent); + background: color-mix(in srgb, var(--color-accent-500) 10%, var(--element-bg-soft)); } - &-logo { - z-index: 1; - object-fit: contain !important; - padding: 6px; + &.is-switching { + // 整体 clip-path 涟漪收缩 → 展开,linear 因为各段 easing 写在关键帧内部 + animation: bitfun-nav-mode-switch 0.48s linear; + + .bitfun-nav-panel__mode-switch-logo { + animation: bitfun-nav-mode-switch-logo 0.48s linear; + } + + .bitfun-nav-panel__mode-switch-copy { + animation: bitfun-nav-mode-switch-copy 0.48s linear; + } } } - &__workspace-create-group { + &__mode-switch-logo { + position: relative; + width: 48px; + height: 48px; + flex-shrink: 0; + border-radius: 50%; + overflow: hidden; + background: color-mix(in srgb, var(--element-bg-medium) 92%, transparent); + box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--border-subtle) 75%, transparent); + + .bitfun-nav-panel__mode-switch:not(.is-assistant) & { + border-radius: 0; + background: transparent; + box-shadow: none; + overflow: visible; + } + } + + &__mode-switch-logo-image { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + padding: 5px; + object-fit: contain; + display: block; + transition: opacity $motion-fast $easing-standard, + transform $motion-fast $easing-standard; + + &--hover { + opacity: 0; + transform: scale(0.92); + } + + &--static { + opacity: 1; + transform: none; + } + + .bitfun-nav-panel__mode-switch:hover &--default { + opacity: 0; + transform: scale(1.06); + } + + .bitfun-nav-panel__mode-switch:hover &--hover { + opacity: 1; + transform: scale(1); + } + } + + &__mode-switch-copy { + min-width: 0; + display: flex; + flex-direction: column; + gap: 2px; flex: 1; + } + + &__mode-switch-label { + font-size: 13px; + font-weight: 600; + line-height: 1.15; + color: var(--color-text-primary); + } + + &__mode-switch-sub { + display: grid; + grid-template-areas: 'sub'; + align-items: start; + } + + &__mode-switch-desc { + grid-area: sub; + display: flex; + flex-direction: column; + gap: 2px; + opacity: 1; + transition: opacity $motion-fast $easing-standard; + + .bitfun-nav-panel__mode-switch:hover & { + opacity: 0; + } + } + + &__mode-switch-desc-main { + font-size: 10px; + line-height: 1.35; + color: var(--color-text-muted); + opacity: 0.78; + } + + &__mode-switch-hint { + grid-area: sub; + align-self: center; + justify-self: start; + font-size: 10px; + line-height: 1.2; + color: var(--color-text-muted); + opacity: 0; + font-weight: 400; + white-space: nowrap; + transition: opacity $motion-fast $easing-standard; + + .bitfun-nav-panel__mode-switch:hover & { + opacity: 0.7; + } + } + + &__workspace-create-group { + width: 100%; display: flex; align-items: stretch; gap: 1px; height: 36px; min-width: 0; + border-radius: $size-radius-base; + overflow: hidden; } &__workspace-create-main { @@ -215,6 +329,32 @@ $_section-header-height: 24px; outline: 1px solid var(--color-accent-500); outline-offset: 1px; } + + &--split-left, + &--split-right { + border-radius: 0; + flex: 1 1 0; + min-width: 0; + } + + &--split-left { + border-top-left-radius: $size-radius-base; + border-bottom-left-radius: $size-radius-base; + } + + &--split-right { + border-top-right-radius: $size-radius-base; + border-bottom-right-radius: $size-radius-base; + color: color-mix(in srgb, var(--color-accent-500) 84%, var(--color-text-secondary)); + + &:hover { + color: var(--color-text-primary); + } + } + + &--single { + border-radius: $size-radius-base; + } } &__workspace-create-mode { @@ -338,6 +478,11 @@ $_section-header-height: 24px; background: var(--border-subtle); border-radius: 2px; } + + // 与按钮涟漪同步:列表内容淡出后换组,再淡入 + &.is-mode-switching { + animation: bitfun-nav-sections-mode-switch 0.48s linear; + } } // ────────────────────────────────────────────── @@ -406,6 +551,82 @@ $_section-header-height: 24px; padding: $size-gap-1 $size-gap-2 $size-gap-2; } + &__assistant-footer { + flex-shrink: 0; + padding: 0 $size-gap-2 $size-gap-1; + } + + &__assistant-entry { + width: 100%; + min-height: 38px; + display: inline-flex; + align-items: center; + gap: $size-gap-2; + padding: 0 12px; + border: 1px dashed color-mix(in srgb, var(--border-subtle) 90%, transparent); + border-radius: 10px; + background: transparent; + color: var(--color-text-secondary); + cursor: pointer; + transition: color $motion-fast $easing-standard, + background $motion-fast $easing-standard, + border-color $motion-fast $easing-standard, + transform $motion-fast $easing-standard; + + &:hover { + color: var(--color-text-primary); + background: color-mix(in srgb, var(--color-accent-500) 10%, transparent); + border-color: color-mix(in srgb, var(--color-accent-500) 36%, transparent); + } + + &:active { + transform: translateY(1px); + background: color-mix(in srgb, var(--color-accent-500) 14%, transparent); + } + + &:focus-visible { + outline: 2px solid var(--color-accent-500); + outline-offset: 0; + } + + &.is-active { + color: var(--color-text-primary); + background: color-mix(in srgb, var(--element-bg-soft) 88%, transparent); + border-color: color-mix(in srgb, var(--color-accent-500) 44%, transparent); + border-style: solid; + box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--color-accent-500) 18%, transparent); + } + } + + &__assistant-entry-icon { + flex-shrink: 0; + color: inherit; + opacity: 0.72; + transition: opacity $motion-fast $easing-standard; + + .bitfun-nav-panel__assistant-entry:hover &, + .bitfun-nav-panel__assistant-entry.is-active & { + opacity: 1; + } + } + + &__assistant-entry-label { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 13px; + font-weight: 500; + line-height: 1.1; + opacity: 0.72; + transition: opacity $motion-fast $easing-standard; + + .bitfun-nav-panel__assistant-entry:hover &, + .bitfun-nav-panel__assistant-entry.is-active & { + opacity: 1; + } + } + &__miniapp-entry-wrap { margin: 0; } @@ -967,10 +1188,105 @@ $_section-header-height: 24px; } +// ── 形变涟漪:按钮 clip-path 收缩成圆点,再弹开回矩形 ────────────────────── +// 各关键帧内用 animation-timing-function 分段控制 easing: +// 收缩段用 ease-in(快速压缩);展开段用 spring curve(弹性回弹) +@keyframes bitfun-nav-mode-switch { + 0% { + clip-path: circle(120% at 50% 50%); + animation-timing-function: cubic-bezier(0.55, 0, 0.75, 0.2); + } + 42% { + clip-path: circle(26px at 50% 50%); + animation-timing-function: linear; + } + // 内容在此帧附近(200ms)悄然替换,圆点完全遮住跳变 + 58% { + clip-path: circle(26px at 50% 50%); + animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1); + } + 100% { + clip-path: circle(120% at 50% 50%); + } +} + +// Logo:缩小旋转消失,旋转反向放大出现 +@keyframes bitfun-nav-mode-switch-logo { + 0% { + transform: scale(1) rotate(0deg); + opacity: 1; + animation-timing-function: cubic-bezier(0.55, 0, 0.75, 0.2); + } + 40% { + transform: scale(0.1) rotate(-160deg); + opacity: 0; + animation-timing-function: linear; + } + 60% { + transform: scale(0.1) rotate(160deg); + opacity: 0; + animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1); + } + 100% { + transform: scale(1) rotate(0deg); + opacity: 1; + } +} + +// 文字区:向下淡出,从上淡入 +@keyframes bitfun-nav-mode-switch-copy { + 0% { + opacity: 1; + transform: translateY(0); + filter: blur(0px); + animation-timing-function: cubic-bezier(0.55, 0, 0.75, 0.2); + } + 36% { + opacity: 0; + transform: translateY(5px); + filter: blur(3px); + animation-timing-function: linear; + } + 64% { + opacity: 0; + transform: translateY(-5px); + filter: blur(3px); + animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1); + } + 100% { + opacity: 1; + transform: translateY(0); + filter: blur(0px); + } +} + // ────────────────────────────────────────────── // Footer: Notification + More-options menu // ────────────────────────────────────────────── +// sections 与按钮涟漪联动:内容淡出后从对侧滑入 +@keyframes bitfun-nav-sections-mode-switch { + 0% { + opacity: 1; + transform: translateY(0); + animation-timing-function: cubic-bezier(0.55, 0, 0.75, 0.2); + } + 36% { + opacity: 0; + transform: translateY(10px); + animation-timing-function: linear; + } + 62% { + opacity: 0; + transform: translateY(-6px); + animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1); + } + 100% { + opacity: 1; + transform: translateY(0); + } +} + @keyframes bitfun-footer-menu-in { from { opacity: 0; @@ -1217,9 +1533,15 @@ $_section-header-height: 24px; &__section, &__section-label, &__layer--scene, - &__collapsible { + &__collapsible, + &__mode-switch, + &__mode-switch-logo, + &__mode-switch-copy, + &__sections { transition: none; animation: none; + clip-path: none; + filter: none; } } } diff --git a/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceItem.tsx b/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceItem.tsx index 6393e319..948acce1 100644 --- a/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceItem.tsx +++ b/src/web-ui/src/app/components/NavPanel/sections/workspaces/WorkspaceItem.tsx @@ -201,18 +201,20 @@ const WorkspaceItem: React.FC = ({ const handleCreateSession = useCallback(async () => { setMenuOpen(false); try { - await handleActivate(); await flowChatManager.createChatSession( - {}, + { + workspacePath: workspace.rootPath, + }, workspace.workspaceKind === WorkspaceKind.Assistant ? 'Claw' : undefined ); + await setActiveWorkspace(workspace.id); } catch (error) { notificationService.error( error instanceof Error ? error.message : t('nav.workspaces.createSessionFailed'), { duration: 4000 } ); } - }, [handleActivate, t, workspace.workspaceKind]); + }, [setActiveWorkspace, t, workspace.id, workspace.rootPath, workspace.workspaceKind]); const handleCreateWorktree = useCallback(async (result: BranchSelectResult) => { try { diff --git a/src/web-ui/src/flow_chat/components/ChatInput.tsx b/src/web-ui/src/flow_chat/components/ChatInput.tsx index d6ce484e..ef35f335 100644 --- a/src/web-ui/src/flow_chat/components/ChatInput.tsx +++ b/src/web-ui/src/flow_chat/components/ChatInput.tsx @@ -1631,28 +1631,6 @@ export const ChatInput: React.FC = ({
- - - {tokenUsage.current > 0 && ( - - )} -
-
- {isCollapsedProcessing && ( - <> - - - Esc - {t('input.cancelShortcut')} - - - )} {canSwitchModes && (
= ({ })()}
)} + + + + {tokenUsage.current > 0 && ( + + )} +
+
+ {isCollapsedProcessing && ( + <> + + + Esc + {t('input.cancelShortcut')} + + + )} { +const resolveSessionWorkspacePath = ( + context: FlowChatContext, + config?: SessionConfig +): string | null => { + const explicitWorkspacePath = config?.workspacePath?.trim(); + if (explicitWorkspacePath) { + return explicitWorkspacePath; + } return context.currentWorkspacePath || null; }; -const resolveSessionWorkspace = (context: FlowChatContext): WorkspaceInfo | null => { - const workspacePath = resolveSessionWorkspacePath(context); +const resolveSessionWorkspace = ( + context: FlowChatContext, + config?: SessionConfig +): WorkspaceInfo | null => { + const workspacePath = resolveSessionWorkspacePath(context, config); if (!workspacePath) return null; const state = workspaceManager.getState(); @@ -110,8 +120,8 @@ export async function createChatSession( mode?: string ): Promise { try { - const workspacePath = resolveSessionWorkspacePath(context); - const workspace = resolveSessionWorkspace(context); + const workspacePath = resolveSessionWorkspacePath(context, config); + const workspace = resolveSessionWorkspace(context, config); if (!workspacePath) { throw new Error('Workspace path is required to create a session'); diff --git a/src/web-ui/src/flow_chat/types/flow-chat.ts b/src/web-ui/src/flow_chat/types/flow-chat.ts index 1a6aa7b6..d9dbe846 100644 --- a/src/web-ui/src/flow_chat/types/flow-chat.ts +++ b/src/web-ui/src/flow_chat/types/flow-chat.ts @@ -227,6 +227,7 @@ export interface SessionConfig { modelName?: string; agentType?: string; context?: Record; + workspacePath?: string; } export interface QueuedMessage { diff --git a/src/web-ui/src/locales/en-US/common.json b/src/web-ui/src/locales/en-US/common.json index 1f058498..e3e27ae0 100644 --- a/src/web-ui/src/locales/en-US/common.json +++ b/src/web-ui/src/locales/en-US/common.json @@ -96,6 +96,14 @@ "myAgent": "My Agent", "shell": "Shell" }, + "displayModes": { + "pro": "Pro Mode", + "assistant": "Assistant Mode", + "switchToPro": "Click to switch to Pro mode", + "switchToAssistant": "Click to switch to Assistant mode", + "proDesc": "Best for focused, one-shot tasks with a clear goal.", + "assistantDesc": "Best for ongoing work with context and personal preferences." + }, "myAgent": { "title": "My Agent", "categories": { @@ -214,6 +222,9 @@ "builtinTools": "Built-in Tools", "mcpServices": "MCP Services" }, + "actions": { + "openMyAgent": "My Agent" + }, "moreOptions": "More options", "notifications": "Notifications" }, diff --git a/src/web-ui/src/locales/zh-CN/common.json b/src/web-ui/src/locales/zh-CN/common.json index 95ec4be9..d3a82185 100644 --- a/src/web-ui/src/locales/zh-CN/common.json +++ b/src/web-ui/src/locales/zh-CN/common.json @@ -96,6 +96,14 @@ "myAgent": "我的智能体", "shell": "Shell" }, + "displayModes": { + "pro": "专业模式", + "assistant": "助理模式", + "switchToPro": "点击切换到专业模式", + "switchToAssistant": "点击切换到助理模式", + "proDesc": "适合目标明确、一次完成的即时任务。", + "assistantDesc": "适合持续推进、需要延续上下文和个人偏好的任务。" + }, "myAgent": { "title": "我的智能体", "categories": { @@ -214,6 +222,9 @@ "builtinTools": "内置工具", "mcpServices": "MCP 服务" }, + "actions": { + "openMyAgent": "我的智能体" + }, "moreOptions": "更多选项", "notifications": "通知" },