feat: enhance UI components, update theme handling, and improve tool expansion logic for better performance

This commit is contained in:
Bohdan Triapitsyn
2025-12-16 13:14:28 +02:00
parent 8f6e6db5c5
commit c87815a2a6
8 changed files with 324 additions and 100 deletions
+35 -22
View File
@@ -26,6 +26,11 @@ import type { TurnGroupingContext } from './hooks/useTurnGrouping';
const ToolOutputDialog = React.lazy(() => import('./message/ToolOutputDialog'));
const DETAILED_DEFAULT_TOOLS = new Set(['task', 'edit', 'multiedit', 'write', 'bash']);
const isDetailedDefaultTool = (toolName: unknown): boolean =>
typeof toolName === 'string' && DETAILED_DEFAULT_TOOLS.has(toolName.toLowerCase());
function useStickyDisplayValue<T>(value: T | null | undefined): T | null | undefined {
const ref = React.useRef<{ hasValue: boolean; value: T | null | undefined }>({ hasValue: false, value: undefined as T | null | undefined });
@@ -124,6 +129,10 @@ const ChatMessage: React.FC<ChatMessageProps> = ({
content: '',
});
React.useEffect(() => {
setExpandedTools(new Set());
}, [message.info.id, toolCallExpansion]);
const messageRole = React.useMemo(() => deriveMessageRole(message.info), [message.info]);
const isUser = messageRole.isUser;
@@ -309,44 +318,48 @@ const ChatMessage: React.FC<ChatMessageProps> = ({
const effectiveExpandedTools = React.useMemo(() => {
// 'collapsed': Activity and tools start collapsed
// 'activity': Activity expanded, tools collapsed
// 'detailed': Activity and tools expanded
// 'activity': Activity expanded, tools collapsed
// 'detailed': Activity expanded, only key tools expanded
if (toolCallExpansion === 'collapsed' || toolCallExpansion === 'activity') {
// Tools default collapsed: expandedTools contains IDs of tools that ARE expanded
return expandedTools;
}
// 'detailed': Tools default expanded
// Collect all relevant tool IDs (from this message and the entire turn if we're rendering a progressive group)
const allToolIds = new Set<string>();
// 1. Add tools from this message
// 'detailed': expand only allowlisted tools by default.
// expandedTools acts as a "toggled" set (XOR with defaults).
const defaultExpandedToolIds = new Set<string>();
for (const part of toolParts) {
if (part.id) {
allToolIds.add(part.id);
const toolName = (part as { tool?: unknown }).tool;
if (part.id && isDetailedDefaultTool(toolName)) {
defaultExpandedToolIds.add(part.id);
}
}
// 2. If we're rendering a progressive group for the turn, include all turn tools
if (turnGroupingContext?.isFirstAssistantInTurn) {
for (const activity of turnGroupingContext.activityParts) {
if (activity.kind === 'tool' && activity.part.id) {
allToolIds.add(activity.part.id);
if (activity.kind !== 'tool') {
continue;
}
const toolPart = activity.part as unknown as { id?: string; tool?: unknown };
if (toolPart.id && isDetailedDefaultTool(toolPart.tool)) {
defaultExpandedToolIds.add(toolPart.id);
}
}
}
// expandedTools contains IDs of tools that ARE collapsed (inverted)
// Return a set of all tool IDs EXCEPT those in expandedTools
const effective = new Set<string>();
for (const id of allToolIds) {
if (!expandedTools.has(id)) {
const effective = new Set(defaultExpandedToolIds);
for (const id of expandedTools) {
if (effective.has(id)) {
effective.delete(id);
} else {
effective.add(id);
}
}
return effective;
}, [toolCallExpansion, expandedTools, toolParts, turnGroupingContext]);
}, [expandedTools, toolCallExpansion, toolParts, turnGroupingContext]);
const agentMention = React.useMemo(() => {
if (!isUser) {
@@ -778,6 +778,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({ className }) => {
}
// Add to recent models on successful selection
addRecentModel(providerId, modelId);
setAgentMenuOpen(false);
if (isCompact) {
closeMobilePanel();
}
@@ -1160,7 +1161,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({ className }) => {
{!mobileModelQuery && favoriteModelsList.length > 0 && (
<div className="rounded-xl border border-border/40 bg-background/95">
<div className="px-2 py-1.5 text-xs font-semibold text-muted-foreground uppercase tracking-wider">
<RiStarFill className="h-3 w-3 inline-block mr-1.5 text-yellow-500" />
<RiStarFill className="h-3 w-3 inline-block mr-1.5 text-primary" />
Favorites
</div>
<div className="flex flex-col border-t border-border/30">
@@ -1635,7 +1636,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({ className }) => {
{favoriteModelsList.length > 0 && (
<DropdownMenuSub>
<DropdownMenuSubTrigger className="typography-meta">
<RiStarFill className="h-3 w-3 flex-shrink-0 mr-2 text-yellow-500" />
<RiStarFill className="h-3 w-3 flex-shrink-0 mr-2 text-primary" />
Favorites
</DropdownMenuSubTrigger>
<DropdownMenuSubContent
@@ -1668,8 +1669,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({ className }) => {
<DropdownMenuItem
key={`fav-${providerID}-${modelID}`}
className="typography-meta"
onSelect={(e) => {
e.preventDefault();
onSelect={() => {
handleProviderAndModelChange(providerID, modelID);
}}
>
@@ -1757,8 +1757,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({ className }) => {
<DropdownMenuItem
key={`recent-${providerID}-${modelID}`}
className="typography-meta"
onSelect={(e) => {
e.preventDefault();
onSelect={() => {
handleProviderAndModelChange(providerID, modelID);
}}
>
@@ -1873,8 +1872,7 @@ export const ModelControls: React.FC<ModelControlsProps> = ({ className }) => {
<DropdownMenuItem
key={model.id}
className="typography-meta"
onSelect={(e) => {
e.preventDefault();
onSelect={() => {
handleProviderAndModelChange(provider.id as string, model.id as string);
}}
>