Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/crates/core/src/service/config/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -619,7 +619,11 @@ pub(crate) fn migrate_0_0_0_to_1_0_0(mut config: Value) -> BitFunResult<Value> {
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
}),
);
}
Expand Down
6 changes: 6 additions & 0 deletions src/crates/core/src/service/config/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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,
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/web-ui/src/app/scenes/skills/SkillsScene.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -28,6 +29,13 @@ export const ExploreGroupRenderer: React.FC<ExploreGroupRendererProps> = React.m
const { t } = useTranslation('flow-chat');
const containerRef = useRef<HTMLDivElement>(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,
Expand Down Expand Up @@ -63,6 +71,29 @@ export const ExploreGroupRenderer: React.FC<ExploreGroupRendererProps> = 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) {
Expand Down Expand Up @@ -146,10 +177,14 @@ export const ExploreGroupRenderer: React.FC<ExploreGroupRendererProps> = 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) {
Expand All @@ -158,13 +193,16 @@ export const ExploreGroupRenderer: React.FC<ExploreGroupRendererProps> = 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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,19 @@
}

// ==================== Search (right-aligned strip; does not span full header) ====================
&__search-btn {
&__search-btn,
&__thinking-toggle {
flex: 0 0 auto;

&:not(:disabled):hover {
background: color-mix(in srgb, var(--element-bg-soft) 82%, transparent);
}
}

&__thinking-toggle {
color: var(--color-text-secondary);
}

&__search {
flex: 0 1 auto;
min-width: 0;
Expand Down
57 changes: 56 additions & 1 deletion src/web-ui/src/flow_chat/components/modern/FlowChatHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -79,6 +83,9 @@ export const FlowChatHeader: React.FC<FlowChatHeaderProps> = ({
}) => {
const { t } = useTranslation('flow-chat');
const [isSearchOpen, setIsSearchOpen] = useState(false);
const [aiExperienceSettings, setAiExperienceSettings] = useState<AIExperienceSettings>(() =>
aiExperienceConfigService.getSettings()
);
const searchInputRef = useRef<HTMLInputElement | null>(null);

const parentLabel = btwParentTitle || t('btw.parent', { defaultValue: 'parent session' });
Expand All @@ -95,8 +102,28 @@ export const FlowChatHeader: React.FC<FlowChatHeaderProps> = ({
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(() => {
Expand Down Expand Up @@ -173,6 +200,20 @@ export const FlowChatHeader: React.FC<FlowChatHeaderProps> = ({
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;
}
Expand Down Expand Up @@ -248,6 +289,20 @@ export const FlowChatHeader: React.FC<FlowChatHeaderProps> = ({
</IconButton>
</div>
) : null}
{!turnListOpen && !isSearchOpen && (
<IconButton
className="flowchat-header__thinking-toggle"
variant="ghost"
size="xs"
onClick={handleToggleCompletedThinkingItems}
tooltip={thinkingItemToggleTooltip}
aria-label={thinkingItemToggleTooltip}
aria-pressed={keepThinkingItemEnabled}
data-testid="flowchat-header-thinking-toggle"
>
{keepThinkingItemEnabled ? <Eye size={14} /> : <EyeOff size={14} />}
</IconButton>
)}
{!turnListOpen && !isSearchOpen && (
<IconButton
className="flowchat-header__search-btn"
Expand Down
20 changes: 16 additions & 4 deletions src/web-ui/src/flow_chat/store/modernFlowChatStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { create } from 'zustand';
import { useShallow } from 'zustand/react/shallow';
import { immer } from 'zustand/middleware/immer';
import type { Session, DialogTurn, ModelRound, FlowItem, FlowToolItem } from '../types/flow-chat';
import { isCollapsibleTool, READ_TOOL_NAMES, SEARCH_TOOL_NAMES } from '../tool-cards';
import { COMMAND_TOOL_NAMES, isCollapsibleTool, READ_TOOL_NAMES, SEARCH_TOOL_NAMES } from '../tool-cards';
import { flowChatStore } from './FlowChatStore';

/**
Expand All @@ -17,6 +17,7 @@ import { flowChatStore } from './FlowChatStore';
export interface ExploreGroupStats {
readCount: number;
searchCount: number;
commandCount: number;
thinkingCount: number;
}

Expand Down Expand Up @@ -114,22 +115,24 @@ function isExploreOnlyRound(round: ModelRound): boolean {
/**
* Compute statistics for a single ModelRound
*/
function computeRoundStats(round: ModelRound): { readCount: number; searchCount: number; thinkingCount: number } {
function computeRoundStats(round: ModelRound): ExploreGroupStats {
let readCount = 0;
let searchCount = 0;
let commandCount = 0;
let thinkingCount = 0;

for (const item of round.items) {
if (item.type === 'tool') {
const toolName = (item as FlowToolItem).toolName;
if (READ_TOOL_NAMES.has(toolName)) readCount++;
else if (SEARCH_TOOL_NAMES.has(toolName)) searchCount++;
else if (COMMAND_TOOL_NAMES.has(toolName)) commandCount++;
} else if (item.type === 'thinking') {
thinkingCount++;
}
}

return { readCount, searchCount, thinkingCount };
return { readCount, searchCount, commandCount, thinkingCount };
}

let cachedSession: Session | null = null;
Expand Down Expand Up @@ -196,6 +199,7 @@ function canReuseVirtualItem(previous: VirtualItem | undefined, next: VirtualIte
previousExploreGroup.data.isLastGroupInTurn === next.data.isLastGroupInTurn &&
previousExploreGroup.data.stats.readCount === next.data.stats.readCount &&
previousExploreGroup.data.stats.searchCount === next.data.stats.searchCount &&
previousExploreGroup.data.stats.commandCount === next.data.stats.commandCount &&
previousExploreGroup.data.stats.thinkingCount === next.data.stats.thinkingCount &&
areRoundArraysReferentiallyEqual(previousExploreGroup.data.rounds, next.data.rounds) &&
areFlowItemArraysReferentiallyEqual(previousExploreGroup.data.allItems, next.data.allItems)
Expand Down Expand Up @@ -271,6 +275,7 @@ export function sessionToVirtualItems(session: Session | null): VirtualItem[] {
allItems: FlowItem[];
readCount: number;
searchCount: number;
commandCount: number;
thinkingCount: number;
startIndex: number;
endIndex: number;
Expand All @@ -288,6 +293,7 @@ export function sessionToVirtualItems(session: Session | null): VirtualItem[] {
currentGroup.allItems.push(...round.items);
currentGroup.readCount += stats.readCount;
currentGroup.searchCount += stats.searchCount;
currentGroup.commandCount += stats.commandCount;
currentGroup.thinkingCount += stats.thinkingCount;
currentGroup.endIndex = index;
} else {
Expand All @@ -296,6 +302,7 @@ export function sessionToVirtualItems(session: Session | null): VirtualItem[] {
allItems: [...round.items],
readCount: stats.readCount,
searchCount: stats.searchCount,
commandCount: stats.commandCount,
thinkingCount: stats.thinkingCount,
startIndex: index,
endIndex: index,
Expand Down Expand Up @@ -330,7 +337,12 @@ export function sessionToVirtualItems(session: Session | null): VirtualItem[] {
groupId: group.rounds.map(r => 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,
}
Expand Down
Loading