diff --git a/README.md b/README.md index fc97f210..0b949085 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ BitFun is a next-generation Agent system built around the idea of **"AI assistan Every user has their own Agent assistant — one that remembers your habits and preferences, carries a unique personality, and keeps growing over time. On top of this assistant, BitFun ships with two built-in capabilities: **Code Agent** (coding assistant) and **Cowork Agent** (knowledge work assistant), along with a unified extension mechanism to define additional Agent roles as needed. -Your assistant isn't confined to the desktop — it can be reached through multiple channels, such as WeChat, Telegram, WhatsApp, and other social platforms, letting you issue instructions anytime, anywhere. Tasks keep running in the background, and you check in or give feedback whenever convenient. +Your assistant isn't confined to the desktop — it can be reached through multiple channels, such as Telegram, WhatsApp, and other social platforms, letting you issue instructions anytime, anywhere. Tasks keep running in the background, and you check in or give feedback whenever convenient. Built with **Rust + TypeScript** for an ultra-lightweight, fluid, cross-platform experience. @@ -117,7 +117,7 @@ The project uses a Rust + TypeScript tech stack, supporting cross-platform and m | **CLI** | Windows, macOS, Linux | 🚧 In Development | | **Server** | - | 🚧 In Development | | **Mobile** (Native App) | iOS, Android | 🚧 In Development | -| **Social Platform Integration** | WeChat, Telegram, WhatsApp, Discord, etc. | 🚧 In Development | +| **Social Platform Integration** | Telegram, WhatsApp, Discord, etc. | 🚧 In Development | diff --git a/README.zh-CN.md b/README.zh-CN.md index 72e708f8..bf213e8a 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -1,4 +1,4 @@ -**中文** | [English](README.md) +**中文** | [English](README.md)
@@ -25,7 +25,7 @@ BitFun 是一个以 **"有个性、有记忆的 AI 助理"** 为核心的新一 每一位用户都拥有属于自己的 Agent 助理——它记得你的习惯与偏好,拥有独特的性格设定,并随时间持续成长。在这个助理之上,BitFun 默认内置了 **Code Agent**(代码代理)与 **Cowork Agent**(桌面端工作助理)两种专业能力,并提供统一的扩展机制供用户按需定制更多 Agent 角色。 -你的助理不只存在于桌面——可以通过多种媒体方式联系,比如通过微信、Telegram、WhatsApp 等社交平台,都可以随时随地向它下达指令,任务在后台持续推进,你只需在方便时查看进度或给出反馈。 +你的助理不只存在于桌面——可以通过多种媒体方式联系,比如通过 Telegram、WhatsApp 等社交平台,都可以随时随地向它下达指令,任务在后台持续推进,你只需在方便时查看进度或给出反馈。 以 **Rust + TypeScript** 构建,追求极致轻量与流畅的跨平台体验。 @@ -118,7 +118,7 @@ npm run desktop:build | **CLI** | Windows、macOS、Linux | 🚧 开发中 | | **Server** | - | 🚧 开发中 | | **手机端**(独立 App) | iOS、Android | 🚧 开发中 | -| **社交平台接入** | 微信、Telegram、WhatsApp、Discord 等 | 🚧 开发中 | +| **社交平台接入** | Telegram、WhatsApp、Discord 等 | 🚧 开发中 | diff --git a/src/web-ui/src/app/components/NavPanel/sections/capabilities/CapabilitiesSection.scss b/src/web-ui/src/app/components/NavPanel/sections/capabilities/CapabilitiesSection.scss deleted file mode 100644 index 8c3ff4d7..00000000 --- a/src/web-ui/src/app/components/NavPanel/sections/capabilities/CapabilitiesSection.scss +++ /dev/null @@ -1,7 +0,0 @@ -/** - * CapabilitiesSection — reuses nav-panel inline list styles. - */ - -.bitfun-nav-panel__inline-list--capabilities { - // Same as GitSection; parent __inline-list already has border-left, margin, etc. -} diff --git a/src/web-ui/src/app/components/NavPanel/sections/capabilities/CapabilitiesSection.tsx b/src/web-ui/src/app/components/NavPanel/sections/capabilities/CapabilitiesSection.tsx deleted file mode 100644 index be83a08c..00000000 --- a/src/web-ui/src/app/components/NavPanel/sections/capabilities/CapabilitiesSection.tsx +++ /dev/null @@ -1,64 +0,0 @@ -/** - * CapabilitiesSection — inline sub-list under the "Capabilities" nav item. - * Items: Sub-agents / Skills / MCP; clicking one opens the Capabilities scene and sets the active view. - */ - -import React, { useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; -import { Bot, Puzzle, Plug } from 'lucide-react'; -import { Tooltip } from '@/component-library'; -import { useSceneStore } from '../../../../stores/sceneStore'; -import { useCapabilitiesSceneStore, type CapabilitiesView } from '../../../../scenes/capabilities/capabilitiesSceneStore'; -import { useApp } from '../../../../hooks/useApp'; -import './CapabilitiesSection.scss'; - -const CAP_VIEWS: { id: CapabilitiesView; icon: React.ElementType; labelKey: string }[] = [ - { id: 'sub-agents', icon: Bot, labelKey: 'subagents' }, - { id: 'skills', icon: Puzzle, labelKey: 'skills' }, - { id: 'mcp', icon: Plug, labelKey: 'mcp' }, -]; - -const CapabilitiesSection: React.FC = () => { - const { t } = useTranslation('scenes/capabilities'); - const activeTabId = useSceneStore((s) => s.activeTabId); - const openScene = useSceneStore((s) => s.openScene); - const activeView = useCapabilitiesSceneStore((s) => s.activeView); - const setActiveView = useCapabilitiesSceneStore((s) => s.setActiveView); - const { switchLeftPanelTab } = useApp(); - - const handleSelect = useCallback( - (view: CapabilitiesView) => { - openScene('capabilities'); - setActiveView(view); - switchLeftPanelTab('capabilities'); - }, - [openScene, setActiveView, switchLeftPanelTab] - ); - - return ( -
- {CAP_VIEWS.map(({ id, icon: Icon, labelKey }) => { - const label = t(labelKey); - return ( - - - - ); - })} -
- ); -}; - -export default CapabilitiesSection; diff --git a/src/web-ui/src/app/components/NavPanel/sections/tools/ToolsSection.tsx b/src/web-ui/src/app/components/NavPanel/sections/tools/ToolsSection.tsx deleted file mode 100644 index c420a86b..00000000 --- a/src/web-ui/src/app/components/NavPanel/sections/tools/ToolsSection.tsx +++ /dev/null @@ -1,92 +0,0 @@ -/** - * ToolsSection — inline sub-list under the "Tools" nav item. - * Shows two categories: built-in tools (with count) and MCP services (real server list). - * Clicking opens the Capabilities scene → mcp view. - */ - -import React, { useState, useEffect, useCallback } from 'react'; -import { useI18n } from '@/infrastructure/i18n'; -import { Wrench, Plug } from 'lucide-react'; -import { Tooltip } from '@/component-library'; -import { useSceneStore } from '../../../../stores/sceneStore'; -import { useCapabilitiesSceneStore } from '../../../../scenes/capabilities/capabilitiesSceneStore'; -import { MCPAPI, type MCPServerInfo } from '@/infrastructure/api/service-api/MCPAPI'; -import { SubagentAPI } from '@/infrastructure/api/service-api/SubagentAPI'; - -const MCP_HEALTHY_STATUSES = new Set(['connected', 'healthy']); - -const ToolsSection: React.FC = () => { - const { t } = useI18n('common'); - const activeTabId = useSceneStore((s) => s.activeTabId); - const openScene = useSceneStore((s) => s.openScene); - const activeView = useCapabilitiesSceneStore((s) => s.activeView); - const setActiveView = useCapabilitiesSceneStore((s) => s.setActiveView); - - const [builtinToolCount, setBuiltinToolCount] = useState(0); - const [mcpServers, setMcpServers] = useState([]); - - const load = useCallback(async () => { - try { - const [tools, servers] = await Promise.all([ - SubagentAPI.listAgentToolNames(), - MCPAPI.getServers(), - ]); - setBuiltinToolCount(tools.length); - setMcpServers(servers); - } catch { - // silent - } - }, []); - - useEffect(() => { load(); }, [load]); - - const handleClick = useCallback(() => { - openScene('capabilities'); - setActiveView('mcp'); - }, [openScene, setActiveView]); - - const isActive = activeTabId === 'capabilities' && activeView === 'mcp'; - - return ( -
- {/* Built-in tools summary */} - - - - - {/* MCP servers — one row per server */} - {mcpServers.map((server) => { - const healthy = MCP_HEALTHY_STATUSES.has((server.status || '').toLowerCase()); - const statusText = server.status || 'Unknown'; - const tooltipText = `${server.name} — ${statusText}`; - return ( - - - - ); - })} -
- ); -}; - -export default ToolsSection; diff --git a/src/web-ui/src/app/components/SceneBar/types.ts b/src/web-ui/src/app/components/SceneBar/types.ts index 5c98d9f4..2f57981d 100644 --- a/src/web-ui/src/app/components/SceneBar/types.ts +++ b/src/web-ui/src/app/components/SceneBar/types.ts @@ -5,7 +5,7 @@ import type { LucideIcon } from 'lucide-react'; /** Scene tab identifier — max 3 open at a time */ -export type SceneTabId = 'welcome' | 'session' | 'terminal' | 'git' | 'settings' | 'file-viewer' | 'profile' | 'capabilities' | 'team' | 'skills'; +export type SceneTabId = 'welcome' | 'session' | 'terminal' | 'git' | 'settings' | 'file-viewer' | 'profile' | 'team' | 'skills'; /** Static definition (from registry) for a scene tab type */ export interface SceneTabDef { diff --git a/src/web-ui/src/app/components/panels/content-canvas/ContentCanvas.tsx b/src/web-ui/src/app/components/panels/content-canvas/ContentCanvas.tsx index b7b40fc1..20e47754 100644 --- a/src/web-ui/src/app/components/panels/content-canvas/ContentCanvas.tsx +++ b/src/web-ui/src/app/components/panels/content-canvas/ContentCanvas.tsx @@ -49,7 +49,7 @@ export const ContentCanvas: React.FC = ({ const { handleCloseWithDirtyCheck, handleCloseAllWithDirtyCheck } = useTabLifecycle({ mode }); useKeyboardShortcuts({ enabled: true, handleCloseWithDirtyCheck }); // Panel/tab state coordinator (auto manage expand/collapse) - usePanelTabCoordinator({ + const { collapsePanel } = usePanelTabCoordinator({ autoCollapseOnEmpty: true, autoExpandOnTabOpen: true, }); @@ -89,7 +89,7 @@ export const ContentCanvas: React.FC = ({ const renderContent = () => { // Show empty state when primary group has no visible tabs if (!hasPrimaryVisibleTabs) { - return ; + return ; } return ( diff --git a/src/web-ui/src/app/components/panels/content-canvas/empty-state/EmptyState.scss b/src/web-ui/src/app/components/panels/content-canvas/empty-state/EmptyState.scss index e2de35bb..27c6dc9f 100644 --- a/src/web-ui/src/app/components/panels/content-canvas/empty-state/EmptyState.scss +++ b/src/web-ui/src/app/components/panels/content-canvas/empty-state/EmptyState.scss @@ -4,20 +4,52 @@ .canvas-empty-state { display: flex; - align-items: center; - justify-content: center; + flex-direction: column; + align-items: stretch; width: 100%; height: 100%; background: var(--color-bg-scene); - padding: 40px; + + &__toolbar { + display: flex; + align-items: center; + justify-content: flex-end; + padding: 4px 6px; + flex-shrink: 0; + } + + &__close-btn { + display: flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + padding: 0; + border: none; + border-radius: 4px; + background: transparent; + color: var(--color-text-secondary, rgba(255, 255, 255, 0.6)); + cursor: pointer; + transition: background 0.15s, color 0.15s; + + &:hover { + background: var(--color-bg-hover, rgba(255, 255, 255, 0.08)); + color: var(--color-text-primary, rgba(255, 255, 255, 0.9)); + } + } &__content { display: flex; flex-direction: column; align-items: center; + justify-content: center; + flex: 1; gap: 48px; + padding: 40px; max-width: 400px; + align-self: center; text-align: center; + width: 100%; } // Message diff --git a/src/web-ui/src/app/components/panels/content-canvas/empty-state/EmptyState.tsx b/src/web-ui/src/app/components/panels/content-canvas/empty-state/EmptyState.tsx index 5d91c004..f4c51aa0 100644 --- a/src/web-ui/src/app/components/panels/content-canvas/empty-state/EmptyState.tsx +++ b/src/web-ui/src/app/components/panels/content-canvas/empty-state/EmptyState.tsx @@ -3,19 +3,38 @@ * Empty state display. */ -import React from 'react'; +import React, { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; +import { X } from 'lucide-react'; +import { Tooltip } from '@/component-library'; import './EmptyState.scss'; export interface EmptyStateProps { - // No callbacks needed + onClose?: () => void; } -export const EmptyState: React.FC = () => { +export const EmptyState: React.FC = ({ onClose }) => { const { t } = useTranslation('components'); + const handleClose = useCallback((e: React.MouseEvent) => { + e.stopPropagation(); + onClose?.(); + }, [onClose]); + return (
+ {onClose && ( +
+ + + +
+ )}
{/* Message */}
diff --git a/src/web-ui/src/app/components/panels/content-canvas/tab-bar/Tab.scss b/src/web-ui/src/app/components/panels/content-canvas/tab-bar/Tab.scss index d768fb68..cc91d87b 100644 --- a/src/web-ui/src/app/components/panels/content-canvas/tab-bar/Tab.scss +++ b/src/web-ui/src/app/components/panels/content-canvas/tab-bar/Tab.scss @@ -40,6 +40,14 @@ } } + // Task-detail tabs: no italic, normal opacity regardless of preview state + &.is-task-detail { + &.is-preview .canvas-tab__title { + font-style: normal; + opacity: 1; + } + } + // Pinned state &.is-pinned { .canvas-tab__title { @@ -61,6 +69,14 @@ // ==================== Child elements ==================== + // Type icon (e.g. task-detail) + &__type-icon { + flex-shrink: 0; + color: var(--color-text-secondary, rgba(255, 255, 255, 0.5)); + display: flex; + align-items: center; + } + // Pin icon &__pin-icon { display: flex; diff --git a/src/web-ui/src/app/components/panels/content-canvas/tab-bar/Tab.tsx b/src/web-ui/src/app/components/panels/content-canvas/tab-bar/Tab.tsx index eb2415f3..8f8c0fc3 100644 --- a/src/web-ui/src/app/components/panels/content-canvas/tab-bar/Tab.tsx +++ b/src/web-ui/src/app/components/panels/content-canvas/tab-bar/Tab.tsx @@ -4,7 +4,7 @@ */ import React, { useCallback, useState } from 'react'; -import { X, Pin } from 'lucide-react'; +import { X, Pin, Split } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { Tooltip } from '@/component-library'; import type { CanvasTab, EditorGroupId, TabState } from '../types'; @@ -110,6 +110,8 @@ export const Tab: React.FC = ({ e.preventDefault(); }, []); + const isTaskDetail = tab.content.type === 'task-detail'; + // Build class names const classNames = [ 'canvas-tab', @@ -117,6 +119,7 @@ export const Tab: React.FC = ({ tab.isDirty && 'is-dirty', isDragging && 'is-dragging', getStateClassName(tab.state), + isTaskDetail && 'is-task-detail', ].filter(Boolean).join(' '); // Show close button only while hovering to avoid reserving layout space. @@ -147,6 +150,11 @@ export const Tab: React.FC = ({ )} + {/* Task-detail type icon */} + {isTaskDetail && ( + + )} + {/* Title */} {tab.title} diff --git a/src/web-ui/src/app/scenes/SceneViewport.tsx b/src/web-ui/src/app/scenes/SceneViewport.tsx index ee9fd604..0a81ade9 100644 --- a/src/web-ui/src/app/scenes/SceneViewport.tsx +++ b/src/web-ui/src/app/scenes/SceneViewport.tsx @@ -9,7 +9,7 @@ */ import React, { Suspense, lazy } from 'react'; -import { MessageSquare, Terminal, GitBranch, Settings, FileCode2, CircleUserRound, Blocks, Puzzle } from 'lucide-react'; +import { MessageSquare, Terminal, GitBranch, Settings, FileCode2, CircleUserRound, Puzzle } from 'lucide-react'; import type { SceneTabId } from '../components/SceneBar/types'; import { useSceneManager } from '../hooks/useSceneManager'; import { useI18n } from '@/infrastructure/i18n/hooks/useI18n'; @@ -21,7 +21,6 @@ const GitScene = lazy(() => import('./git/GitScene')); const SettingsScene = lazy(() => import('./settings/SettingsScene')); const FileViewerScene = lazy(() => import('./file-viewer/FileViewerScene')); const ProfileScene = lazy(() => import('./profile/ProfileScene')); -const CapabilitiesScene = lazy(() => import('./capabilities/CapabilitiesScene')); const TeamScene = lazy(() => import('./team/TeamScene')); const SkillsScene = lazy(() => import('./skills/SkillsScene')); const WelcomeScene = lazy(() => import('./welcome/WelcomeScene')); @@ -49,7 +48,6 @@ const SceneViewport: React.FC = ({ workspacePath, isEntering { id: 'settings' as SceneTabId, Icon: Settings, labelKey: 'scenes.settings' }, { id: 'file-viewer' as SceneTabId, Icon: FileCode2, labelKey: 'scenes.fileViewer' }, { id: 'profile' as SceneTabId, Icon: CircleUserRound, labelKey: 'scenes.projectContext' }, - { id: 'capabilities' as SceneTabId, Icon: Blocks, labelKey: 'scenes.capabilities' }, { id: 'skills' as SceneTabId, Icon: Puzzle, labelKey: 'scenes.skills' }, ].map(({ id, Icon, labelKey }) => { const label = t(labelKey); @@ -106,8 +104,6 @@ function renderScene(id: SceneTabId, workspacePath?: string, isEntering?: boolea return ; case 'profile': return ; - case 'capabilities': - return ; case 'team': return ; case 'skills': diff --git a/src/web-ui/src/app/scenes/capabilities/CapabilitiesScene.scss b/src/web-ui/src/app/scenes/capabilities/CapabilitiesScene.scss deleted file mode 100644 index d5c82c3d..00000000 --- a/src/web-ui/src/app/scenes/capabilities/CapabilitiesScene.scss +++ /dev/null @@ -1,11 +0,0 @@ -/** - * CapabilitiesScene styles. - */ - -.bitfun-capabilities-scene { - display: flex; - flex-direction: column; - flex: 1; - height: 100%; - overflow: hidden; -} diff --git a/src/web-ui/src/app/scenes/capabilities/CapabilitiesScene.tsx b/src/web-ui/src/app/scenes/capabilities/CapabilitiesScene.tsx deleted file mode 100644 index d4e0e2e5..00000000 --- a/src/web-ui/src/app/scenes/capabilities/CapabilitiesScene.tsx +++ /dev/null @@ -1,29 +0,0 @@ -/** - * CapabilitiesScene — Capabilities scene content. Renders view by activeView from capabilitiesSceneStore. - * Left nav uses MainNav with inline CapabilitiesSection (sub-agents / skills / mcp). - */ - -import React from 'react'; -import { useCapabilitiesSceneStore } from './capabilitiesSceneStore'; -import { AgentsView, SkillsView, MCPView } from './views'; -import './CapabilitiesScene.scss'; - -const CapabilitiesScene: React.FC = () => { - const activeView = useCapabilitiesSceneStore((s) => s.activeView); - - const renderView = () => { - switch (activeView) { - case 'skills': - return ; - case 'mcp': - return ; - case 'sub-agents': - default: - return ; - } - }; - - return
{renderView()}
; -}; - -export default CapabilitiesScene; diff --git a/src/web-ui/src/app/scenes/capabilities/capabilitiesSceneStore.ts b/src/web-ui/src/app/scenes/capabilities/capabilitiesSceneStore.ts deleted file mode 100644 index f42c89f0..00000000 --- a/src/web-ui/src/app/scenes/capabilities/capabilitiesSceneStore.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * capabilitiesSceneStore — Zustand store for the Capabilities scene. - * - * Shared between CapabilitiesSection (left nav) and CapabilitiesScene (content area) - * so both reflect the same active view. - */ - -import { create } from 'zustand'; - -export type CapabilitiesView = 'sub-agents' | 'skills' | 'mcp'; - -interface CapabilitiesSceneState { - activeView: CapabilitiesView; - setActiveView: (view: CapabilitiesView) => void; -} - -export const useCapabilitiesSceneStore = create((set) => ({ - activeView: 'sub-agents', - setActiveView: (view) => set({ activeView: view }), -})); diff --git a/src/web-ui/src/app/scenes/capabilities/index.ts b/src/web-ui/src/app/scenes/capabilities/index.ts deleted file mode 100644 index 48d84606..00000000 --- a/src/web-ui/src/app/scenes/capabilities/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as CapabilitiesScene } from './CapabilitiesScene'; diff --git a/src/web-ui/src/app/scenes/capabilities/views/AgentsView.tsx b/src/web-ui/src/app/scenes/capabilities/views/AgentsView.tsx deleted file mode 100644 index 3a770fc4..00000000 --- a/src/web-ui/src/app/scenes/capabilities/views/AgentsView.tsx +++ /dev/null @@ -1,154 +0,0 @@ -/** - * AgentsView — agents card list with toggle and expand details. - */ - -import React, { useState, useEffect, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; -import { Bot, RefreshCw } from 'lucide-react'; -import { Switch, Card, CardBody } from '@/component-library'; -import { configAPI } from '@/infrastructure/api'; -import { SubagentAPI, type SubagentInfo } from '@/infrastructure/api/service-api/SubagentAPI'; -import { useNotification } from '@/shared/notification-system'; -import { isBuiltinSubAgent } from '@/infrastructure/agents/constants'; -import './capabilities-views.scss'; - -function getSourceBadge(agent: SubagentInfo): string { - if (agent.subagentSource === 'builtin' && isBuiltinSubAgent(agent.id)) return 'Sub-Agent'; - return agent.subagentSource ?? ''; -} - -const AgentsView: React.FC = () => { - const { t } = useTranslation('scenes/capabilities'); - const { error: notifyError } = useNotification(); - const [agents, setAgents] = useState([]); - const [isRefreshing, setIsRefreshing] = useState(false); - const [expandedIds, setExpandedIds] = useState>(() => new Set()); - - const load = useCallback( - async (silent = false) => { - try { - const list = await SubagentAPI.listSubagents(); - setAgents(list); - } catch (err) { - if (!silent) notifyError(t('loadFailed')); - } - }, - [notifyError, t] - ); - - useEffect(() => { - load(); - }, [load]); - - const handleRefresh = useCallback(async () => { - setIsRefreshing(true); - try { - await load(true); - } finally { - setIsRefreshing(false); - } - }, [load]); - - const toggleExpanded = useCallback((id: string) => { - setExpandedIds((prev) => { - const next = new Set(prev); - if (next.has(id)) next.delete(id); - else next.add(id); - return next; - }); - }, []); - - const handleToggle = useCallback( - async (agent: SubagentInfo) => { - try { - const isCustom = agent.subagentSource === 'user' || agent.subagentSource === 'project'; - if (isCustom) { - await SubagentAPI.updateSubagentConfig({ subagentId: agent.id, enabled: !agent.enabled }); - } else { - await configAPI.setSubagentConfig(agent.id, !agent.enabled); - } - await load(true); - } catch { - notifyError(t('toggleFailed')); - } - }, - [load, notifyError, t] - ); - - return ( -
-
- {agents.length === 0 ? ( -
- {t('emptyAgents')} -
- ) : ( -
- {agents.map((agent) => { - const isExpanded = expandedIds.has(`agent:${agent.id}`); - return ( - -
toggleExpanded(`agent:${agent.id}`)} - > -
- -
-
- {agent.name} - {agent.model && ( - {agent.model} - )} - {getSourceBadge(agent) && ( - - {getSourceBadge(agent)} - - )} -
-
e.stopPropagation()}> - handleToggle(agent)} - size="small" - /> -
-
- {isExpanded && ( - - {agent.description && ( -
{agent.description}
- )} -
- {t('toolCount')} - {agent.toolCount} -
-
- )} -
- ); - })} -
- )} -
-
- -
-
- ); -}; - -export default AgentsView; diff --git a/src/web-ui/src/app/scenes/capabilities/views/MCPView.tsx b/src/web-ui/src/app/scenes/capabilities/views/MCPView.tsx deleted file mode 100644 index 1881f27e..00000000 --- a/src/web-ui/src/app/scenes/capabilities/views/MCPView.tsx +++ /dev/null @@ -1,163 +0,0 @@ -/** - * MCPView — MCP servers card list with status and reconnect. - */ - -import React, { useState, useEffect, useCallback, useMemo } from 'react'; -import { useTranslation } from 'react-i18next'; -import { Plug, RefreshCw, AlertTriangle } from 'lucide-react'; -import { Card, CardBody } from '@/component-library'; -import { MCPAPI, type MCPServerInfo } from '@/infrastructure/api/service-api/MCPAPI'; -import { useNotification } from '@/shared/notification-system'; -import './capabilities-views.scss'; - -const MCP_HEALTHY_STATUSES = new Set(['connected', 'healthy']); - -const MCPView: React.FC = () => { - const { t } = useTranslation('scenes/capabilities'); - const { error: notifyError, success: notifySuccess } = useNotification(); - const [mcpServers, setMcpServers] = useState([]); - const [isRefreshing, setIsRefreshing] = useState(false); - const [expandedIds, setExpandedIds] = useState>(() => new Set()); - - const load = useCallback( - async (silent = false) => { - try { - const list = await MCPAPI.getServers(); - setMcpServers(list); - } catch (err) { - if (!silent) notifyError(t('loadFailed')); - } - }, - [notifyError, t] - ); - - useEffect(() => { - load(); - }, [load]); - - const handleRefresh = useCallback(async () => { - setIsRefreshing(true); - try { - await load(true); - } finally { - setIsRefreshing(false); - } - }, [load]); - - const handleReconnect = useCallback( - async (server: MCPServerInfo) => { - try { - if ((server.status || '').toLowerCase() === 'stopped') { - await MCPAPI.startServer(server.id); - } else { - await MCPAPI.restartServer(server.id); - } - await load(true); - notifySuccess(t('mcpReconnectSuccess', { name: server.name })); - } catch { - notifyError(t('mcpReconnectFailed', { name: server.name })); - } - }, - [load, notifyError, notifySuccess, t] - ); - - const toggleExpanded = useCallback((id: string) => { - setExpandedIds((prev) => { - const next = new Set(prev); - if (next.has(id)) next.delete(id); - else next.add(id); - return next; - }); - }, []); - - const enabledMcp = useMemo(() => mcpServers.filter((s) => s.enabled), [mcpServers]); - const unhealthyMcp = useMemo( - () => - enabledMcp.filter((s) => !MCP_HEALTHY_STATUSES.has((s.status || '').toLowerCase())), - [enabledMcp] - ); - const hasMcpIssue = unhealthyMcp.length > 0; - - return ( -
- {hasMcpIssue && ( -
- - {t('mcpWarning', { count: unhealthyMcp.length })} -
- )} -
- {mcpServers.length === 0 ? ( -
- ) : ( -
- {mcpServers.map((server) => { - const healthy = MCP_HEALTHY_STATUSES.has((server.status || '').toLowerCase()); - const isExpanded = expandedIds.has(`mcp:${server.id}`); - return ( - -
toggleExpanded(`mcp:${server.id}`)} - > -
- -
-
- {server.name} - - {server.status} - -
- {!healthy && ( -
e.stopPropagation()}> - -
- )} -
- {isExpanded && ( - -
- {t('serverType')} - {server.serverType} -
-
- )} -
- ); - })} -
- )} -
-
- -
-
- ); -}; - -export default MCPView; diff --git a/src/web-ui/src/app/scenes/capabilities/views/SkillsView.tsx b/src/web-ui/src/app/scenes/capabilities/views/SkillsView.tsx deleted file mode 100644 index f0912ad0..00000000 --- a/src/web-ui/src/app/scenes/capabilities/views/SkillsView.tsx +++ /dev/null @@ -1,159 +0,0 @@ -/** - * SkillsView — skills card list with toggle and expand details (path copy). - */ - -import React, { useState, useEffect, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; -import { Puzzle, Check, Copy, RefreshCw } from 'lucide-react'; -import { Switch, Card, CardBody } from '@/component-library'; -import { configAPI } from '@/infrastructure/api'; -import type { SkillInfo } from '@/infrastructure/config/types'; -import { useNotification } from '@/shared/notification-system'; -import './capabilities-views.scss'; - -const SkillsView: React.FC = () => { - const { t } = useTranslation('scenes/capabilities'); - const { error: notifyError, success: notifySuccess } = useNotification(); - const [skills, setSkills] = useState([]); - const [isRefreshing, setIsRefreshing] = useState(false); - const [expandedIds, setExpandedIds] = useState>(() => new Set()); - const [copiedPath, setCopiedPath] = useState(null); - - const load = useCallback( - async (silent = false) => { - try { - const list = await configAPI.getSkillConfigs(); - setSkills(list); - } catch (err) { - if (!silent) notifyError(t('loadFailed')); - } - }, - [notifyError, t] - ); - - useEffect(() => { - load(); - }, [load]); - - const handleRefresh = useCallback(async () => { - setIsRefreshing(true); - try { - await load(true); - } finally { - setIsRefreshing(false); - } - }, [load]); - - const handleCopyPath = useCallback( - async (path: string) => { - try { - await navigator.clipboard.writeText(path); - setCopiedPath(path); - notifySuccess(t('pathCopied')); - setTimeout(() => setCopiedPath(null), 2000); - } catch { - notifyError(t('pathCopyFailed')); - } - }, - [notifySuccess, notifyError, t] - ); - - const toggleExpanded = useCallback((id: string) => { - setExpandedIds((prev) => { - const next = new Set(prev); - if (next.has(id)) next.delete(id); - else next.add(id); - return next; - }); - }, []); - - const handleToggle = useCallback( - async (skill: SkillInfo) => { - try { - await configAPI.setSkillEnabled(skill.name, !skill.enabled); - await load(true); - } catch { - notifyError(t('toggleFailed')); - } - }, - [load, notifyError, t] - ); - - return ( -
-
- {skills.length === 0 ? ( -
- {t('emptySkills')} -
- ) : ( -
- {skills.map((skill) => { - const isExpanded = expandedIds.has(`skill:${skill.name}`); - return ( - -
toggleExpanded(`skill:${skill.name}`)} - > -
- -
-
- {skill.name} - {skill.level} -
-
e.stopPropagation()}> - handleToggle(skill)} - size="small" - /> -
-
- {isExpanded && ( - - {skill.description && ( -
{skill.description}
- )} - -
- )} -
- ); - })} -
- )} -
-
- -
-
- ); -}; - -export default SkillsView; diff --git a/src/web-ui/src/app/scenes/capabilities/views/capabilities-views.scss b/src/web-ui/src/app/scenes/capabilities/views/capabilities-views.scss deleted file mode 100644 index c38ad5db..00000000 --- a/src/web-ui/src/app/scenes/capabilities/views/capabilities-views.scss +++ /dev/null @@ -1,332 +0,0 @@ -/** - * Capabilities scene views — shared card list styles. - * Migrated from SessionsPanel cap-* with BEM block .bitfun-cap. - */ - -@use '../../../../component-library/styles/tokens.scss' as *; - -.bitfun-cap { - &__view { - flex: 1; - min-height: 0; - display: flex; - flex-direction: column; - padding: $size-gap-2 $size-gap-3; - gap: $size-gap-2; - overflow: hidden; - } - - &__content { - flex: 1; - min-height: 0; - overflow-y: auto; - overflow-x: hidden; - } - - &__cards-grid { - display: flex; - flex-direction: column; - gap: $size-gap-2; - padding: 2px 0 8px; - } - - &__card { - overflow: hidden; - - &.is-expanded { - background: var(--card-bg-hover) !important; - } - - &.is-disabled { - opacity: 0.6; - - .bitfun-cap__card-name { - text-decoration: line-through; - } - } - - &.is-unhealthy { - .bitfun-cap__card-name { - color: var(--color-warning, #f59e0b); - } - } - } - - &__card-header { - display: flex; - align-items: center; - gap: 8px; - padding: 8px 10px; - cursor: pointer; - user-select: none; - - &:hover { - background: var(--card-bg-hover, rgba(255, 255, 255, 0.06)); - } - } - - &__card-icon { - display: flex; - align-items: center; - justify-content: center; - width: 24px; - height: 24px; - border-radius: 6px; - flex-shrink: 0; - background: var(--element-bg-soft); - color: var(--color-text-muted); - - &--skill { - background: color-mix(in srgb, var(--color-purple-500) 12%, transparent); - color: var(--color-purple-500); - - .is-disabled & { - background: var(--element-bg-soft); - color: var(--color-text-muted); - } - } - - &--agent { - background: color-mix(in srgb, var(--color-accent-500) 12%, transparent); - color: var(--color-accent-500); - - .is-disabled & { - background: var(--element-bg-soft); - color: var(--color-text-muted); - } - } - - &--mcp { - background: color-mix(in srgb, var(--color-success) 12%, transparent); - color: var(--color-success); - - &.is-error { - background: color-mix(in srgb, var(--color-warning) 12%, transparent); - color: var(--color-warning); - } - } - } - - &__card-info { - display: flex; - align-items: center; - gap: 6px; - flex: 1; - min-width: 0; - } - - &__card-name { - font-size: $font-size-xs; - font-weight: $font-weight-medium; - color: var(--color-text-primary); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - - &__card-actions { - display: flex; - align-items: center; - gap: 4px; - flex-shrink: 0; - } - - &__badge { - display: inline-flex; - align-items: center; - padding: 1px 6px; - border-radius: $size-radius-sm; - font-size: 10px; - font-weight: $font-weight-medium; - white-space: nowrap; - flex-shrink: 0; - border: 1px solid; - background: transparent; - line-height: 16px; - - &--purple { - border-color: var(--border-purple-subtle); - color: var(--color-purple-500); - } - - &--blue { - border-color: var(--border-accent-subtle); - color: var(--color-accent-500); - } - - &--gray { - border-color: var(--border-base); - color: var(--color-text-secondary); - } - - &--green { - border-color: var(--color-success-border); - color: var(--color-success); - } - - &--yellow { - border-color: var(--color-warning-border); - color: var(--color-warning); - } - } - - &__card-details { - padding: 8px 10px 10px !important; - border-top: 1px dashed var(--border-medium); - animation: bitfun-cap-slide-down 0.2s $easing-standard; - } - - &__card-desc { - font-size: $font-size-xs; - color: var(--color-text-secondary); - line-height: $line-height-base; - margin-bottom: 8px; - } - - &__path { - display: flex; - gap: 8px; - align-items: center; - padding: 5px 8px; - border-radius: $size-radius-sm; - border: 1px solid var(--border-base); - background: transparent; - width: 100%; - text-align: left; - cursor: pointer; - transition: border-color $motion-fast $easing-standard, - background $motion-fast $easing-standard; - - &:hover { - border-color: var(--border-accent-soft); - background: color-mix(in srgb, var(--color-accent-500) 4%, transparent); - } - } - - &__path-copy { - flex-shrink: 0; - display: flex; - align-items: center; - color: var(--color-text-muted); - margin-left: auto; - } - - &__meta-row { - display: flex; - gap: 8px; - align-items: center; - padding: 4px 0; - } - - &__path-label, - &__meta-label { - font-size: 11px; - color: var(--color-text-muted); - font-weight: $font-weight-semibold; - flex-shrink: 0; - } - - &__path-value, - &__meta-value { - font-size: $font-size-xs; - color: var(--color-text-primary); - font-family: $font-family-mono; - word-break: break-all; - } - - &__row-reconnect { - display: flex; - align-items: center; - justify-content: center; - width: 22px; - height: 22px; - border: 1px solid color-mix(in srgb, var(--color-warning, #f59e0b) 25%, transparent); - border-radius: 5px; - background: transparent; - color: var(--color-warning, #f59e0b); - cursor: pointer; - transition: all $motion-fast $easing-standard; - flex-shrink: 0; - - &:hover { - background: color-mix(in srgb, var(--color-warning, #f59e0b) 10%, transparent); - } - } - - &__footer { - flex-shrink: 0; - display: flex; - justify-content: center; - padding: 4px 0 0; - border-top: 1px solid color-mix(in srgb, var(--element-border-base) 50%, transparent); - } - - &__refresh { - display: inline-flex; - align-items: center; - gap: 4px; - padding: 5px 10px; - border: none; - border-radius: 6px; - background: transparent; - color: var(--color-text-muted); - font-size: 10px; - cursor: pointer; - transition: all $motion-fast $easing-standard; - - &:hover { - background: var(--element-bg-soft); - color: var(--color-text-secondary); - } - - &.is-spinning svg { - animation: bitfun-cap-spin 0.8s linear infinite; - } - } - - &__empty { - display: flex; - align-items: center; - justify-content: center; - padding: 32px 16px; - color: var(--color-text-muted); - font-size: $font-size-xs; - opacity: 0.5; - } - - &__alert { - display: flex; - align-items: center; - gap: $size-gap-2; - padding: 5px 8px; - border-radius: 6px; - font-size: 11px; - background: color-mix(in srgb, var(--color-warning, #f59e0b) 8%, transparent); - color: var(--color-warning, #f59e0b); - flex-shrink: 0; - - svg { - flex-shrink: 0; - } - } -} - -@keyframes bitfun-cap-slide-down { - from { - opacity: 0; - transform: translateY(-4px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -@keyframes bitfun-cap-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} diff --git a/src/web-ui/src/app/scenes/capabilities/views/index.ts b/src/web-ui/src/app/scenes/capabilities/views/index.ts deleted file mode 100644 index 5892f596..00000000 --- a/src/web-ui/src/app/scenes/capabilities/views/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { default as AgentsView } from './AgentsView'; -export { default as SkillsView } from './SkillsView'; -export { default as MCPView } from './MCPView'; diff --git a/src/web-ui/src/app/scenes/skills/views/InstalledView.tsx b/src/web-ui/src/app/scenes/skills/views/InstalledView.tsx index ab56b286..04f49a35 100644 --- a/src/web-ui/src/app/scenes/skills/views/InstalledView.tsx +++ b/src/web-ui/src/app/scenes/skills/views/InstalledView.tsx @@ -7,7 +7,7 @@ import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { Plus, Trash2, RefreshCw, FolderOpen, X, Package } from 'lucide-react'; -import { Switch, Select, Input, Button, IconButton, ConfirmDialog, Badge } from '@/component-library'; +import { Select, Input, Button, IconButton, ConfirmDialog, Badge } from '@/component-library'; import { useCurrentWorkspace } from '@/infrastructure/hooks/useWorkspace'; import { useNotification } from '@/shared/notification-system'; import { configAPI } from '@/infrastructure/api'; @@ -119,20 +119,6 @@ const InstalledView: React.FC = () => { } }; - const handleToggle = async (skill: SkillInfo) => { - const newEnabled = !skill.enabled; - try { - await configAPI.setSkillEnabled(skill.name, newEnabled); - notification.success(t('messages.toggleSuccess', { - name: skill.name, - status: newEnabled ? t('messages.enabled') : t('messages.disabled'), - })); - loadSkills(); - } catch (err) { - notification.error(t('messages.toggleFailed', { error: err instanceof Error ? err.message : String(err) })); - } - }; - const handleBrowse = async () => { try { const selected = await open({ directory: true, multiple: false, title: t('form.path.label') }); @@ -223,7 +209,6 @@ const InstalledView: React.FC = () => { key={skill.name} className={[ 'bitfun-market__list-item', - !skill.enabled && 'is-disabled', isExpanded && 'is-expanded', ].filter(Boolean).join(' ')} style={{ '--item-index': index } as React.CSSProperties} @@ -241,9 +226,6 @@ const InstalledView: React.FC = () => { {skill.level === 'user' ? t('list.item.user') : t('list.item.project')} - {!skill.enabled && ( - {t('messages.disabled')} - )}

{skill.description?.trim() || '—'} @@ -256,11 +238,6 @@ const InstalledView: React.FC = () => { className="bitfun-market__list-item-action bitfun-installed__item-actions" onClick={(e) => e.stopPropagation()} > - handleToggle(skill)} - size="small" - /> + ); + })} +

+ )} + + {!skillsEditing && ( +
+ {enabledSkillsDisplay.length === 0 ? ( + {t('agentsOverview.noSkills', '未启用任何 Skill')} + ) : ( + enabledSkillsDisplay.map((skill) => ( + + {skill.name} + + )) + )} +
+ )} +
+ )}
)}
@@ -293,6 +365,7 @@ const AgentsOverviewPage: React.FC = () => { const [allAgents, setAllAgents] = useState([]); const [loading, setLoading] = useState(true); const [availableTools, setAvailableTools] = useState([]); + const [availableSkills, setAvailableSkills] = useState([]); const [modeConfigs, setModeConfigs] = useState>({}); const loadAgents = useCallback(async () => { @@ -306,11 +379,12 @@ const AgentsOverviewPage: React.FC = () => { } }; try { - const [modes, subagents, tools, configs] = await Promise.all([ + const [modes, subagents, tools, configs, skills] = await Promise.all([ agentAPI.getAvailableModes().catch(() => []), SubagentAPI.listSubagents().catch(() => []), fetchTools(), configAPI.getModeConfigs().catch(() => ({})), + configAPI.getSkillConfigs().catch(() => []), ]); const modeAgents: AgentWithCapabilities[] = modes.map((m) => enrichCapabilities({ @@ -325,6 +399,7 @@ const AgentsOverviewPage: React.FC = () => { ); setAllAgents([...modeAgents, ...subAgents]); setAvailableTools(tools); + setAvailableSkills(skills.filter((s: SkillInfo) => s.enabled)); setModeConfigs(configs as Record); } finally { setLoading(false); @@ -333,7 +408,6 @@ const AgentsOverviewPage: React.FC = () => { useEffect(() => { loadAgents(); }, [loadAgents]); - // 获取 mode 的有效配置(含默认值回退) const getModeConfig = useCallback((agentId: string): ModeConfigItem | null => { const agent = allAgents.find((a) => a.id === agentId && a.agentKind === 'mode'); if (!agent) return null; @@ -388,18 +462,29 @@ const AgentsOverviewPage: React.FC = () => { } }, [notification, t]); + const handleToggleSkill = useCallback(async (agentId: string, skillName: string) => { + const config = getModeConfig(agentId); + if (!config) return; + const skills = config.available_skills ?? []; + const isEnabling = !skills.includes(skillName); + const newSkills = isEnabling ? [...skills, skillName] : skills.filter((s) => s !== skillName); + try { + await saveModeConfig(agentId, { available_skills: newSkills }); + } catch { + notification.error(t('agentsOverview.skillToggleFailed', 'Skill 切换失败')); + } + }, [getModeConfig, saveModeConfig, notification, t]); + const filteredAgents = allAgents.filter((a) => { - // 文本搜索 if (query) { const q = query.toLowerCase(); if (!a.name.toLowerCase().includes(q) && !a.description.toLowerCase().includes(q)) return false; } - // 类型筛选 if (filterType !== 'all') { if (filterType === 'mode' && a.agentKind !== 'mode') return false; if (filterType === 'subagent' && a.agentKind !== 'subagent') return false; } - // 级别筛选(mode 归属 builtin) + // mode agents always map to 'builtin' level if (filterLevel !== 'all') { const level = a.agentKind === 'mode' ? 'builtin' : (a.subagentSource ?? 'builtin'); if (level !== filterLevel) return false; @@ -506,6 +591,8 @@ const AgentsOverviewPage: React.FC = () => { modeConfig={a.agentKind === 'mode' ? getModeConfig(a.id) : null} onToggleTool={handleToggleTool} onResetTools={handleResetTools} + availableSkills={availableSkills} + onToggleSkill={handleToggleSkill} /> ))} diff --git a/src/web-ui/src/app/scenes/team/components/TeamHomePage.scss b/src/web-ui/src/app/scenes/team/components/TeamHomePage.scss index 31e2933d..f4d3d7b6 100644 --- a/src/web-ui/src/app/scenes/team/components/TeamHomePage.scss +++ b/src/web-ui/src/app/scenes/team/components/TeamHomePage.scss @@ -430,7 +430,13 @@ background: var(--element-bg-subtle); display: flex; flex-direction: column; - gap: $size-gap-3; + gap: 0; + + & > * + * { + margin-top: $size-gap-3; + border-top: 1px dashed var(--border-subtle); + padding-top: $size-gap-3; + } } .th-list__detail-desc { @@ -568,7 +574,6 @@ line-height: 1; } -// 管理操作按钮组(全选/全清/重置/展开) .th-list__tools-actions { display: inline-flex; align-items: center; @@ -576,13 +581,19 @@ margin-left: auto; } -// 折叠状态下的已启用工具 chip 行 .th-list__tools-grid { display: flex; flex-wrap: wrap; gap: $size-gap-2; } +.th-list__tools-empty { + font-size: 10px; + color: var(--color-text-tertiary); + font-style: italic; + opacity: 0.6; +} + .th-list__tool-chip { display: inline-flex; align-items: center; @@ -601,7 +612,6 @@ } -// 展开状态下的工具交互面板 .th-list__tools-panel { display: flex; flex-wrap: wrap; @@ -621,7 +631,6 @@ } } -// 工具面板内的可点击工具 item .th-list__tool-item { display: inline-flex; align-items: center; diff --git a/src/web-ui/src/component-library/components/CodeEditor/CodeEditor.tsx b/src/web-ui/src/component-library/components/CodeEditor/CodeEditor.tsx index 1d95063c..31c7b2ce 100644 --- a/src/web-ui/src/component-library/components/CodeEditor/CodeEditor.tsx +++ b/src/web-ui/src/component-library/components/CodeEditor/CodeEditor.tsx @@ -54,7 +54,7 @@ export const CodeEditor: React.FC = ({ minimap = true, height = '500px', width = '100%', - wordWrap = 'on', + wordWrap = 'off', fontSize = 16, tabSize = 2, onChange, diff --git a/src/web-ui/src/flow_chat/components/TaskDetailPanel/TaskDetailPanel.scss b/src/web-ui/src/flow_chat/components/TaskDetailPanel/TaskDetailPanel.scss index a521549d..5aa34859 100644 --- a/src/web-ui/src/flow_chat/components/TaskDetailPanel/TaskDetailPanel.scss +++ b/src/web-ui/src/flow_chat/components/TaskDetailPanel/TaskDetailPanel.scss @@ -76,8 +76,8 @@ right: 16px; top: 50%; transform: translateY(-50%); - color: var(--color-primary); - animation: task-detail-spin 1s linear infinite; + display: flex; + align-items: center; } &__header-failed { @@ -190,11 +190,6 @@ font-size: 13px; } - &__loading-icon { - color: var(--color-primary); - animation: task-detail-spin 1s linear infinite; - } - &__error { display: flex; align-items: flex-start; @@ -219,11 +214,3 @@ } } -@keyframes task-detail-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} diff --git a/src/web-ui/src/flow_chat/components/TaskDetailPanel/TaskDetailPanel.tsx b/src/web-ui/src/flow_chat/components/TaskDetailPanel/TaskDetailPanel.tsx index 002c0dc4..f6a6cf9d 100644 --- a/src/web-ui/src/flow_chat/components/TaskDetailPanel/TaskDetailPanel.tsx +++ b/src/web-ui/src/flow_chat/components/TaskDetailPanel/TaskDetailPanel.tsx @@ -8,15 +8,14 @@ import { useTranslation } from 'react-i18next'; import { Split, Clock, - AlertCircle, - Loader2 + AlertCircle } from 'lucide-react'; import type { FlowToolItem, FlowTextItem, FlowThinkingItem, FlowItem } from '../../types/flow-chat'; import { FlowChatStore } from '../../store/FlowChatStore'; import { FlowTextBlock } from '../FlowTextBlock'; import { FlowToolCard } from '../FlowToolCard'; import { ModelThinkingDisplay } from '../../tool-cards/ModelThinkingDisplay'; -import { Tooltip } from '@/component-library'; +import { Tooltip, CubeLoading } from '@/component-library'; import { createLogger } from '@/shared/utils/logger'; import './TaskDetailPanel.scss'; @@ -242,7 +241,9 @@ export const TaskDetailPanel: React.FC = ({ data }) => { )} {isRunning && ( - + + + )} {isFailed && ( @@ -270,7 +271,7 @@ export const TaskDetailPanel: React.FC = ({ data }) => { {isRunning && subagentItems.length === 0 && (
- + {t('toolCards.taskDetailPanel.status.running')}
)} diff --git a/src/web-ui/src/flow_chat/tool-cards/AskUserQuestionCard.tsx b/src/web-ui/src/flow_chat/tool-cards/AskUserQuestionCard.tsx index b17ab495..0822b470 100644 --- a/src/web-ui/src/flow_chat/tool-cards/AskUserQuestionCard.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/AskUserQuestionCard.tsx @@ -4,7 +4,7 @@ */ import React, { useState, useCallback, useMemo } from 'react'; -import { CheckCircle, Loader2, AlertCircle, Send, ChevronDown, ChevronRight } from 'lucide-react'; +import { Loader2, AlertCircle, Send, ChevronDown, ChevronRight } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import type { ToolCardProps } from '../types/flow-chat'; import { toolAPI } from '@/infrastructure/api/service-api/ToolAPI'; @@ -130,7 +130,7 @@ export const AskUserQuestionCard: React.FC = ({ const getStatusIcon = () => { if (status === 'completed') { - return ; + return null; } if (isSubmitting) { return ; diff --git a/src/web-ui/src/flow_chat/tool-cards/CodeReviewToolCard.tsx b/src/web-ui/src/flow_chat/tool-cards/CodeReviewToolCard.tsx index 725d06c4..3844ec3d 100644 --- a/src/web-ui/src/flow_chat/tool-cards/CodeReviewToolCard.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/CodeReviewToolCard.tsx @@ -59,7 +59,7 @@ export const CodeReviewToolCard: React.FC = React.memo(({ case 'streaming': return ; case 'completed': - return ; + return null; case 'pending': default: return ; diff --git a/src/web-ui/src/flow_chat/tool-cards/CompactToolCard.scss b/src/web-ui/src/flow_chat/tool-cards/CompactToolCard.scss index 1531dcba..8fed5dfc 100644 --- a/src/web-ui/src/flow_chat/tool-cards/CompactToolCard.scss +++ b/src/web-ui/src/flow_chat/tool-cards/CompactToolCard.scss @@ -126,6 +126,10 @@ .compact-tool-card.status-completed { .compact-card-status-icon { color: var(--color-text-muted); + + .icon-check-done { + color: var(--color-success); + } } } diff --git a/src/web-ui/src/flow_chat/tool-cards/ContextCompressionDisplay.tsx b/src/web-ui/src/flow_chat/tool-cards/ContextCompressionDisplay.tsx index 5f06f501..0d743e6f 100644 --- a/src/web-ui/src/flow_chat/tool-cards/ContextCompressionDisplay.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/ContextCompressionDisplay.tsx @@ -3,7 +3,7 @@ */ import React from 'react'; -import { Archive, CheckCircle } from 'lucide-react'; +import { Archive } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { CubeLoading } from '../../component-library'; import type { FlowToolItem } from '../types/flow-chat'; @@ -84,9 +84,6 @@ export const ContextCompressionDisplay: React.FC if (isLoading) { return ; } - if (data.status === 'completed' && !isFailed) { - return ; - } return null; }; diff --git a/src/web-ui/src/flow_chat/tool-cards/DefaultToolCard.tsx b/src/web-ui/src/flow_chat/tool-cards/DefaultToolCard.tsx index 6b87c148..81a43e59 100644 --- a/src/web-ui/src/flow_chat/tool-cards/DefaultToolCard.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/DefaultToolCard.tsx @@ -4,7 +4,7 @@ */ import React from 'react'; -import { Loader2, CheckCircle, XCircle, Clock } from 'lucide-react'; +import { Loader2, XCircle, Clock } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import type { ToolCardProps } from '../types/flow-chat'; @@ -32,7 +32,7 @@ export const DefaultToolCard: React.FC = ({ case 'streaming': return ; case 'completed': - return ; + return null; case 'cancelled': return ; case 'error': diff --git a/src/web-ui/src/flow_chat/tool-cards/FileOperationToolCard.tsx b/src/web-ui/src/flow_chat/tool-cards/FileOperationToolCard.tsx index abc6a55c..f8248a7a 100644 --- a/src/web-ui/src/flow_chat/tool-cards/FileOperationToolCard.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/FileOperationToolCard.tsx @@ -5,7 +5,7 @@ import React, { useEffect, useCallback, useMemo, useState, useRef } from 'react'; import { useTranslation } from 'react-i18next'; -import { CheckCircle, XCircle, GitBranch, FileText, ChevronDown, ChevronUp, FileEdit, FilePlus, Trash2 } from 'lucide-react'; +import { XCircle, GitBranch, FileText, ChevronDown, ChevronUp, FileEdit, FilePlus, Trash2 } from 'lucide-react'; import { CubeLoading } from '../../component-library'; import type { ToolCardProps } from '../types/flow-chat'; import { BaseToolCard, ToolCardHeader } from './BaseToolCard'; @@ -323,9 +323,6 @@ export const FileOperationToolCard: React.FC = ({ if (isLoading) { return ; } - if (status === 'completed' && !isFailed) { - return ; - } return null; }; @@ -439,7 +436,7 @@ export const FileOperationToolCard: React.FC = ({ content={newStringContent} filePath={currentFilePath} isStreaming={isParamsStreaming} - showLineNumbers={true} + showLineNumbers={false} maxHeight={300} autoScrollToBottom={true} onLineClick={handleCodeLineClick} @@ -458,7 +455,7 @@ export const FileOperationToolCard: React.FC = ({ modifiedContent={newStringContent} filePath={currentFilePath} maxHeight={300} - showLineNumbers={true} + showLineNumbers={false} lineNumberMode="dual" showPrefix={false} contextLines={-1} @@ -478,7 +475,7 @@ export const FileOperationToolCard: React.FC = ({ content={contentPreview} filePath={currentFilePath} isStreaming={isParamsStreaming} - showLineNumbers={true} + showLineNumbers={false} maxHeight={300} autoScrollToBottom={true} onLineClick={handleCodeLineClick} @@ -497,7 +494,7 @@ export const FileOperationToolCard: React.FC = ({ modifiedContent={contentPreview} filePath={currentFilePath} maxHeight={300} - showLineNumbers={true} + showLineNumbers={false} lineNumberMode="single" showPrefix={true} contextLines={-1} diff --git a/src/web-ui/src/flow_chat/tool-cards/GetFileDiffDisplay.tsx b/src/web-ui/src/flow_chat/tool-cards/GetFileDiffDisplay.tsx index 71d35932..70158fb8 100644 --- a/src/web-ui/src/flow_chat/tool-cards/GetFileDiffDisplay.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/GetFileDiffDisplay.tsx @@ -3,7 +3,7 @@ */ import React, { useMemo, useState, useCallback } from 'react'; -import { CheckCircle, ChevronDown, ChevronUp, GitCompare } from 'lucide-react'; +import { ChevronDown, ChevronUp, GitCompare } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { CubeLoading } from '../../component-library'; import type { ToolCardProps } from '../types/flow-chat'; @@ -56,9 +56,6 @@ export const GetFileDiffDisplay: React.FC = React.memo(({ if (status === 'running' || status === 'streaming' || status === 'preparing') { return ; } - if (status === 'completed') { - return ; - } return null; }; diff --git a/src/web-ui/src/flow_chat/tool-cards/GitToolDisplay.tsx b/src/web-ui/src/flow_chat/tool-cards/GitToolDisplay.tsx index 94eaa9dc..34f0bac0 100644 --- a/src/web-ui/src/flow_chat/tool-cards/GitToolDisplay.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/GitToolDisplay.tsx @@ -4,7 +4,7 @@ import React, { useState, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -import { ChevronDown, ChevronUp, GitBranch, CheckCircle, Check, X, AlertTriangle } from 'lucide-react'; +import { ChevronDown, ChevronUp, GitBranch, Check, X, AlertTriangle } from 'lucide-react'; import { CubeLoading, IconButton } from '../../component-library'; import type { ToolCardProps } from '../types/flow-chat'; import { BaseToolCard, ToolCardHeader } from './BaseToolCard'; @@ -174,11 +174,8 @@ export const GitToolDisplay: React.FC = ({ if (isLoading) { return ; } - if (status === 'completed' && !isFailed) { - if (hasWarning) { - return ; - } - return ; + if (status === 'completed' && !isFailed && hasWarning) { + return ; } return null; }; diff --git a/src/web-ui/src/flow_chat/tool-cards/GlobSearchDisplay.tsx b/src/web-ui/src/flow_chat/tool-cards/GlobSearchDisplay.tsx index 60a6d39b..ac9f7eb7 100644 --- a/src/web-ui/src/flow_chat/tool-cards/GlobSearchDisplay.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/GlobSearchDisplay.tsx @@ -3,7 +3,7 @@ */ import React, { useState, useMemo } from 'react'; -import { Loader2, CheckCircle, Clock, File, Folder } from 'lucide-react'; +import { Loader2, Clock, File, Folder, Check } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import type { ToolCardProps } from '../types/flow-chat'; import { CompactToolCard, CompactToolCardHeader } from './CompactToolCard'; @@ -25,7 +25,7 @@ export const GlobSearchDisplay: React.FC = ({ case 'streaming': return ; case 'completed': - return ; + return ; default: return ; } diff --git a/src/web-ui/src/flow_chat/tool-cards/GrepSearchDisplay.tsx b/src/web-ui/src/flow_chat/tool-cards/GrepSearchDisplay.tsx index 3dce57a2..f9fccf7a 100644 --- a/src/web-ui/src/flow_chat/tool-cards/GrepSearchDisplay.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/GrepSearchDisplay.tsx @@ -3,7 +3,7 @@ */ import React, { useState, useMemo } from 'react'; -import { Loader2, CheckCircle, Clock } from 'lucide-react'; +import { Loader2, Clock, Check } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import type { ToolCardProps } from '../types/flow-chat'; import { CompactToolCard, CompactToolCardHeader } from './CompactToolCard'; @@ -25,7 +25,7 @@ export const GrepSearchDisplay: React.FC = ({ case 'streaming': return ; case 'completed': - return ; + return ; default: return ; } diff --git a/src/web-ui/src/flow_chat/tool-cards/IdeControlToolCard.tsx b/src/web-ui/src/flow_chat/tool-cards/IdeControlToolCard.tsx index b31e9dae..99191e0e 100644 --- a/src/web-ui/src/flow_chat/tool-cards/IdeControlToolCard.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/IdeControlToolCard.tsx @@ -5,7 +5,7 @@ */ import React, { useState, useMemo } from 'react'; -import { Loader2, CheckCircle, Settings, Clock } from 'lucide-react'; +import { Loader2, Settings, Clock, Check } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import type { ToolCardProps } from '../types/flow-chat'; import { CompactToolCard, CompactToolCardHeader } from './CompactToolCard'; @@ -25,7 +25,7 @@ export const IdeControlToolCard: React.FC = ({ case 'streaming': return ; case 'completed': - return ; + return ; case 'pending': default: return ; diff --git a/src/web-ui/src/flow_chat/tool-cards/ImageAnalysisCard.tsx b/src/web-ui/src/flow_chat/tool-cards/ImageAnalysisCard.tsx index 814a498f..a286ee2e 100644 --- a/src/web-ui/src/flow_chat/tool-cards/ImageAnalysisCard.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/ImageAnalysisCard.tsx @@ -4,7 +4,7 @@ */ import React, { useState, useMemo } from 'react'; -import { Loader2, CheckCircle, Clock } from 'lucide-react'; +import { Loader2, Clock, Check } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import type { ToolCardProps } from '../types/flow-chat'; import { CompactToolCard, CompactToolCardHeader } from './CompactToolCard'; @@ -24,7 +24,7 @@ export const ImageAnalysisCard: React.FC = ({ case 'streaming': return ; case 'completed': - return ; + return ; case 'pending': default: return ; diff --git a/src/web-ui/src/flow_chat/tool-cards/LSDisplay.tsx b/src/web-ui/src/flow_chat/tool-cards/LSDisplay.tsx index 71fc6c1c..f26a4b51 100644 --- a/src/web-ui/src/flow_chat/tool-cards/LSDisplay.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/LSDisplay.tsx @@ -3,7 +3,7 @@ */ import React, { useState, useMemo } from 'react'; -import { Loader2, CheckCircle, Clock, File, Folder } from 'lucide-react'; +import { Loader2, Clock, File, Folder, Check } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import type { ToolCardProps } from '../types/flow-chat'; import { CompactToolCard, CompactToolCardHeader } from './CompactToolCard'; @@ -32,7 +32,7 @@ export const LSDisplay: React.FC = ({ case 'streaming': return ; case 'completed': - return ; + return ; default: return ; } diff --git a/src/web-ui/src/flow_chat/tool-cards/LinterToolCard.tsx b/src/web-ui/src/flow_chat/tool-cards/LinterToolCard.tsx index 5951f209..1cf24b07 100644 --- a/src/web-ui/src/flow_chat/tool-cards/LinterToolCard.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/LinterToolCard.tsx @@ -4,7 +4,7 @@ */ import React, { useState, useMemo } from 'react'; -import { Loader2, CheckCircle, XCircle, AlertTriangle, Clock } from 'lucide-react'; +import { Loader2, CheckCircle, XCircle, AlertTriangle, Clock, Check } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import type { ToolCardProps } from '../types/flow-chat'; import { CompactToolCard, CompactToolCardHeader } from './CompactToolCard'; @@ -69,7 +69,7 @@ export const LinterToolCard: React.FC = React.memo(({ case 'streaming': return ; case 'completed': - return ; + return ; case 'pending': default: return ; diff --git a/src/web-ui/src/flow_chat/tool-cards/MCPToolDisplay.tsx b/src/web-ui/src/flow_chat/tool-cards/MCPToolDisplay.tsx index a19e892c..6f965f82 100644 --- a/src/web-ui/src/flow_chat/tool-cards/MCPToolDisplay.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/MCPToolDisplay.tsx @@ -4,7 +4,7 @@ */ import React, { useState, useCallback, useEffect, useLayoutEffect, useRef } from 'react'; -import { ChevronDown, ChevronUp, Package, CheckCircle, Check, X } from 'lucide-react'; +import { ChevronDown, ChevronUp, Package, Check, X } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { CubeLoading, IconButton } from '../../component-library'; import type { ToolCardProps } from '../types/flow-chat'; @@ -571,9 +571,6 @@ export const MCPToolDisplay: React.FC = ({ if (isLoading) { return ; } - if (status === 'completed' && !isFailed) { - return ; - } return null; }; diff --git a/src/web-ui/src/flow_chat/tool-cards/MermaidInteractiveDisplay.tsx b/src/web-ui/src/flow_chat/tool-cards/MermaidInteractiveDisplay.tsx index 196ccd13..f5bd35a3 100644 --- a/src/web-ui/src/flow_chat/tool-cards/MermaidInteractiveDisplay.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/MermaidInteractiveDisplay.tsx @@ -3,7 +3,7 @@ */ import React, { useCallback } from 'react'; -import { CheckCircle, Eye, Network } from 'lucide-react'; +import { Eye, Network } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { CubeLoading } from '../../component-library'; import type { ToolCardProps, FlowToolItem } from '../types/flow-chat'; @@ -178,9 +178,6 @@ export const MermaidInteractiveDisplay: React.FC = ({ if (isLoading) { return ; } - if (status === 'completed') { - return ; - } return null; }; diff --git a/src/web-ui/src/flow_chat/tool-cards/ReadFileDisplay.tsx b/src/web-ui/src/flow_chat/tool-cards/ReadFileDisplay.tsx index e9029fae..e2b186f0 100644 --- a/src/web-ui/src/flow_chat/tool-cards/ReadFileDisplay.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/ReadFileDisplay.tsx @@ -3,7 +3,7 @@ */ import React, { useMemo } from 'react'; -import { Loader2, CheckCircle, Clock } from 'lucide-react'; +import { Loader2, Clock, Check } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import type { ToolCardProps } from '../types/flow-chat'; import { CompactToolCard, CompactToolCardHeader } from './CompactToolCard'; @@ -24,7 +24,7 @@ export const ReadFileDisplay: React.FC = React.memo(({ case 'streaming': return ; case 'completed': - return ; + return ; case 'pending': default: return ; diff --git a/src/web-ui/src/flow_chat/tool-cards/SkillDisplay.tsx b/src/web-ui/src/flow_chat/tool-cards/SkillDisplay.tsx index 84791848..d729b93a 100644 --- a/src/web-ui/src/flow_chat/tool-cards/SkillDisplay.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/SkillDisplay.tsx @@ -4,7 +4,7 @@ */ import React from 'react'; -import { Sparkles, CheckCircle } from 'lucide-react'; +import { Sparkles } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { CubeLoading } from '../../component-library'; import type { ToolCardProps } from '../types/flow-chat'; @@ -61,9 +61,6 @@ export const SkillDisplay: React.FC = ({ if (isLoading) { return ; } - if (status === 'completed' && !isFailed) { - return ; - } return null; }; diff --git a/src/web-ui/src/flow_chat/tool-cards/TaskToolDisplay.tsx b/src/web-ui/src/flow_chat/tool-cards/TaskToolDisplay.tsx index 82e7c2dc..93bdecf9 100644 --- a/src/web-ui/src/flow_chat/tool-cards/TaskToolDisplay.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/TaskToolDisplay.tsx @@ -8,7 +8,6 @@ import { ChevronUp, Split, Timer, - CheckCircle, PanelRightOpen } from 'lucide-react'; import { useTranslation } from 'react-i18next'; @@ -160,9 +159,6 @@ export const TaskToolDisplay: React.FC = ({ if (isRunning) { return ; } - if (status === 'completed' && !isFailed) { - return ; - } return null; }; diff --git a/src/web-ui/src/flow_chat/tool-cards/TerminalToolCard.tsx b/src/web-ui/src/flow_chat/tool-cards/TerminalToolCard.tsx index b60427ab..1f8a8add 100644 --- a/src/web-ui/src/flow_chat/tool-cards/TerminalToolCard.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/TerminalToolCard.tsx @@ -15,7 +15,7 @@ import React, { useState, useRef, useCallback, useEffect, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import type { ToolCardProps } from '../types/flow-chat'; -import { Terminal, Play, X, ExternalLink, Square, ChevronDown, ChevronUp, CheckCircle } from 'lucide-react'; +import { Terminal, Play, X, ExternalLink, Square, ChevronDown, ChevronUp } from 'lucide-react'; import { createTerminalTab } from '@/shared/utils/tabUtils'; import { BaseToolCard, ToolCardHeader } from './BaseToolCard'; import { CubeLoading, IconButton, Input } from '../../component-library'; @@ -295,9 +295,6 @@ export const TerminalToolCard: React.FC = ({ if (isLoading) { return ; } - if (status === 'completed' && !isFailed) { - return ; - } return null; }; diff --git a/src/web-ui/src/flow_chat/tool-cards/WebSearchCard.tsx b/src/web-ui/src/flow_chat/tool-cards/WebSearchCard.tsx index ad7ff227..2917fcdb 100644 --- a/src/web-ui/src/flow_chat/tool-cards/WebSearchCard.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/WebSearchCard.tsx @@ -3,7 +3,7 @@ */ import React, { useState, useMemo } from 'react'; -import { Loader2, CheckCircle, Link, Clock } from 'lucide-react'; +import { Loader2, Link, Clock, Check } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import type { ToolCardProps } from '../types/flow-chat'; import { systemAPI } from '../../infrastructure/api'; @@ -28,7 +28,7 @@ export const WebSearchCard: React.FC = ({ case 'preparing': return ; case 'completed': - return ; + return ; default: return ; } diff --git a/src/web-ui/src/infrastructure/config/components/PromptTemplateConfig.tsx b/src/web-ui/src/infrastructure/config/components/PromptTemplateConfig.tsx index 5b457737..16d836d0 100644 --- a/src/web-ui/src/infrastructure/config/components/PromptTemplateConfig.tsx +++ b/src/web-ui/src/infrastructure/config/components/PromptTemplateConfig.tsx @@ -212,7 +212,6 @@ export const PromptTemplateConfig: React.FC = () => { > {sortedTemplates.length === 0 && (
-

{t('empty.createFirst')}