diff --git a/src/apps/desktop/src/api/mod.rs b/src/apps/desktop/src/api/mod.rs index 2cc3bfdd..2aacdcfb 100644 --- a/src/apps/desktop/src/api/mod.rs +++ b/src/apps/desktop/src/api/mod.rs @@ -36,7 +36,6 @@ pub mod storage_commands; pub mod subagent_api; pub mod system_api; pub mod terminal_api; -pub mod token_usage_api; pub mod tool_api; pub use app_state::{AppState, AppStatistics, HealthStatus, RemoteWorkspace}; diff --git a/src/apps/desktop/src/api/token_usage_api.rs b/src/apps/desktop/src/api/token_usage_api.rs deleted file mode 100644 index 57f757c0..00000000 --- a/src/apps/desktop/src/api/token_usage_api.rs +++ /dev/null @@ -1,195 +0,0 @@ -//! Token usage tracking API - -use crate::api::app_state::AppState; -use bitfun_core::service::token_usage::{ - ModelTokenStats, SessionTokenStats, TimeRange, TokenUsageQuery, TokenUsageSummary, -}; -use log::{debug, error, info}; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use tauri::State; - -#[derive(Debug, Deserialize)] -pub struct RecordTokenUsageRequest { - pub model_id: String, - pub session_id: String, - pub turn_id: String, - pub input_tokens: u32, - pub output_tokens: u32, - pub cached_tokens: u32, - #[serde(default)] - pub is_subagent: bool, -} - -#[derive(Debug, Deserialize)] -pub struct GetModelStatsRequest { - pub model_id: String, - pub time_range: Option, - #[serde(default)] - pub include_subagent: bool, -} - -#[derive(Debug, Deserialize)] -pub struct GetSessionStatsRequest { - pub session_id: String, -} - -#[derive(Debug, Deserialize)] -pub struct QueryTokenUsageRequest { - pub model_id: Option, - pub session_id: Option, - pub time_range: TimeRange, - pub limit: Option, - pub offset: Option, - #[serde(default)] - pub include_subagent: bool, -} - -#[derive(Debug, Deserialize)] -pub struct ClearModelStatsRequest { - pub model_id: String, -} - -#[derive(Debug, Serialize)] -pub struct GetAllModelStatsResponse { - pub stats: HashMap, -} - -/// Record token usage for a specific turn -#[tauri::command] -pub async fn record_token_usage( - state: State<'_, AppState>, - request: RecordTokenUsageRequest, -) -> Result<(), String> { - debug!( - "Recording token usage: model={}, session={}, input={}, output={}", - request.model_id, request.session_id, request.input_tokens, request.output_tokens - ); - - state - .token_usage_service - .record_usage( - request.model_id, - request.session_id, - request.turn_id, - request.input_tokens, - request.output_tokens, - request.cached_tokens, - request.is_subagent, - ) - .await - .map_err(|e| { - error!("Failed to record token usage: {}", e); - format!("Failed to record token usage: {}", e) - }) -} - -/// Get token statistics for a specific model -#[tauri::command] -pub async fn get_model_token_stats( - state: State<'_, AppState>, - request: GetModelStatsRequest, -) -> Result, String> { - debug!("Getting token stats for model: {}", request.model_id); - - match request.time_range { - Some(time_range) => state - .token_usage_service - .get_model_stats_filtered(&request.model_id, time_range, request.include_subagent) - .await - .map_err(|e| { - error!("Failed to get filtered model stats: {}", e); - format!("Failed to get filtered model stats: {}", e) - }), - None => Ok(state - .token_usage_service - .get_model_stats(&request.model_id) - .await), - } -} - -/// Get token statistics for all models -#[tauri::command] -pub async fn get_all_model_token_stats( - state: State<'_, AppState>, -) -> Result { - debug!("Getting token stats for all models"); - - let stats = state.token_usage_service.get_all_model_stats().await; - - Ok(GetAllModelStatsResponse { stats }) -} - -/// Get token statistics for a specific session -#[tauri::command] -pub async fn get_session_token_stats( - state: State<'_, AppState>, - request: GetSessionStatsRequest, -) -> Result, String> { - debug!("Getting token stats for session: {}", request.session_id); - - Ok(state - .token_usage_service - .get_session_stats(&request.session_id) - .await) -} - -/// Query token usage records with filters -#[tauri::command] -pub async fn query_token_usage( - state: State<'_, AppState>, - request: QueryTokenUsageRequest, -) -> Result { - debug!("Querying token usage with filters: {:?}", request); - - let query = TokenUsageQuery { - model_id: request.model_id, - session_id: request.session_id, - time_range: request.time_range, - limit: request.limit, - offset: request.offset, - include_subagent: request.include_subagent, - }; - - state - .token_usage_service - .get_summary(query) - .await - .map_err(|e| { - error!("Failed to query token usage: {}", e); - format!("Failed to query token usage: {}", e) - }) -} - -/// Clear token statistics for a specific model -#[tauri::command] -pub async fn clear_model_token_stats( - state: State<'_, AppState>, - request: ClearModelStatsRequest, -) -> Result<(), String> { - info!("Clearing token stats for model: {}", request.model_id); - - state - .token_usage_service - .clear_model_stats(&request.model_id) - .await - .map_err(|e| { - error!("Failed to clear model stats: {}", e); - format!("Failed to clear model stats: {}", e) - }) -} - -/// Clear all token statistics -#[tauri::command] -pub async fn clear_all_token_stats(state: State<'_, AppState>) -> Result<(), String> { - info!("Clearing all token statistics"); - - state - .token_usage_service - .clear_all_stats() - .await - .map_err(|e| { - error!("Failed to clear all stats: {}", e); - format!("Failed to clear all stats: {}", e) - }) -} diff --git a/src/apps/desktop/src/lib.rs b/src/apps/desktop/src/lib.rs index 0368bf7f..0edf92c1 100644 --- a/src/apps/desktop/src/lib.rs +++ b/src/apps/desktop/src/lib.rs @@ -47,7 +47,6 @@ use api::startchat_agent_api::*; use api::storage_commands::*; use api::subagent_api::*; use api::system_api::*; -use api::token_usage_api::*; use api::tool_api::*; /// Agentic Coordinator state @@ -619,14 +618,6 @@ pub async fn run() { i18n_get_supported_languages, i18n_get_config, i18n_set_config, - // Token Usage - record_token_usage, - get_model_token_stats, - get_all_model_token_stats, - get_session_token_stats, - query_token_usage, - clear_model_token_stats, - clear_all_token_stats, // Remote Connect api::remote_connect_api::remote_connect_get_device_info, api::remote_connect_api::remote_connect_get_lan_ip, diff --git a/src/crates/core/src/agentic/session/session_manager.rs b/src/crates/core/src/agentic/session/session_manager.rs index 486628ab..8a9b8fc9 100644 --- a/src/crates/core/src/agentic/session/session_manager.rs +++ b/src/crates/core/src/agentic/session/session_manager.rs @@ -474,6 +474,24 @@ impl SessionManager { .await?; } + if let Some(cron) = crate::service::cron::get_global_cron_service() { + match cron.delete_jobs_for_session(session_id).await { + Ok(removed) if removed > 0 => { + info!( + "Removed {} scheduled job(s) for deleted session_id={}", + removed, session_id + ); + } + Ok(_) => {} + Err(e) => { + warn!( + "Failed to remove scheduled jobs for deleted session_id={}: {}", + session_id, e + ); + } + } + } + // 4. Clean up associated Terminal session use crate::service::terminal::TerminalApi; if let Ok(terminal_api) = TerminalApi::from_singleton() { diff --git a/src/crates/core/src/service/cron/service.rs b/src/crates/core/src/service/cron/service.rs index e7b7b313..c7e74a09 100644 --- a/src/crates/core/src/service/cron/service.rs +++ b/src/crates/core/src/service/cron/service.rs @@ -244,6 +244,25 @@ impl CronService { Ok(existed) } + /// Remove all scheduled jobs bound to the given session (e.g. after session delete). + pub async fn delete_jobs_for_session(&self, session_id: &str) -> BitFunResult { + let session_id = session_id.trim(); + if session_id.is_empty() { + return Ok(0); + } + let _guard = self.mutation_lock.lock().await; + let mut jobs = self.jobs.write().await; + let before = jobs.len(); + jobs.retain(|_, job| job.session_id.trim() != session_id); + let removed = before - jobs.len(); + if removed > 0 { + self.persist_jobs_locked(&jobs).await?; + drop(jobs); + self.wakeup.notify_one(); + } + Ok(removed) + } + pub async fn run_job_now(&self, job_id: &str) -> BitFunResult { { let _guard = self.mutation_lock.lock().await; @@ -532,14 +551,28 @@ impl CronService { job.state.last_run_status = Some(CronJobRunStatus::Error); job.state.last_error = Some(error.clone()); job.state.last_run_finished_at_ms = Some(now_after_submit); - job.state.retry_at_ms = Some(now_after_submit + DEFAULT_RETRY_DELAY_MS); - job.state.consecutive_failures = job.state.consecutive_failures.saturating_add(1); job.updated_at_ms = now_after_submit; - warn!( - "Failed to enqueue scheduled job: job_id={}, session_id={}, error={}", - job.id, job.session_id, error - ); + if cron_enqueue_error_is_missing_session(&error) { + job.enabled = false; + job.state.next_run_at_ms = None; + job.state.pending_trigger_at_ms = None; + job.state.retry_at_ms = None; + job.state.consecutive_failures = + job.state.consecutive_failures.saturating_add(1); + info!( + "Scheduled job auto-disabled (session no longer exists): job_id={}, session_id={}", + job.id, job.session_id + ); + } else { + job.state.retry_at_ms = Some(now_after_submit + DEFAULT_RETRY_DELAY_MS); + job.state.consecutive_failures = + job.state.consecutive_failures.saturating_add(1); + warn!( + "Failed to enqueue scheduled job: job_id={}, session_id={}, error={}", + job.id, job.session_id, error + ); + } } } @@ -718,3 +751,8 @@ struct EnqueueInput { workspace_path: String, user_input: String, } + +/// Permanent failure: coordinator cannot load session metadata (session deleted from disk). +fn cron_enqueue_error_is_missing_session(error: &str) -> bool { + error.contains("Session metadata not found") +} diff --git a/src/web-ui/package.json b/src/web-ui/package.json index 4dee6dc2..2a5ad689 100644 --- a/src/web-ui/package.json +++ b/src/web-ui/package.json @@ -6,6 +6,7 @@ "type": "module", "scripts": { "dev": "vite", + "dev:force": "vite --force", "build": "vite build", "build:desktop": "vite build --mode desktop", "build:web": "vite build --mode web", diff --git a/src/web-ui/src/app/components/NavPanel/MainNav.tsx b/src/web-ui/src/app/components/NavPanel/MainNav.tsx index a2c917c3..57d4fd0e 100644 --- a/src/web-ui/src/app/components/NavPanel/MainNav.tsx +++ b/src/web-ui/src/app/components/NavPanel/MainNav.tsx @@ -394,7 +394,7 @@ const MainNav: React.FC = ({ const createCodeTooltip = t('nav.sessions.newCodeSession'); const createCoworkTooltip = t('nav.sessions.newCoworkSession'); const assistantTooltip = t('nav.items.persona'); - const openProjectTooltip = t('header.openProject'); + const addWorkspaceTooltip = t('nav.tooltips.addWorkspace'); const isAssistantActive = activeTabId === 'assistant'; const agentsTooltip = t('nav.tooltips.agents'); const skillsTooltip = t('nav.tooltips.skills'); @@ -577,12 +577,12 @@ const MainNav: React.FC = ({ onToggle={() => toggleSection('workspace')} actions={
- + - + {multimodalOpen && (
= ({ isMaximized = false, }) => { const { openTabs, activeTabId, tabDefs, activateScene, closeScene } = useSceneManager(); - const { currentWorkspace, normalWorkspacesList, setActiveWorkspace } = useWorkspaceContext(); const sessionTitle = useCurrentSessionTitle(); const settingsTabTitle = useCurrentSettingsTabTitle(); const { t } = useI18n('common'); @@ -88,28 +79,6 @@ const SceneBar: React.FC = ({ onMaximize?.(); }, [isSingleTab, onMaximize]); - const handleCreateSession = useCallback(async () => { - const target = pickWorkspaceForProjectChatSession(currentWorkspace, normalWorkspacesList); - if (!target) { - notificationService.warning(t('nav.sessions.needProjectWorkspaceForSession'), { duration: 4500 }); - return; - } - activateScene('session'); - try { - if (target.id !== currentWorkspace?.id) { - await setActiveWorkspace(target.id); - } - const reusableId = findReusableEmptySessionId(target, 'agentic'); - if (reusableId) { - await flowChatManager.switchChatSession(reusableId); - return; - } - await flowChatManager.createChatSession(flowChatSessionConfigForWorkspace(target), 'agentic'); - } catch (err) { - log.error('Failed to create session', err); - } - }, [activateScene, currentWorkspace, normalWorkspacesList, setActiveWorkspace, t]); - return (
= ({ const subtitle = (tab.id === 'session' && sessionTitle ? sessionTitle : undefined) ?? (tab.id === 'settings' && settingsTabTitle ? settingsTabTitle : undefined); - const actionTitle = tab.id === 'session' ? t('nav.sessions.newCodeSession') : undefined; return ( = ({ def={{ ...def, label: translatedLabel }} isActive={tab.id === activeTabId} subtitle={subtitle} - onActionClick={tab.id === 'session' ? handleCreateSession : undefined} - actionTitle={actionTitle} onActivate={activateScene} onClose={closeScene} /> diff --git a/src/web-ui/src/app/scenes/SceneViewport.tsx b/src/web-ui/src/app/scenes/SceneViewport.tsx index dff4d998..cc2e9e89 100644 --- a/src/web-ui/src/app/scenes/SceneViewport.tsx +++ b/src/web-ui/src/app/scenes/SceneViewport.tsx @@ -14,12 +14,13 @@ import { useSceneManager } from '../hooks/useSceneManager'; import { useI18n } from '@/infrastructure/i18n/hooks/useI18n'; import { useDialogCompletionNotify } from '../hooks/useDialogCompletionNotify'; import { ProcessingIndicator } from '@/flow_chat/components/modern/ProcessingIndicator'; +import SettingsScene from './settings/SettingsScene'; +import AssistantScene from './assistant/AssistantScene'; import './SceneViewport.scss'; const SessionScene = lazy(() => import('./session/SessionScene')); const TerminalScene = lazy(() => import('./terminal/TerminalScene')); 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 AgentsScene = lazy(() => import('./agents/AgentsScene')); @@ -27,7 +28,6 @@ const SkillsScene = lazy(() => import('./skills/SkillsScene')); const MiniAppGalleryScene = lazy(() => import('./miniapps/MiniAppGalleryScene')); const BrowserScene = lazy(() => import('./browser/BrowserScene')); const MermaidEditorScene = lazy(() => import('./mermaid/MermaidEditorScene')); -const AssistantScene = lazy(() => import('./assistant/AssistantScene')); const InsightsScene = lazy(() => import('./my-agent/InsightsScene')); const ShellScene = lazy(() => import('./shell/ShellScene')); const WelcomeScene = lazy(() => import('./welcome/WelcomeScene')); @@ -59,31 +59,36 @@ const SceneViewport: React.FC = ({ workspacePath, isEntering return (
- - -
- )} - > - {openTabs.map(tab => ( + {openTabs.map(tab => { + const isActive = tab.id === activeTabId; + return (
- {renderScene(tab.id, workspacePath, isEntering)} + + +
+ ) : null + } + > + {renderScene(tab.id, workspacePath, isEntering)} +
- ))} - + ); + })}
); diff --git a/src/web-ui/src/app/scenes/settings/SettingsScene.tsx b/src/web-ui/src/app/scenes/settings/SettingsScene.tsx index 9bf7d587..ad371815 100644 --- a/src/web-ui/src/app/scenes/settings/SettingsScene.tsx +++ b/src/web-ui/src/app/scenes/settings/SettingsScene.tsx @@ -6,21 +6,20 @@ * driven by settingsStore.activeTab. */ -import React, { lazy, Suspense } from 'react'; +import React from 'react'; import { useSettingsStore } from './settingsStore'; import './SettingsScene.scss'; +import AIModelConfig from '../../../infrastructure/config/components/AIModelConfig'; +import SessionConfig from '../../../infrastructure/config/components/SessionConfig'; +import AIRulesMemoryConfig from '../../../infrastructure/config/components/AIRulesMemoryConfig'; +import McpToolsConfig from '../../../infrastructure/config/components/McpToolsConfig'; +import EditorConfig from '../../../infrastructure/config/components/EditorConfig'; +import BasicsConfig from '../../../infrastructure/config/components/BasicsConfig'; -const AIModelConfig = lazy(() => import('../../../infrastructure/config/components/AIModelConfig')); -const SessionConfig = lazy(() => import('../../../infrastructure/config/components/SessionConfig')); -const AIRulesMemoryConfig = lazy(() => import('../../../infrastructure/config/components/AIRulesMemoryConfig')); -const McpToolsConfig = lazy(() => import('../../../infrastructure/config/components/McpToolsConfig')); -// const LspConfig = lazy(() => import('../../../infrastructure/config/components/LspConfig')); -const EditorConfig = lazy(() => import('../../../infrastructure/config/components/EditorConfig')); -const BasicsConfig = lazy(() => import('../../../infrastructure/config/components/BasicsConfig')); const SettingsScene: React.FC = () => { const activeTab = useSettingsStore(s => s.activeTab); - let Content: React.LazyExoticComponent | null = null; + let Content: React.ComponentType | null = null; switch (activeTab) { case 'basics': Content = BasicsConfig; break; @@ -28,19 +27,16 @@ const SettingsScene: React.FC = () => { case 'session-config': Content = SessionConfig; break; case 'ai-context': Content = AIRulesMemoryConfig; break; case 'mcp-tools': Content = McpToolsConfig; break; - // case 'lsp': Content = LspConfig; break; case 'editor': Content = EditorConfig; break; } return (
- }> - {Content && ( -
- -
- )} -
+ {Content && ( +
+ +
+ )}
); }; diff --git a/src/web-ui/src/component-library/components/ConfirmDialog/ConfirmDialog.scss b/src/web-ui/src/component-library/components/ConfirmDialog/ConfirmDialog.scss index 83bc566b..e87a5281 100644 --- a/src/web-ui/src/component-library/components/ConfirmDialog/ConfirmDialog.scss +++ b/src/web-ui/src/component-library/components/ConfirmDialog/ConfirmDialog.scss @@ -4,7 +4,7 @@ @use '../../styles/tokens' as *; .confirm-dialog { - padding: $size-gap-4; + padding: $size-gap-4 $size-gap-5; text-align: center; min-width: 320px; max-width: 480px; @@ -87,10 +87,15 @@ &__actions { display: flex; + flex-wrap: wrap; gap: $size-gap-2; - justify-content: center; + justify-content: flex-end; + align-items: center; + width: 100%; + box-sizing: border-box; + padding-inline: $size-gap-3; - .bitfun-btn { + .btn { min-width: 80px; } } diff --git a/src/web-ui/src/flow_chat/components/ChatInput.tsx b/src/web-ui/src/flow_chat/components/ChatInput.tsx index 3273a16f..49d47f78 100644 --- a/src/web-ui/src/flow_chat/components/ChatInput.tsx +++ b/src/web-ui/src/flow_chat/components/ChatInput.tsx @@ -5,7 +5,7 @@ import React, { useRef, useCallback, useEffect, useReducer, useState, useMemo } from 'react'; import { Trans, useTranslation } from 'react-i18next'; -import { ArrowUp, Image, ChevronsUp, ChevronsDown, RotateCcw, Plus, X, Sparkles, Loader2, ChevronRight, Files } from 'lucide-react'; +import { ArrowUp, Image, ChevronsUp, ChevronsDown, RotateCcw, Plus, X, Sparkles, Loader2, ChevronRight, Files, MessageSquarePlus } from 'lucide-react'; import { ContextDropZone, useContextStore } from '../../shared/context-system'; import { useActiveSessionState } from '../hooks/useActiveSessionState'; import { RichTextInput, type MentionState } from './RichTextInput'; @@ -989,6 +989,25 @@ export const ChatInput: React.FC = ({ setSlashCommandState({ isActive: false, kind: 'modes', query: '', selectedIndex: 0 }); window.setTimeout(() => richTextInputRef.current?.focus(), 0); }, [inputState.value, isBtwSession, setQueuedInput]); + + const handleBoostStartBtw = useCallback( + (e: React.SyntheticEvent) => { + e.stopPropagation(); + if (!currentSessionId) { + notificationService.error(t('btw.noSession', { defaultValue: 'No active session for /btw' })); + return; + } + if (isBtwSession) { + notificationService.warning( + t('btw.nestedDisabled', { defaultValue: 'Side questions cannot create another side question' }) + ); + return; + } + selectSlashCommandAction('btw'); + dispatchMode({ type: 'CLOSE_DROPDOWN' }); + }, + [currentSessionId, isBtwSession, selectSlashCommandAction, t] + ); const handleKeyDown = useCallback((e: React.KeyboardEvent) => { // Local /btw shortcut (Ctrl/Cmd+Alt+B) should work even when ChatInput is focused. @@ -1890,6 +1909,23 @@ export const ChatInput: React.FC = ({
+ + {!!currentSessionId && !isBtwSession && ( + <> +
+
e.key === 'Enter' && handleBoostStartBtw(e)} + > + + {t('chatInput.boostStartBtw')} +
+ + )}
)} @@ -1917,7 +1953,7 @@ export const ChatInput: React.FC = ({ )} - + {renderActionButton()} diff --git a/src/web-ui/src/flow_chat/components/modern/FlowChatHeader.scss b/src/web-ui/src/flow_chat/components/modern/FlowChatHeader.scss index 467f8fc4..846dd480 100644 --- a/src/web-ui/src/flow_chat/components/modern/FlowChatHeader.scss +++ b/src/web-ui/src/flow_chat/components/modern/FlowChatHeader.scss @@ -55,14 +55,6 @@ } } - &__btw-create { - flex: 0 0 auto; - - &:not(:disabled):hover { - background: color-mix(in srgb, var(--element-bg-soft) 82%, transparent); - } - } - &__turn-nav { position: relative; display: flex; diff --git a/src/web-ui/src/flow_chat/components/modern/FlowChatHeader.tsx b/src/web-ui/src/flow_chat/components/modern/FlowChatHeader.tsx index 52d26a78..cd84d5c5 100644 --- a/src/web-ui/src/flow_chat/components/modern/FlowChatHeader.tsx +++ b/src/web-ui/src/flow_chat/components/modern/FlowChatHeader.tsx @@ -5,7 +5,7 @@ */ import React, { useEffect, useMemo, useRef, useState } from 'react'; -import { ChevronDown, ChevronUp, CornerUpLeft, List, MessageSquarePlus } from 'lucide-react'; +import { ChevronDown, ChevronUp, CornerUpLeft, List } from 'lucide-react'; import { Tooltip, IconButton } from '@/component-library'; import { useTranslation } from 'react-i18next'; import { globalEventBus } from '@/infrastructure/event-bus'; @@ -35,8 +35,6 @@ export interface FlowChatHeaderProps { btwOrigin?: Session['btwOrigin'] | null; /** BTW parent session title. */ btwParentTitle?: string; - /** Creates a new BTW thread from the current session. */ - onCreateBtwSession?: () => void; /** Ordered turn summaries used by header navigation. */ turns?: FlowChatHeaderTurnSummary[]; /** Jump to a specific turn. */ @@ -54,7 +52,6 @@ export const FlowChatHeader: React.FC = ({ sessionId, btwOrigin, btwParentTitle = '', - onCreateBtwSession, turns = [], onJumpToTurn, onJumpToPreviousTurn, @@ -80,9 +77,6 @@ export const FlowChatHeader: React.FC = ({ title: parentLabel, defaultValue: `Go back to the source session: ${parentLabel}`, }); - const createBtwTooltip = t('flowChatHeader.btwCreateTooltip', { - defaultValue: 'Start a quick side question', - }); const turnListTooltip = t('flowChatHeader.turnList', { defaultValue: 'Turn list', }); @@ -275,19 +269,6 @@ export const FlowChatHeader: React.FC = ({ )} - {onCreateBtwSession && ( - - - - )} ); diff --git a/src/web-ui/src/flow_chat/components/modern/ModernFlowChatContainer.tsx b/src/web-ui/src/flow_chat/components/modern/ModernFlowChatContainer.tsx index 5e9f4cd0..3031c27a 100644 --- a/src/web-ui/src/flow_chat/components/modern/ModernFlowChatContainer.tsx +++ b/src/web-ui/src/flow_chat/components/modern/ModernFlowChatContainer.tsx @@ -109,13 +109,6 @@ export const ModernFlowChatContainer: React.FC = ( handleCollapseGroup, ]); - const handleCreateBtwSession = useCallback(() => { - if (!activeSession?.sessionId) return; - window.dispatchEvent(new CustomEvent('fill-chat-input', { - detail: { message: '/btw ' } - })); - }, [activeSession?.sessionId]); - const turnSummaries = useMemo(() => { return (activeSession?.dialogTurns ?? []) .filter(turn => !!turn.userMessage) @@ -231,7 +224,6 @@ export const ModernFlowChatContainer: React.FC = ( sessionId={activeSession?.sessionId} btwOrigin={btwOrigin} btwParentTitle={btwParentTitle} - onCreateBtwSession={activeSession?.sessionId && !isBtwSession ? handleCreateBtwSession : undefined} turns={turnSummaries} onJumpToTurn={handleJumpToTurn} onJumpToPreviousTurn={handleJumpToPreviousTurn} diff --git a/src/web-ui/src/infrastructure/api/index.ts b/src/web-ui/src/infrastructure/api/index.ts index d7b31700..cc6ec7aa 100644 --- a/src/web-ui/src/infrastructure/api/index.ts +++ b/src/web-ui/src/infrastructure/api/index.ts @@ -31,11 +31,10 @@ import { sessionAPI } from './service-api/SessionAPI'; import { i18nAPI } from './service-api/I18nAPI'; import { btwAPI } from './service-api/BtwAPI'; import { editorAiAPI } from './service-api/EditorAiAPI'; -import { tokenUsageApi } from './tokenUsageApi'; import { insightsApi } from './insightsApi'; // Export API modules -export { workspaceAPI, configAPI, aiApi, toolAPI, agentAPI, systemAPI, projectAPI, diffAPI, snapshotAPI, globalAPI, contextAPI, cronAPI, gitAPI, gitAgentAPI, gitRepoHistoryAPI, startchatAgentAPI, sessionAPI, i18nAPI, btwAPI, editorAiAPI, tokenUsageApi, insightsApi }; +export { workspaceAPI, configAPI, aiApi, toolAPI, agentAPI, systemAPI, projectAPI, diffAPI, snapshotAPI, globalAPI, contextAPI, cronAPI, gitAPI, gitAgentAPI, gitRepoHistoryAPI, startchatAgentAPI, sessionAPI, i18nAPI, btwAPI, editorAiAPI, insightsApi }; // Export types export type { GitRepoHistory }; @@ -62,7 +61,6 @@ export const bitfunAPI = { i18n: i18nAPI, btw: btwAPI, editorAi: editorAiAPI, - tokenUsage: tokenUsageApi, insights: insightsApi, }; diff --git a/src/web-ui/src/infrastructure/api/tokenUsageApi.ts b/src/web-ui/src/infrastructure/api/tokenUsageApi.ts deleted file mode 100644 index 4a5db51d..00000000 --- a/src/web-ui/src/infrastructure/api/tokenUsageApi.ts +++ /dev/null @@ -1,150 +0,0 @@ -// Token usage API client - -import { invoke } from '@tauri-apps/api/core'; - -export interface TokenUsageRecord { - model_id: string; - session_id: string; - turn_id: string; - timestamp: string; - input_tokens: number; - output_tokens: number; - cached_tokens: number; - total_tokens: number; -} - -export interface ModelTokenStats { - model_id: string; - total_input: number; - total_output: number; - total_cached: number; - total_tokens: number; - session_count: number; - request_count: number; - first_used: string | null; - last_used: string | null; -} - -export interface SessionTokenStats { - session_id: string; - model_id: string; - total_input: number; - total_output: number; - total_cached: number; - total_tokens: number; - request_count: number; - created_at: string; - last_updated: string; -} - -export type TimeRange = - | 'Today' - | 'ThisWeek' - | 'ThisMonth' - | 'All' - | { Custom: { start: string; end: string } }; - -export type StatsTimeRange = 'Last7Days' | 'Last30Days' | 'All'; - -export interface TokenUsageSummary { - total_input: number; - total_output: number; - total_cached: number; - total_tokens: number; - by_model: Record; - by_session: Record; - record_count: number; -} - -export const tokenUsageApi = { - /** - * Convert StatsTimeRange to a TimeRange with custom date calculation - */ - _toTimeRange(range: StatsTimeRange): TimeRange | undefined { - if (range === 'All') return undefined; - const now = new Date(); - const start = new Date(); - if (range === 'Last7Days') { - start.setDate(now.getDate() - 7); - } else if (range === 'Last30Days') { - start.setDate(now.getDate() - 30); - } - return { Custom: { start: start.toISOString(), end: now.toISOString() } }; - }, - - /** - * Get token statistics for a specific model - */ - async getModelStats( - modelId: string, - statsTimeRange?: StatsTimeRange, - includeSubagent?: boolean - ): Promise { - const timeRange = statsTimeRange ? this._toTimeRange(statsTimeRange) : undefined; - const needFiltered = timeRange !== undefined || includeSubagent; - return invoke('get_model_token_stats', { - request: { - model_id: modelId, - time_range: needFiltered ? (timeRange ?? 'All') : undefined, - include_subagent: includeSubagent ?? false, - } - }); - }, - - /** - * Get token statistics for all models - */ - async getAllModelStats(): Promise> { - const response = await invoke<{ stats: Record }>('get_all_model_token_stats', {}); - return response.stats; - }, - - /** - * Get token statistics for a specific session - */ - async getSessionStats(sessionId: string): Promise { - return invoke('get_session_token_stats', { - request: { session_id: sessionId } - }); - }, - - /** - * Query token usage with filters - */ - async queryTokenUsage( - modelId?: string, - sessionId?: string, - timeRange: TimeRange = 'All', - limit?: number, - offset?: number, - includeSubagent?: boolean - ): Promise { - return invoke('query_token_usage', { - request: { - model_id: modelId, - session_id: sessionId, - time_range: timeRange, - limit, - offset, - include_subagent: includeSubagent ?? false, - } - }); - }, - - /** - * Clear token statistics for a specific model - */ - async clearModelStats(modelId: string): Promise { - return invoke('clear_model_token_stats', { - request: { model_id: modelId } - }); - }, - - /** - * Clear all token statistics - */ - async clearAllStats(): Promise { - return invoke('clear_all_token_stats', {}); - } -}; - diff --git a/src/web-ui/src/infrastructure/config/components/AIModelConfig.tsx b/src/web-ui/src/infrastructure/config/components/AIModelConfig.tsx index 60b432f5..b9f2f47c 100644 --- a/src/web-ui/src/infrastructure/config/components/AIModelConfig.tsx +++ b/src/web-ui/src/infrastructure/config/components/AIModelConfig.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { Plus, Edit2, Trash2, Wifi, Loader, AlertTriangle, X, Settings, ExternalLink, BarChart3, Eye, EyeOff } from 'lucide-react'; +import { Plus, Edit2, Trash2, Wifi, Loader, AlertTriangle, X, Settings, ExternalLink, Eye, EyeOff } from 'lucide-react'; import { Button, Switch, Select, IconButton, NumberInput, Card, Checkbox, Modal, Input, Textarea, type SelectOption } from '@/component-library'; import { AIModelConfig as AIModelConfigType, @@ -14,7 +14,6 @@ import { aiApi, systemAPI } from '@/infrastructure/api'; import { useNotification } from '@/shared/notification-system'; import { ConfigPageHeader, ConfigPageLayout, ConfigPageContent, ConfigPageSection, ConfigPageRow, ConfigCollectionItem } from './common'; import DefaultModelConfig from './DefaultModelConfig'; -import TokenStatsModal from './TokenStatsModal'; import { createLogger } from '@/shared/utils/logger'; import { translateConnectionTestMessage } from '@/shared/utils/aiConnectionTestMessages'; import './AIModelConfig.scss'; @@ -203,10 +202,7 @@ const AIModelConfig: React.FC = () => { const notification = useNotification(); const [showAdvancedSettings, setShowAdvancedSettings] = useState(false); - - const [showTokenStats, setShowTokenStats] = useState(false); - const [selectedModelForStats, setSelectedModelForStats] = useState<{ id: string; name: string } | null>(null); - + const [creationMode, setCreationMode] = useState<'selection' | 'form' | null>(null); const [selectedProviderId, setSelectedProviderId] = useState(null); @@ -1763,17 +1759,6 @@ const AIModelConfig: React.FC = () => { > {isTesting ? : } - - - - ) : ( -
-

{t('tokenStats.noData')}

-
- )} - - - ); -}; - -export default TokenStatsModal; diff --git a/src/web-ui/src/infrastructure/theme/core/ThemeService.ts b/src/web-ui/src/infrastructure/theme/core/ThemeService.ts index 0ac54571..68857174 100644 --- a/src/web-ui/src/infrastructure/theme/core/ThemeService.ts +++ b/src/web-ui/src/infrastructure/theme/core/ThemeService.ts @@ -10,7 +10,6 @@ import { ThemeEvent, ThemeEventListener, ThemeHooks, - ThemeAdapter, SYSTEM_THEME_ID, ThemeSelectionId, } from '../types'; @@ -46,7 +45,6 @@ export class ThemeService { private systemThemeCleanup: (() => void) | null = null; private listeners: Map> = new Map(); private hooks: ThemeHooks = {}; - private adapters: ThemeAdapter[] = []; constructor() { this.initializeBuiltinThemes(); @@ -758,36 +756,6 @@ export class ThemeService { }; } - - async importTheme(themeExport: ThemeExport): Promise { - const { theme } = themeExport; - - - const validation = this.validateTheme(theme); - if (!validation.valid) { - log.error('Theme validation failed', { errors: validation.errors }); - throw new Error('Invalid theme configuration'); - } - - - this.registerTheme(theme); - - - await this.saveUserThemes(); - } - - - async importWithAdapter(data: any): Promise { - const adapter = this.adapters.find(a => a.supports(data)); - if (!adapter) { - throw new Error('No suitable adapter found for this theme format'); - } - - const theme = adapter.convert(data); - this.registerTheme(theme); - await this.saveUserThemes(); - } - @@ -867,13 +835,6 @@ export class ThemeService { registerHooks(hooks: ThemeHooks): void { this.hooks = { ...this.hooks, ...hooks }; } - - - - - registerAdapter(adapter: ThemeAdapter): void { - this.adapters.push(adapter); - } } diff --git a/src/web-ui/src/infrastructure/theme/hooks/useTheme.ts b/src/web-ui/src/infrastructure/theme/hooks/useTheme.ts index 1ffa8a19..f82a2b2c 100644 --- a/src/web-ui/src/infrastructure/theme/hooks/useTheme.ts +++ b/src/web-ui/src/infrastructure/theme/hooks/useTheme.ts @@ -79,7 +79,6 @@ export function useThemeManagement() { addTheme, removeTheme, exportTheme, - importTheme, refreshThemes, } = useThemeStore(); @@ -88,7 +87,6 @@ export function useThemeManagement() { addTheme, removeTheme, exportTheme, - importTheme, refreshThemes, }; } diff --git a/src/web-ui/src/infrastructure/theme/store/themeStore.ts b/src/web-ui/src/infrastructure/theme/store/themeStore.ts index cdd24e69..4ee23866 100644 --- a/src/web-ui/src/infrastructure/theme/store/themeStore.ts +++ b/src/web-ui/src/infrastructure/theme/store/themeStore.ts @@ -23,7 +23,6 @@ interface ThemeState { addTheme: (theme: ThemeConfig) => Promise; removeTheme: (themeId: ThemeId) => Promise; exportTheme: (themeId: ThemeId) => any; - importTheme: (themeData: any) => Promise; } @@ -157,27 +156,6 @@ export const useThemeStore = create((set) => ({ exportTheme: (themeId: ThemeId) => { return themeService.exportTheme(themeId); }, - - - importTheme: async (themeData: any) => { - set({ loading: true, error: null }); - - try { - await themeService.importTheme(themeData); - - const themes = themeService.getThemeList(); - set({ - themes, - loading: false, - }); - } catch (error) { - log.error('Failed to import theme', error); - set({ - loading: false, - error: error instanceof Error ? error.message : 'Failed to import theme', - }); - } - }, })); diff --git a/src/web-ui/src/locales/en-US/common.json b/src/web-ui/src/locales/en-US/common.json index c1f6d4f2..758525bc 100644 --- a/src/web-ui/src/locales/en-US/common.json +++ b/src/web-ui/src/locales/en-US/common.json @@ -90,7 +90,8 @@ "persona": "Nursery — create and manage all assistant instances", "agents": "Which agents it can call", "skills": "What it knows — specialized knowledge files", - "tools": "What it can use — built-in tools & MCP services" + "tools": "What it can use — built-in tools & MCP services", + "addWorkspace": "Add workspace" }, "sections": { "workspace": "Workspace", diff --git a/src/web-ui/src/locales/en-US/flow-chat.json b/src/web-ui/src/locales/en-US/flow-chat.json index c4366564..2fc56597 100644 --- a/src/web-ui/src/locales/en-US/flow-chat.json +++ b/src/web-ui/src/locales/en-US/flow-chat.json @@ -184,6 +184,7 @@ "addModeTooltip": "Add Plan or Debug", "boostSectionAgent": "Agent", "boostSectionContext": "Context", + "boostStartBtw": "Start side question", "boostAddContext": "Reference file or folder", "boostSkills": "Skills", "boostSkillsLoading": "Loading skills…", diff --git a/src/web-ui/src/locales/en-US/settings/ai-model.json b/src/web-ui/src/locales/en-US/settings/ai-model.json index 0d856f09..ad06377e 100644 --- a/src/web-ui/src/locales/en-US/settings/ai-model.json +++ b/src/web-ui/src/locales/en-US/settings/ai-model.json @@ -207,27 +207,7 @@ "newConfig": "New Configuration", "addModel": "Add Model", "addProvider": "Add Provider", - "createFirst": "Create First Configuration", - "viewStats": "View Statistics" - }, - "tokenStats": { - "title": "Token Usage Statistics", - "loading": "Loading...", - "noData": "No statistics data available", - "totalTokens": "Total Tokens", - "inputTokens": "Input Tokens", - "outputTokens": "Output Tokens", - "cachedTokens": "Cached Tokens", - "sessionCount": "Sessions", - "requestCount": "API Requests", - "firstUsed": "First Used", - "lastUsed": "Last Used", - "clearStats": "Clear Statistics", - "confirmClear": "Are you sure you want to clear all statistics for this model? This action cannot be undone.", - "rangeAll": "All", - "range30Days": "Last 30 Days", - "range7Days": "Last 7 Days", - "includeSubagent": "Include Subagent" + "createFirst": "Create First Configuration" }, "empty": { "noModels": "No model configurations", diff --git a/src/web-ui/src/locales/en-US/settings/basics.json b/src/web-ui/src/locales/en-US/settings/basics.json index 296a7a62..aaebc1b7 100644 --- a/src/web-ui/src/locales/en-US/settings/basics.json +++ b/src/web-ui/src/locales/en-US/settings/basics.json @@ -6,17 +6,13 @@ "hint": "Interface language and visual theme", "language": "Language", "themes": "Themes", - "importTheme": "Import Theme", - "importing": "Importing...", "builtIn": "Built-in", "custom": "Custom", "export": "Export", "delete": "Delete", "confirmDelete": "Are you sure you want to delete theme \"{{name}}\"?", - "importSuccess": "Theme imported successfully!", - "importFailed": "Failed to import theme. Please check the file format.", "languageRowHint": "Choose one language pack as the active UI language.", - "themeRowHint": "Pick an installed theme or manage custom themes.", + "themeRowHint": "Choose the interface color theme.", "systemTheme": "Match system", "systemThemeDescription": "Use light or dark theme based on your OS appearance. Updates when the system switches.", "presets": { diff --git a/src/web-ui/src/locales/en-US/settings/editor.json b/src/web-ui/src/locales/en-US/settings/editor.json index ef0c30ef..53c163be 100644 --- a/src/web-ui/src/locales/en-US/settings/editor.json +++ b/src/web-ui/src/locales/en-US/settings/editor.json @@ -21,7 +21,6 @@ }, "appearance": { "font": "Font", - "fontHint": "Select preferred monospace font", "fontWeight": "Font Weight", "fontWeightNormal": "Normal", "fontWeightBold": "Bold", diff --git a/src/web-ui/src/locales/zh-CN/common.json b/src/web-ui/src/locales/zh-CN/common.json index e3173b8e..8d26fb9a 100644 --- a/src/web-ui/src/locales/zh-CN/common.json +++ b/src/web-ui/src/locales/zh-CN/common.json @@ -90,7 +90,8 @@ "persona": "助理 — 创建与管理所有助理实例", "agents": "它能调用哪些 Agent", "skills": "它懂什么专项知识 — 技能知识文件", - "tools": "它能用什么工具 — 内置工具与 MCP 服务" + "tools": "它能用什么工具 — 内置工具与 MCP 服务", + "addWorkspace": "添加工作区" }, "sections": { "workspace": "工作区", diff --git a/src/web-ui/src/locales/zh-CN/flow-chat.json b/src/web-ui/src/locales/zh-CN/flow-chat.json index 2721c5e2..e7cf1b5a 100644 --- a/src/web-ui/src/locales/zh-CN/flow-chat.json +++ b/src/web-ui/src/locales/zh-CN/flow-chat.json @@ -184,6 +184,7 @@ "addModeTooltip": "附加 Plan 或 Debug", "boostSectionAgent": "智能体", "boostSectionContext": "上下文", + "boostStartBtw": "发起侧问", "boostAddContext": "引用文件或文件夹", "boostSkills": "Skills", "boostSkillsLoading": "正在加载 Skills…", diff --git a/src/web-ui/src/locales/zh-CN/settings/ai-model.json b/src/web-ui/src/locales/zh-CN/settings/ai-model.json index 64a92596..28557e16 100644 --- a/src/web-ui/src/locales/zh-CN/settings/ai-model.json +++ b/src/web-ui/src/locales/zh-CN/settings/ai-model.json @@ -207,27 +207,7 @@ "newConfig": "新建配置", "addModel": "添加模型", "addProvider": "添加服务商", - "createFirst": "创建第一个配置", - "viewStats": "查看统计" - }, - "tokenStats": { - "title": "Token消耗统计", - "loading": "加载中...", - "noData": "暂无统计数据", - "totalTokens": "总消耗", - "inputTokens": "输入Token", - "outputTokens": "输出Token", - "cachedTokens": "缓存Token", - "sessionCount": "会话数", - "requestCount": "请求次数", - "firstUsed": "首次使用", - "lastUsed": "最近使用", - "clearStats": "清除统计", - "confirmClear": "确定要清除该模型的所有统计数据吗?此操作不可恢复。", - "rangeAll": "全部", - "range30Days": "近30天", - "range7Days": "近7天", - "includeSubagent": "包含子代理" + "createFirst": "创建第一个配置" }, "empty": { "noModels": "暂无模型配置", diff --git a/src/web-ui/src/locales/zh-CN/settings/basics.json b/src/web-ui/src/locales/zh-CN/settings/basics.json index 3ee93682..ab2bb1ea 100644 --- a/src/web-ui/src/locales/zh-CN/settings/basics.json +++ b/src/web-ui/src/locales/zh-CN/settings/basics.json @@ -6,17 +6,13 @@ "hint": "界面语言与视觉主题", "language": "语言", "themes": "主题", - "importTheme": "导入主题", - "importing": "导入中...", "builtIn": "内置", "custom": "自定义", "export": "导出", "delete": "删除", "confirmDelete": "确定删除主题 \"{{name}}\" 吗?", - "importSuccess": "主题导入成功!", - "importFailed": "主题导入失败,请检查文件格式。", "languageRowHint": "选择一个语言包作为当前界面语言。", - "themeRowHint": "选择已安装的主题或管理自定义主题。", + "themeRowHint": "选择界面配色主题。", "systemTheme": "跟随系统", "systemThemeDescription": "根据系统浅色/深色外观自动在亮暗主题间切换,并随系统切换而更新。", "presets": { diff --git a/src/web-ui/src/locales/zh-CN/settings/editor.json b/src/web-ui/src/locales/zh-CN/settings/editor.json index 0188ceaa..3716ac4c 100644 --- a/src/web-ui/src/locales/zh-CN/settings/editor.json +++ b/src/web-ui/src/locales/zh-CN/settings/editor.json @@ -21,7 +21,6 @@ }, "appearance": { "font": "字体", - "fontHint": "选择首选等宽字体", "fontWeight": "字体粗细", "fontWeightNormal": "正常", "fontWeightBold": "粗体", diff --git a/src/web-ui/src/shared/notification-system/components/NotificationContainer.scss b/src/web-ui/src/shared/notification-system/components/NotificationContainer.scss index dff0ee9e..e4879cdb 100644 --- a/src/web-ui/src/shared/notification-system/components/NotificationContainer.scss +++ b/src/web-ui/src/shared/notification-system/components/NotificationContainer.scss @@ -5,7 +5,8 @@ .notification-container { position: fixed; bottom: 60px; - right: 16px; + left: 16px; + right: auto; z-index: $z-notification; display: flex; flex-direction: column-reverse; @@ -31,7 +32,8 @@ @media (max-width: 768px) { .notification-container { left: 12px; - right: 12px; + right: auto; + max-width: calc(100vw - 24px); bottom: 64px; .notification-item, diff --git a/src/web-ui/src/shared/notification-system/services/NotificationService.ts b/src/web-ui/src/shared/notification-system/services/NotificationService.ts index 472e788e..ff623bb8 100644 --- a/src/web-ui/src/shared/notification-system/services/NotificationService.ts +++ b/src/web-ui/src/shared/notification-system/services/NotificationService.ts @@ -34,7 +34,10 @@ class NotificationService { error(message: string, options?: ToastOptions): string { - return this.toast('error', message, options); + return this.toast('error', message, { + ...options, + duration: options?.duration ?? 0 + }); } diff --git a/src/web-ui/src/shared/notification-system/store/NotificationStore.ts b/src/web-ui/src/shared/notification-system/store/NotificationStore.ts index 41005c80..89f84594 100644 --- a/src/web-ui/src/shared/notification-system/store/NotificationStore.ts +++ b/src/web-ui/src/shared/notification-system/store/NotificationStore.ts @@ -11,7 +11,7 @@ const DEFAULT_CONFIG: NotificationConfig = { defaultDuration: 3000, enableSound: false, enableAnimation: true, - position: 'bottom-right' + position: 'bottom-left' };