diff --git a/README.md b/README.md index 2b5e40b9..fc97f210 100644 --- a/README.md +++ b/README.md @@ -17,16 +17,32 @@ ## A Word Up Front -In the age of AI, we believe human–machine collaboration needs a fundamentally new form of software. Today, the programming domain is the most mature starting point for that exploration. +In the age of AI, true human–machine collaboration isn't just a ChatBox — it's a partner that knows you, accompanies you, and gets things done for you anywhere, anytime. That's where BitFun's exploration begins. ## What Is BitFun -BitFun is an Agentic Development Environment (ADE). While featuring a cutting-edge Code Agent system, we are more committed to deeply exploring and defining human–machine collaboration patterns, built with Rust + TypeScript for an ultra-lightweight and fluid experience. +BitFun is a next-generation Agent system built around the idea of **"AI assistants with personality and memory"**. + +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. + +Built with **Rust + TypeScript** for an ultra-lightweight, fluid, cross-platform experience. ![BitFun](./png/first_screen_screenshot.png) +### Agent System -### Working Modes +| Agent | Role | Core Capabilities | +|---|---|---| +| **Personal Assistant (WIP🚧)** (default) | Your dedicated AI companion | Long-term memory, personality settings, cross-scenario orchestration, continuous growth | +| **Code Agent** | Coding assistant | Conversation-driven coding, multi-mode task execution, autonomous read / edit / run / verify | +| **Cowork Agent** | Knowledge work assistant | File management, document generation, report organization, autonomous multi-step task execution | +| **Custom Agent** | Domain specialist | Quickly define a domain-specific Agent with Markdown | + +### Code Agent Working Modes + +Code Agent is built for software development, offering multiple modes that cover the full cycle from day-to-day coding to deep debugging, with deep integration into MCP, Skills, and Rules: | Mode | Scenario | Characteristics | |------|----------|-----------------| @@ -34,15 +50,30 @@ BitFun is an Agentic Development Environment (ADE). While featuring a cutting-ed | **Plan** | Complex tasks | Plan first, then execute; align on critical changes upfront. | | **Debug** | Hard problems | Instrument & trace → compare paths → root-cause analysis → verify fix. | | **Review** | Code review | Review code based on key repository conventions. | ---- +### Cowork Agent Workflow + +Cowork Agent is designed for everyday work, following a "clarify first, execute next, stay trackable" collaboration principle, with built-in office Skills and access to the Skill marketplace: + +| Skill | Trigger | Core Capabilities | +|---|---|---| +| **PDF** | Working with .pdf files | Read/extract text & tables, merge/split/rotate, watermark, fill forms, encrypt/decrypt, OCR scanned PDFs | +| **DOCX** | Create or edit Word documents | Create/edit .docx, styles/TOC/headers & footers, image insertion, comments & tracked changes | +| **XLSX** | Working with spreadsheets | Create/analyze .xlsx/.csv, formulas & formatting, financial model standards (color coding, formula validation) | +| **PPTX** | Build presentations | Create/edit .pptx from scratch, visual design guidelines, automated visual QA | +| **agent-browser** | Browser interaction needed | Browser automation: open pages, click/fill forms, screenshot, scrape data, web app testing | +| **skill-creator** | Creating a custom Skill | Guides authoring new Skills to extend the Agent's domain-specific capabilities | +| **find-skills** | Looking for ready-made capabilities | Discover and install community-contributed reusable Skills from the Skill marketplace | + +--- ### Extensibility -- **MCP Protocol**: Extend with external tools and resources via MCP servers. +- **MCP Protocol**: Extend with external tools and resources via MCP servers; supports MCP Apps. - **Skills**: Markdown/script-based capability packages that teach the Agent specific tasks (auto-reads Cursor, Claude Code, Codex configs). -- **Agent Customization**: Quickly define specialized Agents with Markdown. -- **Rules**: Quickly customize professional Agents via Markdown (auto-reads Cursor configs). +- **Agent Customization**: Quickly define a specialized Agent's personality, memory scope, and capabilities with Markdown. +- **Rules**: Project/global-level convention injection; auto-reads Cursor and other mainstream tool configs. +- **Hooks (WIP🚧)**: Inject deterministic automation logic at key task milestones. --- @@ -78,13 +109,15 @@ For more details, see the [Contributing Guide](./CONTRIBUTING.md). ## Platform Support -The project uses a Rust + TypeScript tech stack, supporting cross-platform and multi-form-factor reuse. +The project uses a Rust + TypeScript tech stack, supporting cross-platform and multi-form-factor reuse — keeping your Agent assistant always online and reachable everywhere. + | Form Factor | Supported Platforms | Status | |-------------|---------------------|--------| | **Desktop** (Tauri) | Windows, macOS | ✅ Supported | | **CLI** | Windows, macOS, Linux | 🚧 In Development | | **Server** | - | 🚧 In Development | -| **Mobile** | - | 🚧 In Development | +| **Mobile** (Native App) | iOS, Android | 🚧 In Development | +| **Social Platform Integration** | WeChat, Telegram, WhatsApp, Discord, etc. | 🚧 In Development | diff --git a/README.zh-CN.md b/README.zh-CN.md index 8228c348..72e708f8 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -17,15 +17,32 @@ ## 写在前面的话 -AI时代,我们相信人机协同需要一种全新的软件形态。而当下,编程领域是这一探索最成熟的起点。 +AI 时代,真正的人机协同不是简单的ChatBox,而是一个懂你、陪你并且随时随地替你做事的伙伴。BitFun 的探索,从这里开始。 ## 什么是 BitFun -BitFun 是一款代理式开发环境(ADE,Agentic Development Environment),在拥有前沿Code Agent体系的同时我们更希望深度探索并定义人机协作方式,并以Rust + TypeScript希望提供极致轻巧、流畅的体验。 +BitFun 是一个以 **"有个性、有记忆的 AI 助理"** 为核心的新一代 Agent 系统。 + +每一位用户都拥有属于自己的 Agent 助理——它记得你的习惯与偏好,拥有独特的性格设定,并随时间持续成长。在这个助理之上,BitFun 默认内置了 **Code Agent**(代码代理)与 **Cowork Agent**(桌面端工作助理)两种专业能力,并提供统一的扩展机制供用户按需定制更多 Agent 角色。 + +你的助理不只存在于桌面——可以通过多种媒体方式联系,比如通过微信、Telegram、WhatsApp 等社交平台,都可以随时随地向它下达指令,任务在后台持续推进,你只需在方便时查看进度或给出反馈。 + +以 **Rust + TypeScript** 构建,追求极致轻量与流畅的跨平台体验。 ![BitFun](./png/first_screen_screenshot-zh-CN.png) -### 工作模式 +### Agent 体系 + +| Agent | 定位 | 核心能力 | +|---|---|---| +| **个人助理(WIP🚧)**(默认) | 你专属的 AI 伙伴 | 长期记忆、个性设定、跨场景调度、持续成长 | +| **Code Agent** | 代码代理 | 对话驱动编码,多模式任务执行,自主读/改/跑/验证 | +| **Cowork Agent** | 知识工作代理 | 文件管理、文档生成、报告整理、多步任务自主执行 | +| **自定义 Agent** | 垂域专家 | 通过 Markdown 快速定义专属领域 Agent | + +### Code Agent 工作模式 + +Code Agent 专为软件开发设计,支持多种工作模式覆盖从日常编码到疑难排查的全流程,并深度集成 MCP、Skills、Rules 等扩展体系: | 模式 | 场景 | 特点 | |---|---|---| @@ -33,16 +50,31 @@ BitFun 是一款代理式开发环境(ADE,Agentic Development Environment) | **Plan** | 复杂任务 | 先规划后执行,关键改动点提前对齐 | | **Debug** | 疑难问题 | 插桩取证 → 路径对比 → 根因定位 → 验证修复 | | **Review** | 代码审查 | 基于仓库关键规范进行代码审查 | ---- +### Cowork Agent 工作方式 + +Cowork Agent 专为日常工作设计,遵循"先澄清、再执行、可追踪"的协作原则,内置多个常用办公Skill,并对接skill市场: + +| Skill | 触发场景 | 核心能力 | +|---|---|---| +| **PDF** | 处理 .pdf 文件 | 读取/提取文本与表格、合并/拆分/旋转、添加水印、填写表单、加密解密、OCR 扫描版 | +| **DOCX** | 创建或编辑 Word 文档 | 创建/编辑 .docx、样式/目录/页眉页脚、插入图片、批注与追踪修订 | +| **XLSX** | 处理电子表格 | 创建/分析 .xlsx/.csv,公式与格式化,财务模型规范(颜色编码、公式校验) | +| **PPTX** | 制作演示文稿 | 从零创建/编辑 .pptx,视觉设计规范,自动 QA 视觉检查 | +| **agent-browser** | 需要操控网页 | 浏览器自动化:打开网页、点击/填表、截图、抓取数据、Web 测试 | +| **skill-creator** | 创建自定义 Skill | 引导创作新 Skill,扩展 Agent 的专业能力范围 | +| **find-skills** | 寻找现成能力包 | 从 Skill 市场发现并安装社区贡献的可复用 Skill | + +--- ### 扩展能力 -- **MCP 协议**:通过 MCP 服务器扩展外部工具与资源 -- **Skills**:Markdown/脚本等能力包,教 Agent 完成特定任务(自动读取Cursor、Claude Code、Codex等配置) -- **Agent 自定义**:通过 Markdown 快速自定义专业 Agent -- **Rules**:通过 Markdown 快速自定义专业 Agent(自动读取Cursor配置) +- **MCP 协议**:通过 MCP 服务器扩展外部工具与资源,支持MCP APP +- **Skills**:Markdown/脚本等能力包,教 Agent 完成特定任务(自动读取 Cursor、Claude Code、Codex 等配置) +- **Agent 自定义**:通过 Markdown 快速定义专属 Agent 的性格、记忆与能力范围 +- **Rules**:项目/全局级规范注入,自动读取 Cursor 等主流工具配置 +- **Hooks(WIP🚧)**:在任务关键节点注入确定性自动化逻辑 --- @@ -78,13 +110,15 @@ npm run desktop:build ## 平台支持 -项目采用 Rust + TypeScript 技术栈,支持跨平台和多形态复用。 +项目采用 Rust + TypeScript 技术栈,支持跨平台和多形态复用,确保你的 Agent 助理随时在线、随处可达。 + | 形态 | 支持平台 | 状态 | |------|----------|------| -| **Desktop**(Tauri) | Windows、macOS| ✅ 已支持 | +| **Desktop**(Tauri) | Windows、macOS | ✅ 已支持 | | **CLI** | Windows、macOS、Linux | 🚧 开发中 | | **Server** | - | 🚧 开发中 | -| **手机端** | - | 🚧 开发中 | +| **手机端**(独立 App) | iOS、Android | 🚧 开发中 | +| **社交平台接入** | 微信、Telegram、WhatsApp、Discord 等 | 🚧 开发中 | diff --git a/png/first_screen_screenshot-zh-CN.png b/png/first_screen_screenshot-zh-CN.png index a902fab1..6bc8131a 100644 Binary files a/png/first_screen_screenshot-zh-CN.png and b/png/first_screen_screenshot-zh-CN.png differ diff --git a/png/first_screen_screenshot.png b/png/first_screen_screenshot.png index eb1bae56..8931e01a 100644 Binary files a/png/first_screen_screenshot.png and b/png/first_screen_screenshot.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 518e4057..0cc04397 100644 --- a/src/web-ui/src/app/components/NavPanel/MainNav.tsx +++ b/src/web-ui/src/app/components/NavPanel/MainNav.tsx @@ -11,7 +11,7 @@ * the outer Grid accordion handles the actual height collapse. */ -import React, { useCallback, useState, useMemo } from 'react'; +import React, { useCallback, useState, useMemo, useEffect } from 'react'; import { Plus } from 'lucide-react'; import { useApp } from '../../hooks/useApp'; import { useSceneManager } from '../../hooks/useSceneManager'; @@ -33,7 +33,10 @@ import SkillsSection from './sections/skills/SkillsSection'; import WorkspaceHeader from './components/WorkspaceHeader'; import { useSceneStore } from '../../stores/sceneStore'; import { flowChatManager } from '@/flow_chat/services/FlowChatManager'; +import { configManager } from '@/infrastructure/config/services/ConfigManager'; import { createLogger } from '@/shared/utils/logger'; + +const DEFAULT_MODE_CONFIG_KEY = 'app.session_config.default_mode'; import './NavPanel.scss'; const log = createLogger('MainNav'); @@ -184,6 +187,21 @@ const MainNav: React.FC = ({ const sessionMode = useSessionModeStore(s => s.mode); + const [defaultSessionMode, setDefaultSessionMode] = useState<'code' | 'cowork'>('code'); + + useEffect(() => { + configManager.getConfig<'code' | 'cowork'>(DEFAULT_MODE_CONFIG_KEY).then(mode => { + if (mode === 'code' || mode === 'cowork') setDefaultSessionMode(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); + }).catch(() => {}); + }); + return () => unwatch(); + }, []); + const handleCreateSession = useCallback(async () => { openScene('session'); switchLeftPanelTab('sessions'); @@ -253,7 +271,7 @@ const MainNav: React.FC = ({ isOpen={isOpen} onClick={() => handleItemClick(tab, item)} actionIcon={tab === 'sessions' ? Plus : undefined} - actionTitle={tab === 'sessions' ? t('nav.sessions.newSession') : undefined} + actionTitle={tab === 'sessions' ? (defaultSessionMode === 'cowork' ? t('nav.sessions.newCoworkSession') : t('nav.sessions.newCodeSession')) : undefined} onActionClick={tab === 'sessions' ? handleCreateSession : undefined} /> diff --git a/src/web-ui/src/app/components/NavPanel/components/NavItem.tsx b/src/web-ui/src/app/components/NavPanel/components/NavItem.tsx index 93f741fd..d03eef96 100644 --- a/src/web-ui/src/app/components/NavPanel/components/NavItem.tsx +++ b/src/web-ui/src/app/components/NavPanel/components/NavItem.tsx @@ -63,7 +63,7 @@ const NavItem: React.FC = ({ onActionClick?.(); }; - const button = ( + return ( ); - - const tooltipText = tooltipContent || displayLabel; - - return ( - - {button} - - ); }; export default NavItem; diff --git a/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.scss b/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.scss index 6af75ba3..5326b055 100644 --- a/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.scss +++ b/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.scss @@ -28,7 +28,8 @@ &__inline-action { display: flex; align-items: center; - gap: 5px; + justify-content: center; + gap: 4px; height: 24px; padding: 0 $size-gap-1; border: 1px dashed var(--border-subtle); @@ -43,12 +44,6 @@ background $motion-fast $easing-standard, border-color $motion-fast $easing-standard; - &:hover { - color: var(--color-primary); - background: color-mix(in srgb, var(--color-primary) 8%, transparent); - border-color: var(--color-primary); - } - svg { flex-shrink: 0; } span { @@ -56,57 +51,17 @@ text-overflow: ellipsis; white-space: nowrap; } - } - // Mode chips container - &__mode-switcher { - display: flex; - align-items: center; - gap: 3px; - flex-shrink: 0; - } - - // Individual mode chip with bouncy spring animation - &__mode-chip { - display: flex; - align-items: center; - justify-content: center; - width: 20px; - height: 20px; - border-radius: 50%; - border: none; - background: none; - cursor: pointer; - flex-shrink: 0; - transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1), - color $motion-fast $easing-standard, - background $motion-fast $easing-standard; - - &.is-active { - order: -1; - transform: scale(1); - color: var(--color-accent-500); - background: var(--color-accent-200); - } - - &:not(.is-active) { - transform: scale(0.78); - color: var(--color-text-muted); - background: none; - } - - &:hover:not(.is-active) { - transform: scale(0.88); - color: var(--color-text-secondary); - background: var(--element-bg-soft); - } - - &:active { - transform: scale(0.92); + &.is-code:hover { + color: var(--color-primary); + background: color-mix(in srgb, var(--color-primary) 8%, transparent); + border-color: var(--color-primary); } - &.is-placeholder:not(.is-active) { - opacity: 0.55; + &.is-cowork:hover { + color: var(--color-accent-500); + background: color-mix(in srgb, var(--color-accent-500) 8%, transparent); + border-color: var(--color-accent-500); } } diff --git a/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.tsx b/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.tsx index ee31537d..2e0872f0 100644 --- a/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.tsx +++ b/src/web-ui/src/app/components/NavPanel/sections/sessions/SessionsSection.tsx @@ -13,7 +13,6 @@ import { flowChatStore } from '../../../../../flow_chat/store/FlowChatStore'; import { flowChatManager } from '../../../../../flow_chat/services/FlowChatManager'; import type { FlowChatState, Session } from '../../../../../flow_chat/types/flow-chat'; import { useSceneStore } from '../../../../stores/sceneStore'; -import { useSessionModeStore } from '../../../../stores/sessionModeStore'; import type { SessionMode } from '../../../../stores/sessionModeStore'; import { useApp } from '../../../../hooks/useApp'; import type { SceneTabId } from '../../../SceneBar/types'; @@ -24,11 +23,6 @@ const MAX_VISIBLE_SESSIONS = 8; const log = createLogger('SessionsSection'); const AGENT_SCENE: SceneTabId = 'session'; -const SESSION_MODES: { key: SessionMode; Icon: typeof Code2; labelKey: string }[] = [ - { key: 'code', Icon: Code2, labelKey: 'nav.sessions.modeCode' }, - { key: 'cowork', Icon: Users, labelKey: 'nav.sessions.modeCowork' }, -]; - const resolveSessionMode = (session: Session): SessionMode => { return session.mode?.toLowerCase() === 'cowork' ? 'cowork' : 'code'; }; @@ -40,8 +34,6 @@ const SessionsSection: React.FC = () => { const { t } = useI18n('common'); const { switchLeftPanelTab } = useApp(); const openScene = useSceneStore(s => s.openScene); - const sessionMode = useSessionModeStore(s => s.mode); - const setSessionMode = useSessionModeStore(s => s.setMode); const activeTabId = useSceneStore(s => s.activeTabId); const [flowChatState, setFlowChatState] = useState(() => flowChatStore.getState() @@ -98,22 +90,18 @@ const SessionsSection: React.FC = () => { [activeSessionId, openScene, switchLeftPanelTab, editingSessionId] ); - const handleCreate = useCallback(async () => { + const handleCreate = useCallback(async (mode: SessionMode) => { openScene('session'); switchLeftPanelTab('sessions'); try { await flowChatManager.createChatSession( { modelName: 'claude-sonnet-4.5' }, - sessionMode === 'cowork' ? 'Cowork' : 'agentic' + mode === 'cowork' ? 'Cowork' : 'agentic' ); } catch (err) { log.error('Failed to create session', err); } - }, [openScene, switchLeftPanelTab, sessionMode]); - - const handleModeSwitch = useCallback((mode: SessionMode) => { - setSessionMode(mode); - }, [setSessionMode]); + }, [openScene, switchLeftPanelTab]); const resolveSessionTitle = useCallback( (session: Session): string => { @@ -187,36 +175,26 @@ const SessionsSection: React.FC = () => { return (
- + + + + -
- {SESSION_MODES.map(({ key, Icon, labelKey }) => ( - - handleModeSwitch(key)} - > - - - - ))} -
{sessions.length === 0 ? ( @@ -229,7 +207,6 @@ const SessionsSection: React.FC = () => { const SessionIcon = sessionModeKey === 'cowork' ? Users : Code2; const row = (
= ({ const sessionTitle = useCurrentSessionTitle(); const settingsTabTitle = useCurrentSettingsTabTitle(); const { t } = useI18n('common'); + const [defaultMode, setDefaultMode] = useState<'code' | 'cowork'>('code'); + + useEffect(() => { + configManager.getConfig<'code' | 'cowork'>(DEFAULT_MODE_CONFIG_KEY).then(mode => { + if (mode === 'code' || mode === 'cowork') setDefaultMode(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') setDefaultMode(mode); + }).catch(() => {}); + }); + return () => unwatch(); + }, []); + const hasWindowControls = !!(onMinimize && onMaximize && onClose); const sceneBarClassName = `bitfun-scene-bar ${!hasWindowControls ? 'bitfun-scene-bar--no-controls' : ''} ${className}`.trim(); const isSingleTab = openTabs.length <= 1; @@ -105,6 +122,7 @@ const SceneBar: React.FC = ({ const subtitle = (tab.id === 'session' && sessionTitle ? sessionTitle : undefined) ?? (tab.id === 'settings' && settingsTabTitle ? settingsTabTitle : undefined); + const actionTitle = tab.id === 'session' ? (defaultMode === 'cowork' ? t('nav.sessions.newCoworkSession') : t('nav.sessions.newCodeSession')) : undefined; return ( = ({ isActive={tab.id === activeTabId} subtitle={subtitle} onActionClick={tab.id === 'session' ? handleCreateSession : undefined} - actionTitle={tab.id === 'session' ? t('nav.sections.sessions.newSession') : undefined} + actionTitle={actionTitle} onActivate={activateScene} onClose={closeScene} /> diff --git a/src/web-ui/src/app/components/SceneBar/SceneTab.tsx b/src/web-ui/src/app/components/SceneBar/SceneTab.tsx index e2cf4d07..0988dd8f 100644 --- a/src/web-ui/src/app/components/SceneBar/SceneTab.tsx +++ b/src/web-ui/src/app/components/SceneBar/SceneTab.tsx @@ -9,6 +9,7 @@ import React, { useCallback } from 'react'; import { Plus, X } from 'lucide-react'; +import { Tooltip } from '@/component-library'; import type { SceneTab as SceneTabType, SceneTabDef } from './types'; interface SceneTabProps { @@ -82,17 +83,18 @@ const SceneTab: React.FC = ({ )} {onActionClick && ( - e.stopPropagation()} - role="button" - tabIndex={-1} - aria-label={actionTitle ?? ''} - title={actionTitle ?? ''} - > - + + e.stopPropagation()} + role="button" + tabIndex={-1} + aria-label={actionTitle ?? ''} + > + + )}
diff --git a/src/web-ui/src/app/components/TitleBar/NotificationButton.tsx b/src/web-ui/src/app/components/TitleBar/NotificationButton.tsx index 899ac49b..b0179e8b 100644 --- a/src/web-ui/src/app/components/TitleBar/NotificationButton.tsx +++ b/src/web-ui/src/app/components/TitleBar/NotificationButton.tsx @@ -7,6 +7,8 @@ import React, { useRef, useEffect, useState } from 'react'; import { Bell, BellDot } from 'lucide-react'; +import { Tooltip } from '@/component-library'; +import { useI18n } from '@/infrastructure/i18n/hooks/useI18n'; import { useUnreadCount, useLatestTaskNotification, @@ -19,6 +21,7 @@ interface NotificationButtonProps { } const NotificationButton: React.FC = ({ className = '' }) => { + const { t } = useI18n('common'); const buttonRef = useRef(null); const [tooltipOffset, setTooltipOffset] = useState(0); @@ -41,6 +44,7 @@ const NotificationButton: React.FC = ({ className = '' }, [activeNotification]); return ( + + ); }; diff --git a/src/web-ui/src/app/scenes/settings/SettingsScene.tsx b/src/web-ui/src/app/scenes/settings/SettingsScene.tsx index 5bdf6a2a..539e14ee 100644 --- a/src/web-ui/src/app/scenes/settings/SettingsScene.tsx +++ b/src/web-ui/src/app/scenes/settings/SettingsScene.tsx @@ -11,9 +11,8 @@ import { useSettingsStore } from './settingsStore'; import './SettingsScene.scss'; const AIModelConfig = lazy(() => import('../../../infrastructure/config/components/AIModelConfig')); -const AIFeaturesConfig = lazy(() => import('../../../infrastructure/config/components/AIFeaturesConfig')); +const SessionConfig = lazy(() => import('../../../infrastructure/config/components/SessionConfig')); const AIRulesMemoryConfig = lazy(() => import('../../../infrastructure/config/components/AIRulesMemoryConfig')); -const AgentsConfig = lazy(() => import('../../../infrastructure/config/components/AgentsConfig')); const McpToolsConfig = lazy(() => import('../../../infrastructure/config/components/McpToolsConfig')); const LspConfig = lazy(() => import('../../../infrastructure/config/components/LspConfig')); const DebugConfig = lazy(() => import('../../../infrastructure/config/components/DebugConfig')); @@ -31,11 +30,10 @@ const SettingsScene: React.FC = () => { switch (activeTab) { case 'theme': Content = ThemeConfigComponent; break; case 'models': Content = AIModelConfig; break; - case 'ai-features': Content = AIFeaturesConfig; break; + case 'session-config': Content = SessionConfig; break; case 'ai-context': Content = AIRulesMemoryConfig; break; case 'prompt-templates': Content = PromptTemplateConfig; break; case 'mcp-tools': Content = McpToolsConfig; break; - case 'agents': Content = AgentsConfig; break; case 'lsp': Content = LspConfig; break; case 'debug': Content = DebugConfig; break; case 'logging': Content = LoggingConfig; break; diff --git a/src/web-ui/src/app/scenes/settings/settingsConfig.ts b/src/web-ui/src/app/scenes/settings/settingsConfig.ts index 128b838f..ae339b63 100644 --- a/src/web-ui/src/app/scenes/settings/settingsConfig.ts +++ b/src/web-ui/src/app/scenes/settings/settingsConfig.ts @@ -8,8 +8,7 @@ export type ConfigTab = | 'theme' | 'models' - | 'agents' - | 'ai-features' + | 'session-config' | 'ai-context' | 'prompt-templates' | 'mcp-tools' @@ -43,9 +42,8 @@ export const SETTINGS_CATEGORIES: ConfigCategoryDef[] = [ id: 'smartCapabilities', nameKey: 'configCenter.categories.smartCapabilities', tabs: [ - { id: 'ai-features', labelKey: 'configCenter.tabs.aiFeatures' }, + { id: 'session-config', labelKey: 'configCenter.tabs.sessionConfig' }, { id: 'prompt-templates', labelKey: 'configCenter.tabs.promptTemplates' }, - { id: 'agents', labelKey: 'configCenter.tabs.agents' }, { id: 'ai-context', labelKey: 'configCenter.tabs.aiContext' }, { id: 'mcp-tools', labelKey: 'configCenter.tabs.mcpTools' }, ], diff --git a/src/web-ui/src/app/scenes/team/TeamView.scss b/src/web-ui/src/app/scenes/team/TeamView.scss index 3cfbba36..ea8add84 100644 --- a/src/web-ui/src/app/scenes/team/TeamView.scss +++ b/src/web-ui/src/app/scenes/team/TeamView.scss @@ -1,11 +1,23 @@ @use '../../../component-library/styles/tokens' as *; +// ─── Page enter animations ──────────────────────────────────────────────────── + +@keyframes tv-page-reveal { + from { opacity: 0; } + to { opacity: 1; } +} + +@media (prefers-reduced-motion: reduce) { + .tv { animation: none !important; } +} + // ─── Main layout ────────────────────────────────────────────────────────────── .tv { display: flex; flex-direction: column; height: 100%; overflow: hidden; + animation: tv-page-reveal 0.32s $easing-decelerate both; // ── Editor bar (back button) ───────────────────────────────────────────── &__editor-bar { diff --git a/src/web-ui/src/app/scenes/team/TeamView.tsx b/src/web-ui/src/app/scenes/team/TeamView.tsx index f90bacd9..b84a0908 100644 --- a/src/web-ui/src/app/scenes/team/TeamView.tsx +++ b/src/web-ui/src/app/scenes/team/TeamView.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { ArrowLeft } from 'lucide-react'; import { useTeamStore } from './teamStore'; import AgentsOverviewPage from './components/AgentsOverviewPage'; +import CreateAgentPage from './components/CreateAgentPage'; import ExpertTeamsPage from './components/ExpertTeamsPage'; import TeamTabBar from './components/TeamTabBar'; import AgentGallery from './components/AgentGallery'; @@ -44,6 +45,7 @@ const TeamView: React.FC = () => { if (page === 'editor') return ; if (page === 'expertTeamsOverview') return ; + if (page === 'createAgent') return ; return ; }; diff --git a/src/web-ui/src/app/scenes/team/components/AgentsOverviewPage.tsx b/src/web-ui/src/app/scenes/team/components/AgentsOverviewPage.tsx index fdd07c5e..7cf0497a 100644 --- a/src/web-ui/src/app/scenes/team/components/AgentsOverviewPage.tsx +++ b/src/web-ui/src/app/scenes/team/components/AgentsOverviewPage.tsx @@ -1,5 +1,6 @@ import React, { useState, useCallback, useEffect } from 'react'; -import { Bot, Cpu, SlidersHorizontal } from 'lucide-react'; +import { Bot, SlidersHorizontal, Wrench, RotateCcw, Pencil, X, Plus } from 'lucide-react'; + import { useTranslation } from 'react-i18next'; import { Search, Switch, IconButton, Badge } from '@/component-library'; import { @@ -11,8 +12,19 @@ import { CAPABILITY_ACCENT } from '../teamIcons'; import { agentAPI } from '@/infrastructure/api/service-api/AgentAPI'; import { SubagentAPI } from '@/infrastructure/api/service-api/SubagentAPI'; import type { SubagentSource } from '@/infrastructure/api/service-api/SubagentAPI'; +import { configAPI } from '@/infrastructure/api/service-api/ConfigAPI'; +import type { ModeConfigItem } from '@/infrastructure/config/types'; +import { useNotification } from '@/shared/notification-system'; import './TeamHomePage.scss'; +// ─── Types ──────────────────────────────────────────────────────────────────── + +interface ToolInfo { + name: string; + description: string; + is_readonly: boolean; +} + // ─── Agent badge ────────────────────────────────────────────────────────────── interface AgentBadgeConfig { @@ -64,14 +76,23 @@ const AgentListItem: React.FC<{ soloEnabled: boolean; onToggleSolo: (agentId: string, enabled: boolean) => void; index: number; -}> = ({ agent, soloEnabled, onToggleSolo, index }) => { + availableTools: ToolInfo[]; + modeConfig: ModeConfigItem | null; + onToggleTool: (agentId: string, toolName: string) => Promise; + onResetTools: (agentId: string) => Promise; +}> = ({ agent, soloEnabled, onToggleSolo, index, availableTools, modeConfig, onToggleTool, onResetTools }) => { const { t } = useTranslation('scenes/team'); const [expanded, setExpanded] = useState(false); + const [toolsEditing, setToolsEditing] = useState(false); const toggleExpand = useCallback(() => setExpanded((v) => !v), []); const badge = getAgentBadge(agent.agentKind, agent.subagentSource); + const isMode = agent.agentKind === 'mode'; + const enabledTools = modeConfig?.available_tools ?? agent.defaultTools ?? []; + const totalTools = isMode ? availableTools.length : (agent.defaultTools?.length ?? 0); + return (
{agent.name} - - {agent.agentKind === 'mode' ? : } - {badge.label} - + {badge.label} {agent.model && ( {agent.model} )} @@ -125,6 +143,8 @@ const AgentListItem: React.FC<{ {expanded && (

{agent.description}

+ + {/* 能力评级 */}
{agent.capabilities.map((cap) => (
@@ -151,67 +171,255 @@ const AgentListItem: React.FC<{
))}
+ + {/* 工具管理区块 */} + {totalTools > 0 && ( +
+ {/* 标题行 */} +
+ + {t('agentsOverview.tools', '工具')} + + {isMode ? `${enabledTools.length}/${totalTools}` : totalTools} + + + {/* mode agent:编辑 / 取消 + 重置 */} + {isMode && ( +
e.stopPropagation()}> + {toolsEditing ? ( + <> + onResetTools(agent.id)} + > + + + setToolsEditing(false)} + > + + + + ) : ( + setToolsEditing(true)} + > + + + )} +
+ )} +
+ + {/* mode agent 编辑态:全部工具可点击切换 */} + {isMode && toolsEditing && ( +
e.stopPropagation()}> + {[...availableTools] + .sort((a, b) => { + const aOn = enabledTools.includes(a.name); + const bOn = enabledTools.includes(b.name); + if (aOn && !bOn) return -1; + if (!aOn && bOn) return 1; + return 0; + }) + .map((tool) => { + const isOn = enabledTools.includes(tool.name); + return ( + + ); + })} +
+ )} + + {/* mode agent 默认态:只显示已启用的工具 chip */} + {isMode && !toolsEditing && ( +
+ {enabledTools.map((tool) => ( + + {tool.replace(/_/g, ' ')} + + ))} +
+ )} + + {/* sub-agent:只读工具 chip */} + {!isMode && ( +
+ {(agent.defaultTools ?? []).map((tool) => ( + + {tool.replace(/_/g, ' ')} + + ))} +
+ )} +
+ )}
)}
); }; +// ─── Filter types ───────────────────────────────────────────────────────────── + +type FilterLevel = 'all' | 'builtin' | 'user' | 'project'; +type FilterType = 'all' | 'mode' | 'subagent'; + // ─── Page ───────────────────────────────────────────────────────────────────── const AgentsOverviewPage: React.FC = () => { const { t } = useTranslation('scenes/team'); - const { agentSoloEnabled, setAgentSoloEnabled } = useTeamStore(); + const { agentSoloEnabled, setAgentSoloEnabled, openCreateAgent } = useTeamStore(); + const notification = useNotification(); const [query, setQuery] = useState(''); + const [filterLevel, setFilterLevel] = useState('all'); + const [filterType, setFilterType] = useState('all'); const [allAgents, setAllAgents] = useState([]); const [loading, setLoading] = useState(true); + const [availableTools, setAvailableTools] = useState([]); + const [modeConfigs, setModeConfigs] = useState>({}); - useEffect(() => { - let cancelled = false; + const loadAgents = useCallback(async () => { setLoading(true); - - Promise.all([ - agentAPI.getAvailableModes().catch(() => []), - SubagentAPI.listSubagents().catch(() => []), - ]).then(([modes, subagents]) => { - if (cancelled) return; - + const fetchTools = async (): Promise => { + try { + const { invoke } = await import('@tauri-apps/api/core'); + return await invoke('get_all_tools_info'); + } catch { + return []; + } + }; + try { + const [modes, subagents, tools, configs] = await Promise.all([ + agentAPI.getAvailableModes().catch(() => []), + SubagentAPI.listSubagents().catch(() => []), + fetchTools(), + configAPI.getModeConfigs().catch(() => ({})), + ]); const modeAgents: AgentWithCapabilities[] = modes.map((m) => enrichCapabilities({ - id: m.id, - name: m.name, - description: m.description, - isReadonly: m.isReadonly, - toolCount: m.toolCount, - defaultTools: m.defaultTools ?? [], - enabled: m.enabled, - capabilities: [], - agentKind: 'mode', + id: m.id, name: m.name, description: m.description, + isReadonly: m.isReadonly, toolCount: m.toolCount, + defaultTools: m.defaultTools ?? [], enabled: m.enabled, + capabilities: [], agentKind: 'mode', }) ); - const subAgents: AgentWithCapabilities[] = subagents.map((s) => - enrichCapabilities({ - ...s, - capabilities: [], - agentKind: 'subagent', - }) + enrichCapabilities({ ...s, capabilities: [], agentKind: 'subagent' }) ); - setAllAgents([...modeAgents, ...subAgents]); - }).finally(() => { - if (!cancelled) setLoading(false); - }); - - return () => { cancelled = true; }; + setAvailableTools(tools); + setModeConfigs(configs as Record); + } finally { + setLoading(false); + } }, []); + 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; + const userConfig = modeConfigs[agentId]; + const defaultTools = agent.defaultTools ?? []; + if (!userConfig) { + return { mode_id: agentId, available_tools: defaultTools, enabled: true, default_tools: defaultTools }; + } + if (!userConfig.available_tools || userConfig.available_tools.length === 0) { + return { ...userConfig, available_tools: defaultTools, default_tools: defaultTools }; + } + return { ...userConfig, default_tools: userConfig.default_tools ?? defaultTools }; + }, [allAgents, modeConfigs]); + + const saveModeConfig = useCallback(async (agentId: string, updates: Partial) => { + const config = getModeConfig(agentId); + if (!config) return; + const updated = { ...config, ...updates }; + await configAPI.setModeConfig(agentId, updated); + setModeConfigs((prev) => ({ ...prev, [agentId]: updated })); + try { + const { globalEventBus } = await import('@/infrastructure/event-bus'); + globalEventBus.emit('mode:config:updated'); + } catch { /* ignore */ } + }, [getModeConfig]); + + const handleToggleTool = useCallback(async (agentId: string, toolName: string) => { + const config = getModeConfig(agentId); + if (!config) return; + const tools = config.available_tools ?? []; + const isEnabling = !tools.includes(toolName); + const newTools = isEnabling ? [...tools, toolName] : tools.filter((t) => t !== toolName); + try { + await saveModeConfig(agentId, { available_tools: newTools }); + } catch { + notification.error(t('agentsOverview.toolToggleFailed', '工具切换失败')); + } + }, [getModeConfig, saveModeConfig, notification, t]); + + const handleResetTools = useCallback(async (agentId: string) => { + try { + await configAPI.resetModeConfig(agentId); + const updated = await configAPI.getModeConfigs(); + setModeConfigs(updated as Record); + notification.success(t('agentsOverview.toolsResetSuccess', '已重置为默认工具')); + try { + const { globalEventBus } = await import('@/infrastructure/event-bus'); + globalEventBus.emit('mode:config:updated'); + } catch { /* ignore */ } + } catch { + notification.error(t('agentsOverview.toolToggleFailed', '重置失败')); + } + }, [notification, t]); + const filteredAgents = allAgents.filter((a) => { - if (!query) return true; - const q = query.toLowerCase(); - return a.name.toLowerCase().includes(q) || a.description.toLowerCase().includes(q); + // 文本搜索 + 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) + if (filterLevel !== 'all') { + const level = a.agentKind === 'mode' ? 'builtin' : (a.subagentSource ?? 'builtin'); + if (level !== filterLevel) return false; + } + return true; }); + const LEVEL_FILTERS: { key: FilterLevel; label: string }[] = [ + { key: 'all', label: t('agentsOverview.filterAll', '全部') }, + { key: 'builtin', label: t('agentsOverview.filterBuiltin', '内置') }, + { key: 'user', label: t('agentsOverview.filterUser', '用户') }, + { key: 'project', label: t('agentsOverview.filterProject', '项目') }, + ]; + + const TYPE_FILTERS: { key: FilterType; label: string }[] = [ + { key: 'all', label: t('agentsOverview.filterAll', '全部') }, + { key: 'mode', label: 'Agent' }, + { key: 'subagent', label: 'Sub-Agent' }, + ]; + return (
@@ -221,6 +429,14 @@ const AgentsOverviewPage: React.FC = () => {

{t('agentsOverview.title')}

{t('agentsOverview.subtitle')}

+
{
{t('agentsOverview.sectionTitle')} {filteredAgents.length} +
+
+ {LEVEL_FILTERS.map(({ key, label }) => ( + + ))} +
+
+
+ {TYPE_FILTERS.map(({ key, label }) => ( + + ))} +
+
{loading ? (
@@ -259,6 +502,10 @@ const AgentsOverviewPage: React.FC = () => { soloEnabled={agentSoloEnabled[a.id] ?? a.enabled} onToggleSolo={setAgentSoloEnabled} index={i} + availableTools={availableTools} + modeConfig={a.agentKind === 'mode' ? getModeConfig(a.id) : null} + onToggleTool={handleToggleTool} + onResetTools={handleResetTools} /> ))}
diff --git a/src/web-ui/src/app/scenes/team/components/CreateAgentPage.tsx b/src/web-ui/src/app/scenes/team/components/CreateAgentPage.tsx new file mode 100644 index 00000000..1b12a1cb --- /dev/null +++ b/src/web-ui/src/app/scenes/team/components/CreateAgentPage.tsx @@ -0,0 +1,196 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import { ArrowLeft } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; +import { Input, Textarea, Switch, Button } from '@/component-library'; +import { SubagentAPI } from '@/infrastructure/api/service-api/SubagentAPI'; +import type { SubagentLevel } from '@/infrastructure/api/service-api/SubagentAPI'; +import { useNotification } from '@/shared/notification-system'; +import { useCurrentWorkspace } from '@/infrastructure/hooks/useWorkspace'; +import { useTeamStore } from '../teamStore'; +import '../TeamView.scss'; +import './TeamHomePage.scss'; + +const NAME_REGEX = /^[a-zA-Z][a-zA-Z0-9_-]*$/; + +const CreateAgentPage: React.FC = () => { + const { t } = useTranslation('scenes/team'); + const { openAgentsOverview } = useTeamStore(); + const notification = useNotification(); + const { hasWorkspace } = useCurrentWorkspace(); + + const [level, setLevel] = useState('user'); + const [name, setName] = useState(''); + const [nameError, setNameError] = useState(null); + const [description, setDescription] = useState(''); + const [prompt, setPrompt] = useState(''); + const [readonly, setReadonly] = useState(true); + const [toolNames, setToolNames] = useState([]); + const [selectedTools, setSelectedTools] = useState>(new Set()); + const [submitting, setSubmitting] = useState(false); + + useEffect(() => { + SubagentAPI.listAgentToolNames().then(setToolNames).catch(() => setToolNames([])); + }, []); + + const validateName = useCallback((v: string) => { + if (!v.trim()) return t('agentsOverview.form.nameRequired', '名称不能为空'); + if (!NAME_REGEX.test(v.trim())) return t('agentsOverview.form.nameFormat', '只能以字母开头,包含字母/数字/下划线/连字符'); + return null; + }, [t]); + + const toggleTool = (tool: string) => { + setSelectedTools((prev) => { + const next = new Set(prev); + next.has(tool) ? next.delete(tool) : next.add(tool); + return next; + }); + }; + + const handleSubmit = async () => { + const err = validateName(name); + if (err) { setNameError(err); return; } + if (!description.trim()) { notification.error(t('agentsOverview.form.descRequired', '描述不能为空')); return; } + if (!prompt.trim()) { notification.error(t('agentsOverview.form.promptRequired', '系统提示词不能为空')); return; } + setSubmitting(true); + try { + await SubagentAPI.createSubagent({ + level, + name: name.trim(), + description: description.trim(), + prompt: prompt.trim(), + readonly, + tools: selectedTools.size > 0 ? Array.from(selectedTools) : undefined, + }); + notification.success(t('agentsOverview.form.createSuccess', `已创建 Agent「${name.trim()}」`)); + openAgentsOverview(); + } catch (err) { + notification.error( + t('agentsOverview.form.createFailed', '创建失败:') + + (err instanceof Error ? err.message : String(err)) + ); + } finally { + setSubmitting(false); + } + }; + + return ( +
+ {/* 顶部导航栏 */} +
+ +
+ + {/* 页面内容 */} +
+
+
+

{t('agentsOverview.form.title', '新建 Sub-Agent')}

+

{t('agentsOverview.form.subtitle', '创建一个自定义的用户级或项目级 Sub-Agent')}

+
+ +
+ {/* 名称 */} +
+ + { setName(e.target.value); setNameError(validateName(e.target.value)); }} + onBlur={() => setNameError(validateName(name))} + placeholder={t('agentsOverview.form.namePlaceholder', '字母开头,可含字母/数字/下划线')} + inputSize="small" + error={!!nameError} + /> + {nameError && {nameError}} +
+ + {/* 描述 */} +
+ + setDescription(e.target.value)} + placeholder={t('agentsOverview.form.descPlaceholder', '简要描述该 Agent 的用途')} + inputSize="small" + /> +
+ + {/* 级别 + 只读模式 同行 */} +
+
+ {(['user', 'project'] as SubagentLevel[]).map((lv) => { + const disabled = lv === 'project' && !hasWorkspace; + return ( + + ); + })} +
+
+ + setReadonly(e.target.checked)} size="small" /> +
+
+ + {/* 工具 */} + {toolNames.length > 0 && ( +
+ +
+ {toolNames.map((tool) => ( + + ))} +
+
+ )} + + {/* 系统提示词 */} +
+ +