diff --git a/src/crates/core/src/service/config/manager.rs b/src/crates/core/src/service/config/manager.rs index d49050ccc..68dda4130 100644 --- a/src/crates/core/src/service/config/manager.rs +++ b/src/crates/core/src/service/config/manager.rs @@ -619,7 +619,11 @@ pub(crate) fn migrate_0_0_0_to_1_0_0(mut config: Value) -> BitFunResult { app.insert( "ai_experience".to_string(), serde_json::json!({ - "enable_session_title_generation": true + "enable_session_title_generation": true, + "enable_visual_mode": false, + "enable_agent_companion": true, + "show_thinking_process": true, + "show_completed_thinking_item": true }), ); } diff --git a/src/crates/core/src/service/config/types.rs b/src/crates/core/src/service/config/types.rs index 8c6b873db..193a257ab 100644 --- a/src/crates/core/src/service/config/types.rs +++ b/src/crates/core/src/service/config/types.rs @@ -109,6 +109,10 @@ pub struct AIExperienceConfig { pub enable_visual_mode: bool, /// Whether to show the pixel Agent companion in the collapsed chat input. pub enable_agent_companion: bool, + /// Whether to show model thinking process in FlowChat. + pub show_thinking_process: bool, + /// Whether completed thinking blocks remain as expandable collapsed items. + pub show_completed_thinking_item: bool, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -1214,6 +1218,8 @@ impl Default for AIExperienceConfig { enable_session_title_generation: true, enable_visual_mode: false, enable_agent_companion: true, + show_thinking_process: true, + show_completed_thinking_item: true, } } } diff --git a/src/web-ui/src/app/scenes/skills/SkillsScene.scss b/src/web-ui/src/app/scenes/skills/SkillsScene.scss index 1b31d5c45..d799db4b6 100644 --- a/src/web-ui/src/app/scenes/skills/SkillsScene.scss +++ b/src/web-ui/src/app/scenes/skills/SkillsScene.scss @@ -641,7 +641,7 @@ $skills-column-gap: clamp(14px, 1.6vw, 20px); .skills-split__pagination--installed { flex-shrink: 0; - padding: $size-gap-2 0 0; + padding: $size-gap-2 $size-gap-2 $size-gap-3; margin-top: auto; } diff --git a/src/web-ui/src/flow_chat/components/modern/ExploreGroupRenderer.tsx b/src/web-ui/src/flow_chat/components/modern/ExploreGroupRenderer.tsx index 4859da3fd..6c0dcfa23 100644 --- a/src/web-ui/src/flow_chat/components/modern/ExploreGroupRenderer.tsx +++ b/src/web-ui/src/flow_chat/components/modern/ExploreGroupRenderer.tsx @@ -14,6 +14,7 @@ import { FlowToolCard } from '../FlowToolCard'; import { ModelThinkingDisplay } from '../../tool-cards/ModelThinkingDisplay'; import { useToolCardHeightContract } from '../../tool-cards/useToolCardHeightContract'; import { useFlowChatStaticContext, useFlowChatViewContext } from './FlowChatContext'; +import { aiExperienceConfigService } from '@/infrastructure/config/services/AIExperienceConfigService'; import './ExploreRegion.scss'; export interface ExploreGroupRendererProps { @@ -28,6 +29,13 @@ export const ExploreGroupRenderer: React.FC = React.m const { t } = useTranslation('flow-chat'); const containerRef = useRef(null); const [scrollState, setScrollState] = useState({ hasScroll: false, atTop: true, atBottom: true }); + const [thinkingDisplaySettings, setThinkingDisplaySettings] = useState(() => { + const settings = aiExperienceConfigService.getSettings(); + return { + showThinkingProcess: settings.show_thinking_process, + showCompletedThinkingItem: settings.show_completed_thinking_item, + }; + }); const { exploreGroupStates, @@ -63,6 +71,29 @@ export const ExploreGroupRenderer: React.FC = React.m const isCollapsed = !isExpanded; const allowManualToggle = !isGroupStreaming; + useEffect(() => { + let cancelled = false; + aiExperienceConfigService.getSettingsAsync().then(settings => { + if (cancelled) return; + setThinkingDisplaySettings({ + showThinkingProcess: settings.show_thinking_process, + showCompletedThinkingItem: settings.show_completed_thinking_item, + }); + }); + + const unsubscribe = aiExperienceConfigService.addChangeListener(settings => { + setThinkingDisplaySettings({ + showThinkingProcess: settings.show_thinking_process, + showCompletedThinkingItem: settings.show_completed_thinking_item, + }); + }); + + return () => { + cancelled = true; + unsubscribe(); + }; + }, []); + const checkScrollState = useCallback(() => { const el = containerRef.current; if (!el) { @@ -146,10 +177,14 @@ export const ExploreGroupRenderer: React.FC = React.m // Build summary text with i18n. const displaySummary = useMemo(() => { - const { readCount, searchCount, thinkingCount } = stats; + const { readCount, searchCount, commandCount, thinkingCount } = stats; const parts: string[] = []; - if (thinkingCount > 0) { + if ( + thinkingCount > 0 && + thinkingDisplaySettings.showThinkingProcess && + thinkingDisplaySettings.showCompletedThinkingItem + ) { parts.push(t('exploreRegion.thinkingCount', { count: thinkingCount })); } if (readCount > 0) { @@ -158,13 +193,16 @@ export const ExploreGroupRenderer: React.FC = React.m if (searchCount > 0) { parts.push(t('exploreRegion.searchCount', { count: searchCount })); } + if (commandCount > 0) { + parts.push(t('exploreRegion.commandCount', { count: commandCount })); + } if (parts.length === 0) { return t('exploreRegion.exploreCount', { count: allItems.length }); } return parts.join(t('exploreRegion.separator')); - }, [stats, allItems.length, t]); + }, [stats, allItems.length, t, thinkingDisplaySettings]); const handleToggle = useCallback(() => { if (isCollapsed) { 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 3add8c736..8af67a05a 100644 --- a/src/web-ui/src/flow_chat/components/modern/FlowChatHeader.scss +++ b/src/web-ui/src/flow_chat/components/modern/FlowChatHeader.scss @@ -70,7 +70,8 @@ } // ==================== Search (right-aligned strip; does not span full header) ==================== - &__search-btn { + &__search-btn, + &__thinking-toggle { flex: 0 0 auto; &:not(:disabled):hover { @@ -78,6 +79,10 @@ } } + &__thinking-toggle { + color: var(--color-text-secondary); + } + &__search { flex: 0 1 auto; min-width: 0; 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 9e7536c47..3e8fd32a9 100644 --- a/src/web-ui/src/flow_chat/components/modern/FlowChatHeader.tsx +++ b/src/web-ui/src/flow_chat/components/modern/FlowChatHeader.tsx @@ -7,15 +7,19 @@ */ import React, { useEffect, useRef, useState, useCallback } from 'react'; -import { ChevronDown, ChevronUp, CornerUpLeft, List, Search, X } from 'lucide-react'; +import { ChevronDown, ChevronUp, CornerUpLeft, Eye, EyeOff, List, Search, X } from 'lucide-react'; import { IconButton, Input } from '@/component-library'; import { useTranslation } from 'react-i18next'; import { globalEventBus } from '@/infrastructure/event-bus'; import { SessionFilesBadge } from './SessionFilesBadge'; import type { Session } from '../../types/flow-chat'; import { FLOWCHAT_FOCUS_ITEM_EVENT, type FlowChatFocusItemRequest } from '../../events/flowchatNavigation'; +import { aiExperienceConfigService, type AIExperienceSettings } from '@/infrastructure/config/services/AIExperienceConfigService'; +import { createLogger } from '@/shared/utils/logger'; import './FlowChatHeader.scss'; +const log = createLogger('FlowChatHeader'); + export interface FlowChatHeaderTurnSummary { turnId: string; turnIndex: number; @@ -79,6 +83,9 @@ export const FlowChatHeader: React.FC = ({ }) => { const { t } = useTranslation('flow-chat'); const [isSearchOpen, setIsSearchOpen] = useState(false); + const [aiExperienceSettings, setAiExperienceSettings] = useState(() => + aiExperienceConfigService.getSettings() + ); const searchInputRef = useRef(null); const parentLabel = btwParentTitle || t('btw.parent', { defaultValue: 'parent session' }); @@ -95,8 +102,28 @@ export const FlowChatHeader: React.FC = ({ const turnListTooltip = t('flowChatHeader.turnList', { defaultValue: 'Turn list', }); + const keepThinkingItemEnabled = aiExperienceSettings.show_completed_thinking_item; + const thinkingItemToggleTooltip = keepThinkingItemEnabled + ? t('flowChatHeader.hideCompletedThinkingItems', { defaultValue: 'Hide completed thinking items' }) + : t('flowChatHeader.showCompletedThinkingItems', { defaultValue: 'Show completed thinking items' }); const hasTurnNavigation = turns.length > 0 && !!onJumpToTurn; + useEffect(() => { + let cancelled = false; + aiExperienceConfigService.getSettingsAsync().then(settings => { + if (!cancelled) { + setAiExperienceSettings(settings); + } + }); + + const unsubscribe = aiExperienceConfigService.addChangeListener(setAiExperienceSettings); + + return () => { + cancelled = true; + unsubscribe(); + }; + }, []); + // When collapsing the turn list with an active query, reopen the header search bar. const prevTurnListOpenRef = useRef(turnListOpen); useEffect(() => { @@ -173,6 +200,20 @@ export const FlowChatHeader: React.FC = ({ onTurnListOpenChange?.(!turnListOpen); }; + const handleToggleCompletedThinkingItems = async () => { + const nextSettings: AIExperienceSettings = { + ...aiExperienceSettings, + show_completed_thinking_item: !keepThinkingItemEnabled, + }; + setAiExperienceSettings(nextSettings); + try { + await aiExperienceConfigService.saveSettings(nextSettings); + } catch (error) { + log.error('Failed to toggle completed thinking items', error); + setAiExperienceSettings(aiExperienceSettings); + } + }; + if (!visible) { return null; } @@ -248,6 +289,20 @@ export const FlowChatHeader: React.FC = ({ ) : null} + {!turnListOpen && !isSearchOpen && ( + + {keepThinkingItemEnabled ? : } + + )} {!turnListOpen && !isSearchOpen && ( r.id).join('-'), rounds: group.rounds, allItems: group.allItems, - stats: { readCount: group.readCount, searchCount: group.searchCount, thinkingCount: group.thinkingCount }, + stats: { + readCount: group.readCount, + searchCount: group.searchCount, + commandCount: group.commandCount, + thinkingCount: group.thinkingCount, + }, isGroupStreaming, isLastGroupInTurn: isLastGroup, } diff --git a/src/web-ui/src/flow_chat/tool-cards/CompactToolCard.scss b/src/web-ui/src/flow_chat/tool-cards/CompactToolCard.scss index 40fdf0a8e..b02485289 100644 --- a/src/web-ui/src/flow_chat/tool-cards/CompactToolCard.scss +++ b/src/web-ui/src/flow_chat/tool-cards/CompactToolCard.scss @@ -13,6 +13,32 @@ gap: 0; font-size: var(--flowchat-font-size-base); line-height: 1.65; + + &.compact-tool-card-wrapper--expanded { + margin: 4px 0; + border: 1px solid var(--border-base); + border-radius: 8px; + background: var(--color-bg-flowchat); + overflow: hidden; + box-shadow: + 0 1px 4px rgba(0, 0, 0, 0.08), + inset 0 1px 0 rgba(255, 255, 255, 0.02); + + &:hover { + border-color: var(--border-medium); + } + + > .compact-tool-card { + padding: 8px 10px !important; + } + } + + &.compact-tool-card-wrapper--expanded.requires-confirmation { + border-color: var(--color-warning, #f59e0b); + box-shadow: + 0 0 0 1px rgba(245, 158, 11, 0.14), + 0 0 12px rgba(245, 158, 11, 0.16); + } } /* Tool item wrapper around .compact-tool-card-wrapper (see ModelRoundItem / FlowItemRenderer). @@ -224,18 +250,15 @@ /* ========== Expanded content area ========== */ .compact-tool-card-expanded { position: relative; - margin-top: 8px; - margin-left: 1rem; - padding: 12px; - background: var(--color-bg-primary); - border: 1px solid var(--border-base); - border-radius: 6px; + margin: 0; + padding: 10px; + background: transparent; + border: none; + border-top: 1px solid var(--border-subtle); + border-radius: 0; animation: expandDown 0.2s ease-out; - box-shadow: - 0 2px 8px rgba(0, 0, 0, 0.15), - inset 0 1px 0 rgba(255, 255, 255, 0.02); - backdrop-filter: blur(8px); - + box-shadow: none; + backdrop-filter: none; } /* ========== Common expanded content styles ========== */ @@ -243,12 +266,13 @@ .compact-detail-info-inline { display: flex; flex-wrap: wrap; - gap: 12px; + gap: 8px 12px; align-items: center; - margin-bottom: 12px; + margin: 0 0 10px; padding-bottom: 8px; border-bottom: 1px solid var(--border-subtle); font-size: 11px; + line-height: 1.45; } .compact-detail-inline-item { @@ -275,19 +299,19 @@ .compact-expanded-results-list { display: flex; flex-direction: column; - gap: 12px; + gap: 8px; max-height: 400px; overflow-y: auto; } .compact-expanded-result-item { - padding: 8px; - border-radius: 4px; + padding: 6px 0; + border-radius: 0; transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); &:hover { - background: var(--element-bg-soft); + background: transparent; } } @@ -341,10 +365,11 @@ margin-top: 0; pre { + margin: 0; background: var(--color-bg-secondary); border: 1px solid var(--border-subtle); - border-radius: 4px; - padding: 10px; + border-radius: 6px; + padding: 8px 10px; color: var(--color-text-primary); font-family: var(--tool-card-font-mono); font-size: 11px; @@ -362,11 +387,9 @@ @keyframes expandDown { from { opacity: 0; - transform: translateY(-10px); } to { opacity: 1; - transform: translateY(0); } } diff --git a/src/web-ui/src/flow_chat/tool-cards/CompactToolCard.tsx b/src/web-ui/src/flow_chat/tool-cards/CompactToolCard.tsx index 873d5d17c..79377796e 100644 --- a/src/web-ui/src/flow_chat/tool-cards/CompactToolCard.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/CompactToolCard.tsx @@ -4,7 +4,7 @@ * * Features: * - Collapsed: transparent background, no border, single-line display - * - Expanded: shows detailed content with dark background box + * - Expanded: keeps header and details in one lightweight card surface * - Simple gray style, text brightens on hover */ @@ -58,10 +58,17 @@ export const CompactToolCard: React.FC = ({ status === 'streaming' || status === 'running' || status === 'analyzing'; + const hasExpandedContent = Boolean(expandedContent); return (
= ({ thin const displayContent = useTypewriter(content, isActive); const [isExpanded, setIsExpanded] = useState(isLastItem); + const [thinkingDisplaySettings, setThinkingDisplaySettings] = useState(() => { + const settings = aiExperienceConfigService.getSettings(); + return { + showThinkingProcess: settings.show_thinking_process, + showCompletedThinkingItem: settings.show_completed_thinking_item, + }; + }); const userToggledRef = useRef(false); const { applyExpandedState } = useToolCardHeightContract({ toolId: thinkingItem.id, @@ -43,6 +51,29 @@ export const ModelThinkingDisplay: React.FC = ({ thin }, }); + useEffect(() => { + let cancelled = false; + aiExperienceConfigService.getSettingsAsync().then(settings => { + if (cancelled) return; + setThinkingDisplaySettings({ + showThinkingProcess: settings.show_thinking_process, + showCompletedThinkingItem: settings.show_completed_thinking_item, + }); + }); + + const unsubscribe = aiExperienceConfigService.addChangeListener(settings => { + setThinkingDisplaySettings({ + showThinkingProcess: settings.show_thinking_process, + showCompletedThinkingItem: settings.show_completed_thinking_item, + }); + }); + + return () => { + cancelled = true; + unsubscribe(); + }; + }, []); + useEffect(() => { if (userToggledRef.current) return; if (!isLastItem && isExpanded) { @@ -109,6 +140,13 @@ export const ModelThinkingDisplay: React.FC = ({ thin const renderedContent = isActive ? displayContent : content; + if ( + !thinkingDisplaySettings.showThinkingProcess || + (!isActive && !thinkingDisplaySettings.showCompletedThinkingItem) + ) { + return null; + } + return (
.base-tool-card-expanded)):not(:has(> .base-tool-card-error)) { + margin: 0; + border-color: transparent; + border-radius: 0; + background: transparent; + box-shadow: none; + backdrop-filter: none; + overflow: visible; + + &:hover, + &:active { + border-color: transparent; + box-shadow: none; + } + + &.requires-confirmation { + border-color: transparent; + border-width: 1px; + box-shadow: none; + animation: none; + + &:hover { + border-color: transparent; + box-shadow: none; + } + } + + .base-tool-card-header { + --tool-card-header-pad-y: 0px; + --tool-card-header-pad-x: 0px; + + gap: 8px; + padding: 0 8px 0 0; + line-height: 1.65; + overflow: visible; + + &::before, + &::after { + display: none; + } + } + + .tool-card-icon-slot { + align-self: center; + padding-right: 0; + margin-right: 0; + + &::after { + display: none; + } + } + + .tool-card-icon-marks { + width: auto; + height: auto; + } + + .tool-card-icon.tool-identifier-icon.tool-card-icon-main svg { + width: 12px; + height: 12px; + } + + .tool-card-action, + .tool-card-content { + font-size: var(--flowchat-font-size-base); + line-height: 1.65; + color: var(--color-text-muted); + } + + &:hover { + .tool-card-action, + .tool-card-content { + color: var(--color-text-primary); + } + + .terminal-command { + color: var(--color-text-primary); + } + } +} + /* ========== Fix scrollbar visibility on card hover ========== */ .base-tool-card-wrapper.terminal-tool-card:hover { .terminal-execution-output, diff --git a/src/web-ui/src/flow_chat/tool-cards/index.ts b/src/web-ui/src/flow_chat/tool-cards/index.ts index 9a5f2020a..7fcac0c3f 100644 --- a/src/web-ui/src/flow_chat/tool-cards/index.ts +++ b/src/web-ui/src/flow_chat/tool-cards/index.ts @@ -521,11 +521,11 @@ export type { PlanDisplayProps } from './CreatePlanDisplay'; import type { FlowItem, FlowToolItem } from '../types/flow-chat'; /** - * Collapsible explorer tools (only these 5). + * Collapsible explorer tools. * They are auto-collapsed during streaming to reduce visual noise. */ export const COLLAPSIBLE_TOOL_NAMES = new Set([ - 'Read', 'LS', 'Grep', 'Glob', 'WebSearch' + 'Read', 'LS', 'Grep', 'Glob', 'WebSearch', 'Bash' ]); /** Read tools (counted in readCount). */ @@ -534,6 +534,9 @@ export const READ_TOOL_NAMES = new Set(['Read', 'LS']); /** Search tools (counted in searchCount). */ export const SEARCH_TOOL_NAMES = new Set(['Grep', 'Glob', 'WebSearch']); +/** Command tools (counted in commandCount). */ +export const COMMAND_TOOL_NAMES = new Set(['Bash']); + /** Check whether a tool is collapsible. */ export function isCollapsibleTool(toolName: string): boolean { return COLLAPSIBLE_TOOL_NAMES.has(toolName); @@ -544,7 +547,7 @@ export function isCollapsibleTool(toolName: string): boolean { * - Subagent items are never collapsed. * - Text needs context (use isCollapsibleItemWithContext). * - Thinking can be collapsed with explorer tools. - * - Only the 5 explorer tools are collapsible. + * - Only explorer tools are collapsible. */ export function isCollapsibleItem(item: FlowItem): boolean { // Subagent items are never collapsed. @@ -556,7 +559,7 @@ export function isCollapsibleItem(item: FlowItem): boolean { // Thinking can be collapsed with explorer tools. if (item.type === 'thinking') return true; - // Tools: only the 5 explorer tools are collapsible. + // Tools: only explorer tools are collapsible. if (item.type === 'tool') { return isCollapsibleTool((item as FlowToolItem).toolName); } @@ -597,7 +600,7 @@ export function isCollapsibleItemWithContext( return false; } - // Tools: only the 5 explorer tools are collapsible. + // Tools: only explorer tools are collapsible. if (item.type === 'tool') { return isCollapsibleTool((item as FlowToolItem).toolName); } diff --git a/src/web-ui/src/infrastructure/config/components/PersonalizationConfig.tsx b/src/web-ui/src/infrastructure/config/components/PersonalizationConfig.tsx index 00d5cfb9f..7eaddf70d 100644 --- a/src/web-ui/src/infrastructure/config/components/PersonalizationConfig.tsx +++ b/src/web-ui/src/infrastructure/config/components/PersonalizationConfig.tsx @@ -112,6 +112,39 @@ const PersonalizationConfig: React.FC = () => { + + +
+ updateSetting('show_thinking_process', e.target.checked)} + size="small" + /> +
+
+ +
+ updateSetting('show_completed_thinking_item', e.target.checked)} + size="small" + /> +
+
+
+