diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f2983a90a..ef0986ab0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,14 +13,14 @@ Be respectful, kind, and constructive. We welcome contributors of all background ### Prerequisites - Node.js (LTS recommended) -- npm -- Rust toolchain (install via rustup) -- Tauri dependencies for desktop development +- pnpm (run `corepack enable`) +- Rust toolchain (install via [rustup](https://rustup.rs/)) +- [Tauri prerequisites](https://v2.tauri.app/start/prerequisites/) for desktop development ### Install dependencies ```bash -npm install +pnpm install ``` ### Common commands diff --git a/CONTRIBUTING_CN.md b/CONTRIBUTING_CN.md index f887b8931..755ba1d3d 100644 --- a/CONTRIBUTING_CN.md +++ b/CONTRIBUTING_CN.md @@ -13,14 +13,14 @@ ### 环境准备 - Node.js(建议 LTS 版本) -- npm +- pnpm(执行 `corepack enable`) - Rust toolchain(通过 rustup 安装) - 桌面端开发需准备 Tauri 依赖 ### 安装依赖 ```bash -npm install +pnpm install ``` ### 常用命令 diff --git a/README.md b/README.md index cd4e9fbc9..1ed7832e6 100644 --- a/README.md +++ b/README.md @@ -48,10 +48,35 @@ BitFun is an Agentic Development Environment (ADE). While featuring a cutting-ed ## Quick Start + +### Use Directly + Download the latest installer for the desktop app from [Release](https://github.com/GCWing/BitFun/releases). After installation, configure your model and you're ready to go. Other form factors are currently only specification drafts and not yet developed. If needed, please build from source. +### Build from Source + +Make sure you have the following prerequisites installed: + +- Node.js (LTS recommended) +- pnpm (run `corepack enable`) +- Rust toolchain (install via [rustup](https://rustup.rs/)) +- [Tauri prerequisites](https://v2.tauri.app/start/prerequisites/) for desktop development + +```bash +# Install dependencies +pnpm install + +# Run desktop app in development mode +npm run desktop:dev + +# Build desktop app +npm run desktop:build +``` + +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. diff --git a/README.zh-CN.md b/README.zh-CN.md index 5517770dc..f24e37f3d 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -48,10 +48,35 @@ BitFun 是一款代理式开发环境(ADE,Agentic Development Environment) ## 快速开始 -桌面端程序在[Release](https://github.com/GCWing/BitFun/releases)处下载最新安装包,安装后配置模型即可开始使用。 + +### 直接使用 + +桌面端程序在 [Release](https://github.com/GCWing/BitFun/releases) 处下载最新安装包,安装后配置模型即可开始使用。 其他形态暂时仅是规范雏形未完成开发,如有需要请从源码构建。 +### 从源码构建 + +请确保已安装以下前置依赖: + +- Node.js(推荐 LTS 版本) +- pnpm(执行 `corepack enable`) +- Rust 工具链(通过 [rustup](https://rustup.rs/) 安装) +- [Tauri 前置依赖](https://v2.tauri.app/start/prerequisites/)(桌面端开发需要) + +```bash +# 安装依赖 +pnpm install + +# 以开发模式运行桌面端 +npm run desktop:dev + +# 构建桌面端 +npm run desktop:build +``` + +更多详情请参阅[贡献指南](./CONTRIBUTING_CN.md)。 + ## 平台支持 项目采用 Rust + TypeScript 技术栈,支持跨平台和多形态复用。 diff --git a/package.json b/package.json index f3f6b8555..fd0b2f3a8 100644 --- a/package.json +++ b/package.json @@ -19,11 +19,11 @@ "build:web": "cd src/web-ui && vite build", "preview": "cd src/web-ui && vite preview", "desktop:dev": "node scripts/dev.cjs desktop", - "desktop:dev:raw": "cd src/apps/desktop && cargo tauri dev", - "desktop:build": "npm run build:web && cd src/apps/desktop && cargo tauri build", - "desktop:build:exe": "npm run build:web && cd src/apps/desktop && cargo tauri build --no-bundle", - "desktop:build:nsis": "npm run build:web && cd src/apps/desktop && cargo tauri build --bundles nsis", - "desktop:build:x86_64": "npm run build:web && cd src/apps/desktop && cargo tauri build --target x86_64-apple-darwin", + "desktop:dev:raw": "cd src/apps/desktop && pnpm tauri dev", + "desktop:build": "npm run build:web && cd src/apps/desktop && pnpm tauri build", + "desktop:build:exe": "npm run build:web && cd src/apps/desktop && pnpm tauri build --no-bundle", + "desktop:build:nsis": "npm run build:web && cd src/apps/desktop && pnpm tauri build --bundles nsis", + "desktop:build:x86_64": "npm run build:web && cd src/apps/desktop && pnpm tauri build --target x86_64-apple-darwin", "cli:dev": "cd src/apps/cli && cargo run --", "cli:build": "cd src/apps/cli && cargo build --release", "cli:run": "cd src/apps/cli && cargo run --release --", @@ -52,7 +52,7 @@ "@lezer/highlight": "^1.2.1", "@monaco-editor/react": "^4.6.0", "@react-sigma/core": "^5.0.6", - "@tauri-apps/api": "~2.9.0", + "@tauri-apps/api": "^2", "@tauri-apps/plugin-dialog": "^2.6", "@tauri-apps/plugin-fs": "^2", "@tauri-apps/plugin-log": "^2.8.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 92e2d507a..183a91c3d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -36,7 +36,7 @@ importers: specifier: ^5.0.6 version: 5.0.6(graphology@0.26.0(graphology-types@0.24.8))(react@18.3.1)(sigma@3.0.2(graphology-types@0.24.8)) '@tauri-apps/api': - specifier: ^2.10.1 + specifier: ^2 version: 2.10.1 '@tauri-apps/plugin-dialog': specifier: ^2.6 diff --git a/scripts/dev.cjs b/scripts/dev.cjs index 84630dc62..1949099fd 100644 --- a/scripts/dev.cjs +++ b/scripts/dev.cjs @@ -24,26 +24,58 @@ const ROOT_DIR = path.resolve(__dirname, '..'); */ function runSilent(command, cwd = ROOT_DIR) { try { - execSync(command, { + const stdout = execSync(command, { cwd, stdio: 'pipe', - encoding: 'utf-8' + encoding: 'buffer' }); - return true; + return { ok: true, stdout: decodeOutput(stdout), stderr: '' }; } catch (error) { - return false; + const stdout = error.stdout ? decodeOutput(error.stdout) : ''; + const stderr = error.stderr ? decodeOutput(error.stderr) : ''; + return { ok: false, stdout, stderr, error }; } } +function decodeOutput(output) { + if (!output) return ''; + if (typeof output === 'string') return output; + const buffer = Buffer.isBuffer(output) ? output : Buffer.from(output); + if (process.platform !== 'win32') return buffer.toString('utf-8'); + + const utf8 = buffer.toString('utf-8'); + if (!utf8.includes('�')) return utf8; + + try { + const { TextDecoder } = require('util'); + const decoder = new TextDecoder('gbk'); + const gbk = decoder.decode(buffer); + if (gbk && !gbk.includes('�')) return gbk; + return gbk || utf8; + } catch (error) { + return utf8; + } +} + +function tailOutput(output, maxLines = 12) { + if (!output) return ''; + const lines = output + .split(/\r?\n/) + .map((line) => line.trimEnd()) + .filter((line) => line.trim() !== ''); + if (lines.length <= maxLines) return lines.join('\n'); + return lines.slice(-maxLines).join('\n'); +} + /** * Run command with inherited output */ function runInherit(command, cwd = ROOT_DIR) { try { execSync(command, { cwd, stdio: 'inherit' }); - return true; + return { ok: true, error: null }; } catch (error) { - return false; + return { ok: false, error }; } } @@ -86,17 +118,35 @@ async function main() { // Step 1: Copy resources printStep(1, 3, 'Copy resources'); - if (runSilent('npm run copy-monaco --silent')) { + const copyResult = runSilent('npm run copy-monaco --silent'); + if (copyResult.ok) { printSuccess('Monaco Editor resources ready'); } else { printError('Copy resources failed'); + const output = tailOutput(copyResult.stderr || copyResult.stdout); + if (output) { + printError(output); + } else if (copyResult.error) { + printError(copyResult.error.message); + } + if (copyResult.error && copyResult.error.status !== undefined) { + printError(`Exit code: ${copyResult.error.status}`); + } + printInfo('Hint: run `pnpm install` in repo root if dependencies are missing'); process.exit(1); } // Step 2: Generate version info printStep(2, 3, 'Generate version info'); - if (!runInherit('node scripts/generate-version.cjs')) { + const versionResult = runInherit('node scripts/generate-version.cjs'); + if (!versionResult.ok) { printError('Generate version info failed'); + if (versionResult.error && versionResult.error.message) { + printError(versionResult.error.message); + } + if (versionResult.error && versionResult.error.status !== undefined) { + printError(`Exit code: ${versionResult.error.status}`); + } process.exit(1); } @@ -110,7 +160,7 @@ async function main() { try { if (mode === 'desktop') { - await runCommand('cargo tauri dev', path.join(ROOT_DIR, 'src/apps/desktop')); + await runCommand('pnpm tauri dev', path.join(ROOT_DIR, 'src/apps/desktop')); } else { await runCommand('npx vite', path.join(ROOT_DIR, 'src/web-ui')); } diff --git a/src/apps/desktop/tauri.conf.json b/src/apps/desktop/tauri.conf.json index f66faa128..0e2d07705 100644 --- a/src/apps/desktop/tauri.conf.json +++ b/src/apps/desktop/tauri.conf.json @@ -4,9 +4,9 @@ "version": "0.1.0", "identifier": "com.bitfun.desktop", "build": { - "beforeDevCommand": "cd ../.. && npm run dev:web", + "beforeDevCommand": "npm run dev:web", "devUrl": "http://localhost:1422", - "beforeBuildCommand": "cd ../.. && npm run build:web", + "beforeBuildCommand": "npm run build:web", "frontendDist": "../../../dist" }, "bundle": { diff --git a/src/crates/core/src/agentic/agents/agentic_mode.rs b/src/crates/core/src/agentic/agents/agentic_mode.rs index 9dad2cde4..1d9ff5347 100644 --- a/src/crates/core/src/agentic/agents/agentic_mode.rs +++ b/src/crates/core/src/agentic/agents/agentic_mode.rs @@ -26,6 +26,7 @@ impl AgenticMode { "AnalyzeImage".to_string(), "Skill".to_string(), "AskUserQuestion".to_string(), + "Git".to_string(), ], } } diff --git a/src/crates/core/src/agentic/agents/prompt_builder/prompt_builder.rs b/src/crates/core/src/agentic/agents/prompt_builder/prompt_builder.rs index 6913d6be5..bd4885988 100644 --- a/src/crates/core/src/agentic/agents/prompt_builder/prompt_builder.rs +++ b/src/crates/core/src/agentic/agents/prompt_builder/prompt_builder.rs @@ -17,6 +17,7 @@ const PLACEHOLDER_PROJECT_LAYOUT: &str = "{PROJECT_LAYOUT}"; const PLACEHOLDER_RULES: &str = "{RULES}"; const PLACEHOLDER_MEMORIES: &str = "{MEMORIES}"; const PLACEHOLDER_LANGUAGE_PREFERENCE: &str = "{LANGUAGE_PREFERENCE}"; +const PLACEHOLDER_VISUAL_MODE: &str = "{VISUAL_MODE}"; pub struct PromptBuilder { pub workspace_path: String, @@ -156,6 +157,32 @@ These files are maintained by the user and should NOT be modified unless explici } } + /// Get visual mode instruction from user config + /// + /// Reads `app.ai_experience.enable_visual_mode` from global config. + /// Returns a prompt snippet when enabled, or empty string when disabled. + async fn get_visual_mode_instruction(&self) -> String { + let enabled = match GlobalConfigManager::get_service().await { + Ok(service) => service + .get_config::(Some("app.ai_experience.enable_visual_mode")) + .await + .unwrap_or(false), + Err(e) => { + debug!("Failed to read visual mode config: {}", e); + false + } + }; + + if enabled { + r"# Visualizing complex logic as you explain +Use Mermaid diagrams to visualize complex logic, workflows, architectures, and data flows whenever it helps clarify the explanation. +Prefer MermaidInteractive tool when available, otherwise output Mermaid code blocks directly. +".to_string() + } else { + String::new() + } + } + /// Get user language preference instruction /// /// Read app.language from global config, generate simple language instruction @@ -194,6 +221,7 @@ These files are maintained by the user and should NOT be modified unless explici /// - `{PROJECT_CONTEXT_FILES}` - Project context files (AGENTS.md, CLAUDE.md, etc.) /// - `{RULES}` - AI rules /// - `{MEMORIES}` - AI memories + /// - `{VISUAL_MODE}` - Visual mode instruction (Mermaid diagrams, read from global config) /// /// If a placeholder is not in the template, corresponding content will not be added pub async fn build_prompt_from_template(&self, template: &str) -> BitFunResult { @@ -264,6 +292,12 @@ These files are maintained by the user and should NOT be modified unless explici result = result.replace(PLACEHOLDER_MEMORIES, &memories); } + // Replace {VISUAL_MODE} + if result.contains(PLACEHOLDER_VISUAL_MODE) { + let visual_mode = self.get_visual_mode_instruction().await; + result = result.replace(PLACEHOLDER_VISUAL_MODE, &visual_mode); + } + Ok(result.trim().to_string()) } } diff --git a/src/crates/core/src/agentic/agents/prompts/agentic_mode.md b/src/crates/core/src/agentic/agents/prompts/agentic_mode.md index 29c90f07e..4c4bb0172 100644 --- a/src/crates/core/src/agentic/agents/prompts/agentic_mode.md +++ b/src/crates/core/src/agentic/agents/prompts/agentic_mode.md @@ -71,6 +71,7 @@ I've found some existing telemetry code. Let me mark the first todo as in_progre # Asking questions as you work You have access to the AskUserQuestion tool to ask the user questions when you need clarification, want to validate assumptions, or need to make a decision you're unsure about. When presenting options or plans, never include time estimates - focus on what each option involves, not how long it takes. +{VISUAL_MODE} # Doing tasks The user will primarily request you perform software engineering tasks. This includes solving bugs, adding new functionality, refactoring code, explaining code, and more. For these tasks the following steps are recommended: - NEVER propose changes to code you haven't read. If a user asks about or wants you to modify a file, read it first. Understand existing code before suggesting modifications. diff --git a/src/crates/core/src/service/config/global.rs b/src/crates/core/src/service/config/global.rs index 1c9a1f02a..585d5f466 100644 --- a/src/crates/core/src/service/config/global.rs +++ b/src/crates/core/src/service/config/global.rs @@ -124,10 +124,15 @@ impl GlobalConfigManager { Ok(()) } - /// Reloads configuration. + /// Reloads configuration in-place. + /// + /// Re-reads the config from disk into the existing `ConfigService` instance, + /// preserving the `Arc` pointer so that all holders (e.g. `AppState`) stay in sync. pub async fn reload() -> BitFunResult<()> { - let new_service = Arc::new(ConfigService::new().await?); - Self::update_service(new_service).await + let service = Self::get_service().await?; + service.reload().await?; + Self::broadcast_update(ConfigUpdateEvent::ConfigReloaded).await; + Ok(()) } /// Subscribes to configuration update events. diff --git a/src/crates/core/src/service/config/manager.rs b/src/crates/core/src/service/config/manager.rs index 418b3b4ac..77d9f48ca 100644 --- a/src/crates/core/src/service/config/manager.rs +++ b/src/crates/core/src/service/config/manager.rs @@ -602,8 +602,8 @@ pub(crate) fn migrate_0_0_0_to_1_0_0(mut config: Value) -> BitFunResult { app.insert( "ai_experience".to_string(), serde_json::json!({ - "enableSessionTitleGeneration": true, - "enableWelcomePanelAiAnalysis": true + "enable_session_title_generation": true, + "enable_welcome_panel_ai_analysis": true }), ); } diff --git a/src/crates/core/src/service/config/types.rs b/src/crates/core/src/service/config/types.rs index 839210f50..30294b07a 100644 --- a/src/crates/core/src/service/config/types.rs +++ b/src/crates/core/src/service/config/types.rs @@ -48,12 +48,14 @@ pub struct AppConfig { /// AI experience configuration. #[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(default, rename_all = "camelCase")] +#[serde(default)] pub struct AIExperienceConfig { /// Whether to enable automatic AI-generated summaries for session titles. pub enable_session_title_generation: bool, /// Whether to enable AI analysis of work status on the FlowChat welcome page. pub enable_welcome_panel_ai_analysis: bool, + /// Whether to enable visual mode. + pub enable_visual_mode: bool, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -862,6 +864,7 @@ impl Default for AIExperienceConfig { Self { enable_session_title_generation: true, enable_welcome_panel_ai_analysis: true, + enable_visual_mode: false, } } } diff --git a/src/web-ui/src/flow_chat/tool-cards/CreatePlanDisplay.tsx b/src/web-ui/src/flow_chat/tool-cards/CreatePlanDisplay.tsx index 10565277a..0801f2447 100644 --- a/src/web-ui/src/flow_chat/tool-cards/CreatePlanDisplay.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/CreatePlanDisplay.tsx @@ -13,6 +13,7 @@ import { ideControl } from '@/shared/services/ide-control/api'; import { flowChatManager } from '@/flow_chat/services/FlowChatManager'; import { workspaceAPI } from '@/infrastructure/api/service-api/WorkspaceAPI'; import { fileSystemService } from '@/tools/file-system/services/FileSystemService'; +import { planBuildStateService } from '@/shared/services/PlanBuildStateService'; import yaml from 'yaml'; import { Tooltip } from '@/component-library'; import { createLogger } from '@/shared/utils/logger'; @@ -20,13 +21,6 @@ import './CreatePlanDisplay.scss'; const log = createLogger('PlanDisplay'); -interface TodoWriteUpdateEvent { - sessionId: string; - turnId: string; - todos: Array<{ id: string; content: string; status: string }>; - merge: boolean; -} - interface PlanTodo { id: string; content: string; @@ -46,152 +40,6 @@ interface PlanData { // key: cacheKey (toolId or planFilePath), value: PlanData const planDataCache = new Map(); -// Track active builds by cacheKey to match TodoWrite events. -const buildingPlans = new Map>(); - -// Subscribers for cache updates, keyed by cacheKey. -const cacheSubscribers = new Map void>>(); - -/** - * Subscribe to cache updates for a cacheKey. - */ -function subscribeToCacheUpdate(cacheKey: string, callback: (data: PlanData) => void): () => void { - if (!cacheSubscribers.has(cacheKey)) { - cacheSubscribers.set(cacheKey, new Set()); - } - cacheSubscribers.get(cacheKey)!.add(callback); - - return () => { - const subscribers = cacheSubscribers.get(cacheKey); - if (subscribers) { - subscribers.delete(callback); - if (subscribers.size === 0) { - cacheSubscribers.delete(cacheKey); - } - } - }; -} - -function notifyCacheUpdate(cacheKey: string, data: PlanData): void { - const subscribers = cacheSubscribers.get(cacheKey); - if (subscribers) { - subscribers.forEach(callback => callback(data)); - } -} - -/** - * Module-level TodoWrite handler to keep cache updated after unmount. - */ -async function handleGlobalTodoWriteUpdate(event: Event): Promise { - const customEvent = event as CustomEvent; - const { todos: incomingTodos } = customEvent.detail; - - if (!incomingTodos.length) { - return; - } - - for (const [cacheKey, todoIds] of buildingPlans.entries()) { - const matchedTodos = incomingTodos.filter(t => todoIds.has(t.id)); - if (matchedTodos.length === 0) { - continue; - } - - const cachedPlan = planDataCache.get(cacheKey); - if (!cachedPlan) { - log.warn('Cached plan data not found', { cacheKey }); - continue; - } - - try { - const content = await workspaceAPI.readFileContent(cachedPlan.planFilePath); - - const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/); - if (!frontmatterMatch) { - log.warn('Failed to parse plan file frontmatter', { filePath: cachedPlan.planFilePath }); - continue; - } - - const parsed = yaml.parse(frontmatterMatch[1]); - const planContent = content.replace(/^---\n[\s\S]*?\n---\n*/, '').trim(); - - const updatedTodos = (parsed.todos || []).map((todo: PlanTodo) => { - const incomingTodo = incomingTodos.find(t => t.id === todo.id); - if (incomingTodo) { - return { ...todo, status: incomingTodo.status }; - } - return todo; - }); - - const updatedFrontmatter = yaml.stringify({ - ...parsed, - todos: updatedTodos - }); - const updatedContent = `---\n${updatedFrontmatter}---\n\n${planContent}`; - - // Mark file as writing to avoid watcher feedback loops. - markFileWriting(cachedPlan.planFilePath); - await workspaceAPI.writeFileContent('', cachedPlan.planFilePath, updatedContent); - - const newPlanData: PlanData = { - name: parsed.name || cachedPlan.name, - overview: parsed.overview || cachedPlan.overview, - todos: updatedTodos, - planFilePath: cachedPlan.planFilePath, - planContent: planContent, - }; - - planDataCache.set(cacheKey, newPlanData); - - notifyCacheUpdate(cacheKey, newPlanData); - - const allCompleted = updatedTodos.every((t: PlanTodo) => t.status === 'completed'); - if (allCompleted) { - buildingPlans.delete(cacheKey); - } - - } catch (error) { - log.error('Failed to sync todo status', { cacheKey, error }); - } - } -} - -// Set up the global listener once. -let isGlobalListenerSetup = false; -function setupGlobalTodoWriteListener(): void { - if (isGlobalListenerSetup) return; - isGlobalListenerSetup = true; - - window.addEventListener('bitfun:todowrite-update', handleGlobalTodoWriteUpdate); -} - -setupGlobalTodoWriteListener(); - -// Clear build tracking when a dialog is cancelled. -window.addEventListener('bitfun:dialog-cancelled', () => { - if (buildingPlans.size > 0) { - buildingPlans.clear(); - } -}); - -// Track files being written to avoid watcher loops. -const writingFiles = new Set(); - -/** - * Mark a file as being written. - */ -function markFileWriting(filePath: string): void { - const normalizedPath = filePath.replace(/\\/g, '/'); - writingFiles.add(normalizedPath); - setTimeout(() => { - writingFiles.delete(normalizedPath); - }, 1000); -} - -function isFileWriting(filePath: string): boolean { - const normalizedPath = filePath.replace(/\\/g, '/'); - return writingFiles.has(normalizedPath); -} - // ==================== PlanDisplay core component ==================== export interface PlanDisplayProps { @@ -224,9 +72,9 @@ export const PlanDisplay: React.FC = ({ return planDataCache.get(effectiveCacheKey) || null; }); - // Initialize build state from the global tracker to survive unmounts. + // Initialize build state from the shared service to survive unmounts. const [isBuildStarted, setIsBuildStarted] = useState(() => { - return buildingPlans.has(effectiveCacheKey); + return planFilePath ? planBuildStateService.isBuildActive(planFilePath) : false; }); const [isTodosExpanded, setIsTodosExpanded] = useState(false); @@ -249,6 +97,33 @@ export const PlanDisplay: React.FC = ({ const planData = refreshedData || initialPlanData; + // Subscribe to shared build state service for cross-component sync. + useEffect(() => { + if (!planFilePath) return; + + // Sync initial state (in case planFilePath just became available). + setIsBuildStarted(planBuildStateService.isBuildActive(planFilePath)); + + const unsubscribe = planBuildStateService.subscribe(planFilePath, (event) => { + setIsBuildStarted(event.isBuilding); + + if (event.updatedTodos) { + const cached = planDataCache.get(effectiveCacheKey); + const newPlanData: PlanData = { + name: cached?.name || initialName, + overview: cached?.overview || initialOverview, + todos: event.updatedTodos, + planFilePath: planFilePath, + planContent: event.planContent || cached?.planContent, + }; + setRefreshedData(newPlanData); + planDataCache.set(effectiveCacheKey, newPlanData); + } + }); + + return unsubscribe; + }, [planFilePath, effectiveCacheKey, initialName, initialOverview]); + // Load latest content on mount and refresh on file changes. useEffect(() => { if (!planFilePath) { @@ -263,7 +138,7 @@ export const PlanDisplay: React.FC = ({ const loadFromFile = async () => { // Skip refresh while writing to avoid feedback loops. - if (isFileWriting(planFilePath)) { + if (planBuildStateService.isFileWriting(planFilePath)) { return; } @@ -297,7 +172,7 @@ export const PlanDisplay: React.FC = ({ loadFromFile(); } - let debounceTimer: NodeJS.Timeout | null = null; + let debounceTimer: ReturnType | null = null; const unwatch = fileSystemService.watchFileChanges(dirPath, (event) => { const eventPath = event.path.replace(/\\/g, '/'); @@ -352,34 +227,6 @@ export const PlanDisplay: React.FC = ({ } } }, [buildStatus, isBuildStarted]); - - useEffect(() => { - const unsubscribe = subscribeToCacheUpdate(effectiveCacheKey, (newData) => { - setRefreshedData(newData); - }); - - return () => { - unsubscribe(); - }; - }, [effectiveCacheKey]); - - // Reset build state on dialog cancel (keep todos to preserve progress). - useEffect(() => { - if (!isBuildStarted) return; - - const handleDialogCancelled = () => { - if (buildStatus === 'built') return; - - setIsBuildStarted(false); - }; - - window.addEventListener('bitfun:dialog-cancelled', handleDialogCancelled); - return () => { - window.removeEventListener('bitfun:dialog-cancelled', handleDialogCancelled); - }; - }, [isBuildStarted, buildStatus]); - - // The global TodoWrite listener keeps cache updated after unmount. const planFileName = useMemo(() => { if (!planData?.planFilePath) return ''; @@ -418,11 +265,9 @@ export const PlanDisplay: React.FC = ({ setRefreshedData(latestPlanData); planDataCache.set(effectiveCacheKey, latestPlanData); - // Register build tracking so unmounted components still receive updates. - const todoIds = new Set(latestPlanData.todos.map(t => t.id)); - buildingPlans.set(effectiveCacheKey, todoIds); - - setIsBuildStarted(true); + // Register build in shared service (notifies all subscribers including PlanViewer). + const todoIds = latestPlanData.todos.map(t => t.id); + planBuildStateService.startBuild(planFilePath, todoIds); // Send message using the latest data. const simpleTodos = latestPlanData.todos.map(t => ({ @@ -446,8 +291,7 @@ ${JSON.stringify(simpleTodos, null, 2)} await flowChatManager.sendMessage(message, undefined, displayMessage, 'agentic', 'agentic'); } catch (error) { log.error('Build failed', { cacheKey: effectiveCacheKey, planFilePath, error }); - buildingPlans.delete(effectiveCacheKey); - setIsBuildStarted(false); + planBuildStateService.cancelBuild(planFilePath); } }, [planFilePath, buildStatus, effectiveCacheKey, initialName, initialOverview, initialTodos]); @@ -602,7 +446,7 @@ export const CreatePlanDisplay: React.FC = ({ initialName={initialName} initialOverview={initialOverview} initialTodos={initialTodos} - status={status} + status={status as PlanDisplayProps['status']} cacheKey={toolItem.id} /> ); diff --git a/src/web-ui/src/infrastructure/config/components/AIFeaturesConfig.tsx b/src/web-ui/src/infrastructure/config/components/AIFeaturesConfig.tsx index a09f4b10d..ed2b15525 100644 --- a/src/web-ui/src/infrastructure/config/components/AIFeaturesConfig.tsx +++ b/src/web-ui/src/infrastructure/config/components/AIFeaturesConfig.tsx @@ -19,13 +19,13 @@ const log = createLogger('AIFeaturesConfig'); interface AIExperienceSettings { - enableSessionTitleGeneration: boolean; - enableWelcomePanelAiAnalysis: boolean; + enable_session_title_generation: boolean; + enable_welcome_panel_ai_analysis: boolean; } const defaultSettings: AIExperienceSettings = { - enableSessionTitleGeneration: true, - enableWelcomePanelAiAnalysis: true, + enable_session_title_generation: true, + enable_welcome_panel_ai_analysis: true, }; @@ -39,12 +39,12 @@ interface FeatureConfig { const FEATURE_CONFIGS: FeatureConfig[] = [ { id: 'sessionTitle', - settingKey: 'enableSessionTitleGeneration', + settingKey: 'enable_session_title_generation', agentName: 'startchat-func-agent', }, { id: 'welcomeAnalysis', - settingKey: 'enableWelcomePanelAiAnalysis', + settingKey: 'enable_welcome_panel_ai_analysis', agentName: 'startchat-func-agent', }, { diff --git a/src/web-ui/src/infrastructure/config/components/AgenticModeConfig.tsx b/src/web-ui/src/infrastructure/config/components/AgenticModeConfig.tsx index 0089bcde8..46038c8ce 100644 --- a/src/web-ui/src/infrastructure/config/components/AgenticModeConfig.tsx +++ b/src/web-ui/src/infrastructure/config/components/AgenticModeConfig.tsx @@ -1,21 +1,16 @@ import React, { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; -import { RotateCcw, ListChecks, X, Save } from 'lucide-react'; -import { IconButton, Card, Switch, Select } from '@/component-library'; +import { RotateCcw, ListChecks, X } from 'lucide-react'; +import { IconButton, Card, Switch } from '@/component-library'; import { notificationService } from '@/shared/notification-system'; import { configAPI } from '@/infrastructure/api'; -import type { ModeConfigItem } from '../types'; +import type { ModeConfigItem, AIExperienceConfig } from '../types'; import { ConfigPageHeader, ConfigPageLayout, ConfigPageContent } from './common'; import { createLogger } from '@/shared/utils/logger'; import './AgenticModeConfig.scss'; const log = createLogger('AgenticModeConfig'); -interface AgenticPreferences { - visualMode: boolean; - priorityStrategy: 'economy' | 'quality' | 'balanced'; -} - interface ToolInfo { name: string; description: string; @@ -31,11 +26,7 @@ export const AgenticModeConfig: React.FC = ({ embedded = const [loading, setLoading] = useState(false); const [availableTools, setAvailableTools] = useState([]); const [agenticConfig, setAgenticConfig] = useState(null); - - const [preferences, setPreferences] = useState({ - visualMode: false, - priorityStrategy: 'balanced', - }); + const [visualMode, setVisualMode] = useState(false); const [toolsCollapsed, setToolsCollapsed] = useState(true); @@ -47,15 +38,19 @@ export const AgenticModeConfig: React.FC = ({ embedded = try { setLoading(true); - const [toolsData, modeConfig] = await Promise.all([ + const [toolsData, modeConfig, aiExperience] = await Promise.all([ fetchAvailableTools(), - configAPI.getModeConfig('agentic') + configAPI.getModeConfig('agentic'), + configAPI.getConfig('app.ai_experience') as Promise ]); setAvailableTools(toolsData); setAgenticConfig(modeConfig); + if (aiExperience) { + setVisualMode(aiExperience.enable_visual_mode ?? false); + } - log.debug('Data loaded', { toolsCount: toolsData.length, agenticConfig: modeConfig }); + log.debug('Data loaded', { toolsCount: toolsData.length, agenticConfig: modeConfig, visualMode: aiExperience?.enable_visual_mode }); } catch (error) { log.error('Failed to load data', { error }); const errorMsg = error instanceof Error ? error.message : 'Unknown error'; @@ -78,6 +73,21 @@ export const AgenticModeConfig: React.FC = ({ embedded = } }; + const saveToolsConfig = async (newConfig: ModeConfigItem) => { + try { + setAgenticConfig(newConfig); + await configAPI.setModeConfig('agentic', newConfig); + + const { globalEventBus } = await import('@/infrastructure/event-bus'); + globalEventBus.emit('mode:config:updated'); + log.debug('Mode config saved and event emitted'); + } catch (error) { + log.error('Failed to save tools config', { error }); + setAgenticConfig(agenticConfig); + notificationService.error(`${t('messages.saveFailed')}: ` + (error instanceof Error ? error.message : String(error))); + } + }; + const toggleTool = async (toolName: string) => { if (!agenticConfig) return; @@ -88,45 +98,17 @@ export const AgenticModeConfig: React.FC = ({ embedded = ? [...tools, toolName] : tools.filter(t => t !== toolName); - setAgenticConfig({ - ...agenticConfig, - available_tools: newTools - }); + await saveToolsConfig({ ...agenticConfig, available_tools: newTools }); }; - const selectAllTools = () => { + const selectAllTools = async () => { if (!agenticConfig) return; - setAgenticConfig({ - ...agenticConfig, - available_tools: availableTools.map(t => t.name) - }); + await saveToolsConfig({ ...agenticConfig, available_tools: availableTools.map(t => t.name) }); }; - const clearAllTools = () => { + const clearAllTools = async () => { if (!agenticConfig) return; - setAgenticConfig({ - ...agenticConfig, - available_tools: [] - }); - }; - - const saveToolsConfig = async () => { - if (!agenticConfig) return; - - try { - setLoading(true); - await configAPI.setModeConfig('agentic', agenticConfig); - notificationService.success(t('messages.saveSuccess')); - - const { globalEventBus } = await import('@/infrastructure/event-bus'); - globalEventBus.emit('mode:config:updated'); - log.debug('Mode config update event emitted'); - } catch (error) { - log.error('Failed to save tools config', { error }); - notificationService.error(`${t('messages.saveFailed')}: ` + (error instanceof Error ? error.message : String(error))); - } finally { - setLoading(false); - } + await saveToolsConfig({ ...agenticConfig, available_tools: [] }); }; const resetToolsConfig = async () => { @@ -147,6 +129,19 @@ export const AgenticModeConfig: React.FC = ({ embedded = } }; + const handleVisualModeChange = async (e: React.ChangeEvent) => { + const checked = e.target.checked; + setVisualMode(checked); + try { + await configAPI.setConfig('app.ai_experience.enable_visual_mode', checked); + log.debug('Visual mode updated', { enabled: checked }); + } catch (error) { + log.error('Failed to save visual mode config', { error }); + setVisualMode(!checked); + notificationService.error(t('messages.saveFailed', 'Failed to save')); + } + }; + return ( = ({ embedded = {t('preferences.visualMode.label', 'Visual Mode')} - {t('preferences.visualMode.description', 'Enable visual feedback during execution')} + {t('preferences.visualMode.description', 'Use Mermaid diagrams to visualize complex logic and flows')} setPreferences(prev => ({ ...prev, visualMode: checked }))} + checked={visualMode} + onChange={handleVisualModeChange} size="small" + disabled={loading} /> - -
-
- - {t('preferences.priorityStrategy.label', 'Priority Strategy')} - - - {t('preferences.priorityStrategy.description', 'Control model selection and invocation strategy')} - -
-
-