diff --git a/src/web-ui/src/component-library/components/Markdown/Markdown.scss b/src/web-ui/src/component-library/components/Markdown/Markdown.scss index 340e0471..210f8bc9 100644 --- a/src/web-ui/src/component-library/components/Markdown/Markdown.scss +++ b/src/web-ui/src/component-library/components/Markdown/Markdown.scss @@ -210,7 +210,7 @@ .markdown-renderer .inline-code { - padding: 0.2em 0.4em; + padding: 0.1em 0.4em; margin: 0 0.05em; font-size: 0.9em; background: rgba(255, 255, 255, 0.05); @@ -224,6 +224,8 @@ color: #a8b4c8; font-weight: 500; transition: all 0.15s ease; + box-decoration-break: clone; + -webkit-box-decoration-break: clone; } diff --git a/src/web-ui/src/flow_chat/components/ChatInput.scss b/src/web-ui/src/flow_chat/components/ChatInput.scss index ceb39924..3f6da031 100644 --- a/src/web-ui/src/flow_chat/components/ChatInput.scss +++ b/src/web-ui/src/flow_chat/components/ChatInput.scss @@ -580,8 +580,8 @@ background: color-mix(in srgb, var(--color-text-muted) 14%, transparent); &--Plan { - background: rgba(6, 182, 212, 0.2); - color: rgba(14, 116, 144, 0.98); + background: rgba(245, 158, 11, 0.15); + color: rgba(180, 110, 0, 0.98); } &--debug { diff --git a/src/web-ui/src/flow_chat/components/ChatInput.tsx b/src/web-ui/src/flow_chat/components/ChatInput.tsx index 0d5891e9..81973a17 100644 --- a/src/web-ui/src/flow_chat/components/ChatInput.tsx +++ b/src/web-ui/src/flow_chat/components/ChatInput.tsx @@ -184,6 +184,7 @@ export const ChatInput: React.FC = ({ const setChatInputActive = useChatInputState(state => state.setActive); const setChatInputExpanded = useChatInputState(state => state.setExpanded); + const setChatInputHeight = useChatInputState(state => state.setInputHeight); useEffect(() => { const unsubscribe = FlowChatStore.getInstance().subscribe(setFlowChatState); @@ -1328,10 +1329,13 @@ export const ChatInput: React.FC = ({ useEffect(() => { const handleClickOutside = (event: MouseEvent) => { + const target = event.target as Node; + // Do not collapse when clicking the scroll-to-latest bar. + if ((target as Element)?.closest?.('.scroll-to-latest-bar')) return; if ( inputState.isActive && containerRef.current && - !containerRef.current.contains(event.target as Node) + !containerRef.current.contains(target) ) { if (inputState.value.trim() === '') { dispatchInput({ type: 'DEACTIVATE' }); @@ -1344,6 +1348,19 @@ export const ChatInput: React.FC = ({ document.removeEventListener('mousedown', handleClickOutside); }; }, [inputState.isActive, inputState.value]); + + useEffect(() => { + const dropZone = containerRef.current?.closest('.bitfun-chat-input-drop-zone') as HTMLElement | null; + const el = dropZone ?? containerRef.current; + if (!el) return; + const observer = new ResizeObserver(() => { + setChatInputHeight(el.offsetHeight); + }); + observer.observe(el); + setChatInputHeight(el.offsetHeight); + return () => observer.disconnect(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); const renderActionButton = () => { if (!derivedState) return ; diff --git a/src/web-ui/src/flow_chat/components/PlannerButton.scss b/src/web-ui/src/flow_chat/components/PlannerButton.scss index 2a71c358..bf06ff55 100644 --- a/src/web-ui/src/flow_chat/components/PlannerButton.scss +++ b/src/web-ui/src/flow_chat/components/PlannerButton.scss @@ -23,8 +23,8 @@ border-radius: 11px; // Default state without glass effect - background: rgba(59, 130, 246, 0.08); - border: 1px solid rgba(59, 130, 246, 0.2); + background: rgba(245, 158, 11, 0.08); + border: 1px solid rgba(245, 158, 11, 0.2); color: rgba(255, 255, 255, 0.7); box-shadow: none; backdrop-filter: none; @@ -57,18 +57,18 @@ // Hover: enable glass effect &:hover { background: linear-gradient(135deg, - rgba(59, 130, 246, 0.20) 0%, - rgba(96, 165, 250, 0.17) 30%, - rgba(59, 130, 246, 0.13) 60%, - rgba(59, 130, 246, 0.23) 100% + rgba(245, 158, 11, 0.20) 0%, + rgba(251, 191, 36, 0.17) 30%, + rgba(245, 158, 11, 0.13) 60%, + rgba(245, 158, 11, 0.23) 100% ); - border-color: rgba(59, 130, 246, 0.5); + border-color: rgba(245, 158, 11, 0.5); color: var(--color-text-primary); box-shadow: - 0 10px 20px rgba(59, 130, 246, 0.25), - 0 6px 12px rgba(96, 165, 250, 0.2), + 0 10px 20px rgba(245, 158, 11, 0.2), + 0 6px 12px rgba(251, 191, 36, 0.15), inset 0 1px 0 rgba(255, 255, 255, 0.25), - inset 0 -1px 0 rgba(59, 130, 246, 0.15); + inset 0 -1px 0 rgba(245, 158, 11, 0.15); backdrop-filter: blur(12px) saturate(1.2) brightness(1.05); -webkit-backdrop-filter: blur(12px) saturate(1.2) brightness(1.05); transform: translateY(-1px); @@ -87,8 +87,8 @@ width: 5px; height: 5px; border-radius: 50%; - background: rgba(59, 130, 246, 0.9); - box-shadow: 0 0 6px rgba(59, 130, 246, 0.6); + background: rgba(245, 158, 11, 0.9); + box-shadow: 0 0 6px rgba(245, 158, 11, 0.6); animation: bitfun-planner-button-pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; flex-shrink: 0; position: relative; diff --git a/src/web-ui/src/flow_chat/components/ScrollToLatestBar.scss b/src/web-ui/src/flow_chat/components/ScrollToLatestBar.scss index 30c13256..e7883306 100644 --- a/src/web-ui/src/flow_chat/components/ScrollToLatestBar.scss +++ b/src/web-ui/src/flow_chat/components/ScrollToLatestBar.scss @@ -12,8 +12,8 @@ cursor: pointer; pointer-events: none; - // Default height: ChatInput active (box ~80px + bottom gap 16px = 96px, add 24px margin) - height: 120px; + // Default height: ChatInput active (box ~80px + bottom gap 16px = 96px, add 49px margin for content visibility) + height: 145px; transition: height 0.35s cubic-bezier(0.4, 0, 0.2, 1); @@ -138,7 +138,7 @@ // ========== Responsive tweaks ========== @media (max-width: 768px) { .scroll-to-latest-bar { - height: 110px; + height: 135px; &--input-collapsed { height: 70px; diff --git a/src/web-ui/src/flow_chat/components/ScrollToLatestBar.tsx b/src/web-ui/src/flow_chat/components/ScrollToLatestBar.tsx index e0b679aa..0b4120c4 100644 --- a/src/web-ui/src/flow_chat/components/ScrollToLatestBar.tsx +++ b/src/web-ui/src/flow_chat/components/ScrollToLatestBar.tsx @@ -14,6 +14,8 @@ interface ScrollToLatestBarProps { isInputExpanded?: boolean; /** Whether ChatInput is active. */ isInputActive?: boolean; + /** Measured height of the ChatInput container in pixels (0 if unknown). */ + inputHeight?: number; className?: string; } @@ -22,6 +24,7 @@ export const ScrollToLatestBar: React.FC = ({ onClick, isInputExpanded = false, isInputActive = true, + inputHeight = 0, className = '' }) => { const { t } = useTranslation('flow-chat'); @@ -35,9 +38,17 @@ export const ScrollToLatestBar: React.FC = ({ ? 'scroll-to-latest-bar--input-expanded' : ''; + // Dynamically offset the bar height based on measured ChatInput height. + // bottom: 16px (drop-zone offset) + inputHeight + 28px (content margin above input) + const dynamicStyle: React.CSSProperties = + isInputActive && !isInputExpanded && inputHeight > 0 + ? { height: `${inputHeight + 16 + 28}px` } + : {}; + return (
( /> )); - case 'critical': + case 'critical': { + // If next group is the matching subagent, skip here — rendered by subagent case. + const nextGroup = groupedItems[groupIndex + 1]; + const isTaskForSubagent = group.item.type === 'tool' && + nextGroup?.type === 'subagent' && + nextGroup.parentTaskToolId === group.item.id; + if (isTaskForSubagent) return null; return ( ( isLastItem={isLast} /> ); + } - case 'subagent': - return ( + case 'subagent': { + // If previous group is the matching task tool, wrap both in a unified card. + const prevGroup = groupedItems[groupIndex - 1]; + const hasPairedTask = prevGroup?.type === 'critical' && + prevGroup.item.type === 'tool' && + group.parentTaskToolId === prevGroup.item.id; + + const subagentContainer = ( ( roundId={round.id} /> ); + + if (hasPairedTask) { + return ( +
+ + {subagentContainer} +
+ ); + } + return subagentContainer; + } default: return null; diff --git a/src/web-ui/src/flow_chat/components/modern/SubagentItems.scss b/src/web-ui/src/flow_chat/components/modern/SubagentItems.scss index 89af287d..eda80228 100644 --- a/src/web-ui/src/flow_chat/components/modern/SubagentItems.scss +++ b/src/web-ui/src/flow_chat/components/modern/SubagentItems.scss @@ -1,52 +1,11 @@ /** * Subagent items styles. - * Controls visibility for subagent text and tool cards. - * Blends with the TaskTool card design. + * When expanded, the subagent container merges with the task tool card header + * to form a single unified card with squared top corners. */ -// Subagent outer wrapper with relative positioning context. .subagent-items-wrapper { position: relative; - margin-top: -6px; // Counter the base-tool-card margin. - - // Top fade mask. - &::before { - content: ''; - position: absolute; - top: 0; - left: 1px; - right: 1px; - height: 24px; - pointer-events: none; - z-index: 3; - background: linear-gradient( - to bottom, - var(--color-bg-flowchat, #121214) 0%, - color-mix(in srgb, var(--color-bg-flowchat, #121214) 80%, transparent) 40%, - color-mix(in srgb, var(--color-bg-flowchat, #121214) 40%, transparent) 70%, - transparent 100% - ); - } - - // Bottom fade mask (inside the border). - &::after { - content: ''; - position: absolute; - bottom: 1px; // Avoid the bottom border. - left: 1px; - right: 1px; - height: 24px; - pointer-events: none; - z-index: 3; - border-radius: 0 0 7px 7px; - background: linear-gradient( - to top, - var(--color-bg-flowchat, #121214) 0%, - color-mix(in srgb, var(--color-bg-flowchat, #121214) 80%, transparent) 40%, - color-mix(in srgb, var(--color-bg-flowchat, #121214) 40%, transparent) 70%, - transparent 100% - ); - } } .subagent-items-wrapper--collapsed { @@ -59,23 +18,17 @@ // Subagent container for items under the same parent task. .subagent-items-container { - // Blend with the TaskTool card. - margin-left: 0; - margin-right: 0; padding: 12px 14px; - - // Match BaseToolCard expanded area styling. + + // Continue the task card border on left/right/bottom; top border is hidden. background: var(--color-bg-flowchat); border: 1px solid var(--border-base); + border-top: none; + // Top corners are square to merge with the header card above. border-radius: 0 0 8px 8px; backdrop-filter: blur(8px); - - // Top divider. - border-top: none; - box-shadow: inset 0 1px 0 0 var(--border-base, rgba(107, 114, 128, 0.25)); - - // Fixed height to avoid growth during streaming output. - // Keep height even when content is short; overflow scrolls. + + // Fixed height; overflow scrolls. height: 400px; overflow-y: auto; } @@ -84,18 +37,15 @@ overflow: hidden; } -// TaskTool card bottom radius is controlled by TaskToolDisplay.scss. - // Keep the legacy subagent-item class for compatibility. .subagent-item { display: block; - + &.subagent-item--collapsed { display: none; } - + &.subagent-item--expanded { display: block; } } - diff --git a/src/web-ui/src/flow_chat/components/modern/UserMessageItem.scss b/src/web-ui/src/flow_chat/components/modern/UserMessageItem.scss index 2cd61d50..7947761a 100644 --- a/src/web-ui/src/flow_chat/components/modern/UserMessageItem.scss +++ b/src/web-ui/src/flow_chat/components/modern/UserMessageItem.scss @@ -14,10 +14,6 @@ position: relative; &:hover { - background: var(--element-bg-strong); - border-color: var(--border-prominent); - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12); - .user-message-item__copy-btn { opacity: 1; } @@ -72,9 +68,6 @@ user-select: text; transition: all 0.2s ease; - &:hover { - opacity: 0.8; - } } // Expanded state. diff --git a/src/web-ui/src/flow_chat/components/modern/VirtualMessageList.tsx b/src/web-ui/src/flow_chat/components/modern/VirtualMessageList.tsx index b488fa08..763dd2bf 100644 --- a/src/web-ui/src/flow_chat/components/modern/VirtualMessageList.tsx +++ b/src/web-ui/src/flow_chat/components/modern/VirtualMessageList.tsx @@ -264,6 +264,7 @@ export const VirtualMessageList = forwardRef((_, ref) => const isInputActive = useChatInputState(state => state.isActive); const isInputExpanded = useChatInputState(state => state.isExpanded); + const inputHeight = useChatInputState(state => state.inputHeight); const activeSessionState = useActiveSessionState(); const isProcessing = activeSessionState.isProcessing; @@ -1918,6 +1919,7 @@ export const VirtualMessageList = forwardRef((_, ref) => onClick={scrollToLatestEndPosition} isInputActive={isInputActive} isInputExpanded={isInputExpanded} + inputHeight={inputHeight} />
); diff --git a/src/web-ui/src/flow_chat/store/chatInputStateStore.ts b/src/web-ui/src/flow_chat/store/chatInputStateStore.ts index a095bf67..2e420fd3 100644 --- a/src/web-ui/src/flow_chat/store/chatInputStateStore.ts +++ b/src/web-ui/src/flow_chat/store/chatInputStateStore.ts @@ -9,16 +9,21 @@ interface ChatInputStateStore { isActive: boolean; /** Whether ChatInput is expanded (full height mode) */ isExpanded: boolean; + /** Measured height of the ChatInput container in pixels (0 if unknown) */ + inputHeight: number; setActive: (isActive: boolean) => void; setExpanded: (isExpanded: boolean) => void; + setInputHeight: (height: number) => void; } export const useChatInputState = create((set) => ({ isActive: true, isExpanded: false, + inputHeight: 0, setActive: (isActive) => set({ isActive }), setExpanded: (isExpanded) => set({ isExpanded }), + setInputHeight: (inputHeight) => set({ inputHeight }), })); diff --git a/src/web-ui/src/flow_chat/tool-cards/CreatePlanDisplay.scss b/src/web-ui/src/flow_chat/tool-cards/CreatePlanDisplay.scss index 87fe3b8a..3d8e39a3 100644 --- a/src/web-ui/src/flow_chat/tool-cards/CreatePlanDisplay.scss +++ b/src/web-ui/src/flow_chat/tool-cards/CreatePlanDisplay.scss @@ -17,6 +17,11 @@ &:hover { border-color: var(--border-medium); + box-shadow: + 0 4px 16px rgba(0, 0, 0, 0.2), + 0 2px 8px rgba(0, 0, 0, 0.12), + inset 0 1px 0 rgba(255, 255, 255, 0.04); + transform: translateY(-1px); } &--loading { @@ -35,7 +40,12 @@ align-items: center; justify-content: space-between; padding: 10px 14px; - background: rgba(255, 255, 255, 0.02); + background: linear-gradient( + 90deg, + rgba(245, 158, 11, 0.07) 0%, + rgba(245, 158, 11, 0.04) 50%, + rgba(255, 255, 255, 0.02) 100% + ); border-bottom: 1px solid var(--border-base); &--clickable { @@ -43,7 +53,12 @@ transition: background 0.15s ease; &:hover { - background: rgba(255, 255, 255, 0.06); + background: linear-gradient( + 90deg, + rgba(245, 158, 11, 0.13) 0%, + rgba(245, 158, 11, 0.08) 50%, + rgba(255, 255, 255, 0.05) 100% + ); .file-name { color: var(--tool-card-text-primary); @@ -61,15 +76,15 @@ display: flex; align-items: center; justify-content: center; - width: 24px; - height: 24px; - background: rgba(255, 255, 255, 0.06); + width: 28px; + height: 28px; + background: transparent; border-radius: 4px; - color: var(--tool-card-text-muted); + color: #f59e0b; svg { - width: 14px; - height: 14px; + width: 18px; + height: 18px; } } @@ -85,8 +100,16 @@ } .create-plan-content { + display: flex; + align-items: flex-start; + gap: 10px; padding: 14px 16px; + .plan-content-left { + flex: 1; + min-width: 0; + } + .plan-title { margin: 0 0 6px 0; font-size: 15px; @@ -101,55 +124,39 @@ color: var(--tool-card-text-secondary); line-height: 1.5; } - } - .create-plan-todos { - border-top: 1px solid var(--border-base); - padding: 10px 16px; - - .todos-header { - display: flex; + .todos-toggle-btn { + display: inline-flex; align-items: center; - justify-content: space-between; + justify-content: center; + flex-shrink: 0; + align-self: center; + width: 32px; + height: 32px; + padding: 0; + border: 1px solid transparent; + border-radius: 6px; + background: transparent; + color: var(--tool-card-text-muted); cursor: pointer; - padding: 4px 0; - margin: -4px 0; - border-radius: 4px; - transition: background 0.15s ease; - - &:hover { - background: rgba(255, 255, 255, 0.04); - } + transition: all 0.15s ease; - .todos-count { - font-size: 12px; - color: var(--tool-card-text-muted); + svg { + width: 22px; + height: 22px; } - - .todos-toggle-btn { - display: flex; - align-items: center; - justify-content: center; - width: 20px; - height: 20px; - padding: 0; - border: none; - background: transparent; - color: var(--tool-card-text-muted); - cursor: pointer; - border-radius: 4px; - transition: all 0.15s ease; - - &:hover { - color: var(--tool-card-text-primary); - background: rgba(255, 255, 255, 0.08); - } + + &:hover { + border-color: rgba(245, 158, 11, 0.35); + background: rgba(245, 158, 11, 0.07); + color: #f59e0b; } } - - &--expanded .todos-header { - margin-bottom: 8px; - } + } + + .create-plan-todos { + border-top: 1px solid var(--border-base); + padding: 10px 16px; .todos-list { display: flex; 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 d2116f4a..dd865bbb 100644 --- a/src/web-ui/src/flow_chat/tool-cards/CreatePlanDisplay.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/CreatePlanDisplay.tsx @@ -7,7 +7,7 @@ import React, { useState, useMemo, useCallback, useEffect, useRef } from 'react'; import { useTranslation } from 'react-i18next'; -import { FileText, Circle, Loader2, CheckCircle, CheckCircle2, PlayCircle, XCircle, ChevronDown, ChevronUp } from 'lucide-react'; +import { ClipboardList, Circle, Loader2, CheckCircle, CheckCircle2, PlayCircle, XCircle, ChevronsUpDown, ChevronsDownUp } from 'lucide-react'; import type { ToolCardProps } from '../types/flow-chat'; import { ideControl } from '@/shared/services/ide-control/api'; import { flowChatManager } from '@/flow_chat/services/FlowChatManager'; @@ -207,11 +207,6 @@ export const PlanDisplay: React.FC = ({ }; }, [effectiveCacheKey, planFilePath, initialName, initialOverview, initialTodos]); - const remainingTodos = useMemo(() => { - if (!planData?.todos) return 0; - return planData.todos.filter(t => t.status !== 'completed').length; - }, [planData]); - // Build button status transitions: build -> building -> built. const buildStatus = useMemo((): 'build' | 'building' | 'built' => { if (planData?.todos?.length) { @@ -331,7 +326,7 @@ ${JSON.stringify(simpleTodos, null, 2)} >
- +
{planFileName}
@@ -342,45 +337,45 @@ ${JSON.stringify(simpleTodos, null, 2)}
-

{planData.name}

-

{planData.overview}

-
- - {planData.todos && planData.todos.length > 0 && ( -
-
+

{planData.name}

+

{planData.overview}

+
+ {planData.todos && planData.todos.length > 0 && ( + + {isTodosExpanded ? : } + + )} +
+ + {planData.todos && planData.todos.length > 0 && isTodosExpanded && ( +
+
+ {planData.todos.map((todo, index) => ( +
+ {todo.status === 'completed' && ( + + )} + {todo.status === 'in_progress' && ( + + )} + {(!todo.status || todo.status === 'pending') && ( + + )} + {todo.status === 'cancelled' && ( + + )} + {todo.content} +
+ ))}
- {isTodosExpanded && ( -
- {planData.todos.map((todo, index) => ( -
- {todo.status === 'completed' && ( - - )} - {todo.status === 'in_progress' && ( - - )} - {(!todo.status || todo.status === 'pending') && ( - - )} - {todo.status === 'cancelled' && ( - - )} - {todo.content} -
- ))} -
- )}
)} diff --git a/src/web-ui/src/flow_chat/tool-cards/FileOperationToolCard.scss b/src/web-ui/src/flow_chat/tool-cards/FileOperationToolCard.scss index fdd9e765..5c941777 100644 --- a/src/web-ui/src/flow_chat/tool-cards/FileOperationToolCard.scss +++ b/src/web-ui/src/flow_chat/tool-cards/FileOperationToolCard.scss @@ -26,8 +26,9 @@ font-weight: 500; } - .delete-label { - color: var(--color-text-primary); + .delete-file-name { + text-decoration: line-through; + color: var(--color-text-muted); } } @@ -89,24 +90,16 @@ } } -.delete-label { - font-size: 10px; - font-weight: 500; +.delete-file-name { + text-decoration: line-through; color: var(--color-text-muted); - flex-shrink: 0; } .diff-preview-group { display: flex; align-items: center; gap: 3px; - padding: 2px 6px; - border: none; - border-radius: 20px; - background: transparent; - cursor: pointer; flex-shrink: 0; - transition: background 0.15s ease, color 0.15s ease; font-family: var(--tool-card-font-mono); font-size: 10px; font-weight: 600; @@ -120,23 +113,6 @@ .deletions { color: var(--color-error); } - - svg { - flex-shrink: 0; - opacity: 0.6; - } - - &:hover { - background: color-mix(in srgb, var(--color-text-primary) 10%, transparent); - - svg { - opacity: 1; - } - } - - &:active { - background: color-mix(in srgb, var(--color-text-primary) 16%, transparent); - } } .compact-actions { diff --git a/src/web-ui/src/flow_chat/tool-cards/FileOperationToolCard.tsx b/src/web-ui/src/flow_chat/tool-cards/FileOperationToolCard.tsx index d625ce64..c3a3c26c 100644 --- a/src/web-ui/src/flow_chat/tool-cards/FileOperationToolCard.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/FileOperationToolCard.tsx @@ -5,7 +5,7 @@ import React, { useEffect, useCallback, useMemo, useState, useRef } from 'react'; import { useTranslation } from 'react-i18next'; -import { XCircle, GitBranch, FileText, ChevronDown, ChevronUp, FileEdit, FilePlus, Trash2, Loader2, Clock, Check } from 'lucide-react'; +import { XCircle, GitBranch, FileText, ChevronDown, ChevronUp, FileEdit, FilePlus, FileX2, Loader2, Clock, Check } from 'lucide-react'; import { CubeLoading } from '../../component-library'; import type { ToolCardProps } from '../types/flow-chat'; import { BaseToolCard, ToolCardHeader } from './BaseToolCard'; @@ -44,12 +44,11 @@ export const FileOperationToolCard: React.FC = ({ const toolId = toolItem.id ?? toolCall?.id; const [isErrorExpanded, setIsErrorExpanded] = useState(false); - const [isPreviewExpanded, setIsPreviewExpanded] = useState(true); const [operationDiffStats, setOperationDiffStats] = useState<{ additions: number; deletions: number } | null>(null); const hasInitializedCompletionEffectRef = useRef(false); const previousCompletionEndTimeRef = useRef(toolItem.endTime ?? null); - const { cardRootRef, applyExpandedState } = useToolCardHeightContract({ + const { cardRootRef } = useToolCardHeightContract({ toolId, toolName: toolItem.toolName, }); @@ -73,12 +72,6 @@ export const FileOperationToolCard: React.FC = ({ const currentFilePath = getFilePath(); - useEffect(() => { - if (isParamsStreaming) { - setIsPreviewExpanded(true); - } - }, [isParamsStreaming]); - const getOldString = useCallback((): string => { const params = partialParams || toolCall?.input; if (!params) return ''; @@ -326,7 +319,7 @@ export const FileOperationToolCard: React.FC = ({ if (currentFilePath && onOpenInEditor) { onOpenInEditor(currentFilePath); } - }, [currentFilePath, onOpenInEditor, isFailed, isErrorExpanded, sessionId, status, handleOpenInCodeEditor, toolItem.toolName]); + }, [currentFilePath, onOpenInEditor, isFailed, sessionId, status, handleOpenInCodeEditor, toolItem.toolName]); const handleOpenBaselineDiff = useCallback(async () => { if (!currentFile || !currentWorkspace) { @@ -366,7 +359,7 @@ export const FileOperationToolCard: React.FC = ({ const iconMap: Record = { 'Write': { icon: , className: 'write-icon' }, 'Edit': { icon: , className: 'edit-icon' }, - 'Delete': { icon: , className: 'delete-icon' } + 'Delete': { icon: , className: 'delete-icon' } }; return iconMap[toolItem.toolName] || { icon: , className: 'file-icon' }; @@ -390,17 +383,6 @@ export const FileOperationToolCard: React.FC = ({ return null; }; - const handlePreviewToggle = useCallback((e: React.MouseEvent) => { - e.stopPropagation(); - const nextExpanded = !isPreviewExpanded; - - applyExpandedState(isPreviewExpanded, nextExpanded, setIsPreviewExpanded, { - detail: { - filePath: currentFilePath, - }, - }); - }, [applyExpandedState, currentFilePath, isPreviewExpanded]); - const renderHeader = () => { const { className: iconClassName } = getToolIconInfo(); const isDeleteTool = toolItem.toolName === 'Delete'; @@ -415,11 +397,25 @@ export const FileOperationToolCard: React.FC = ({ iconClassName={iconClassName} action={actionText} content={ - - - {fileName} - - + <> + + + {fileName} + + + {!isDeleteTool && !isParamsStreaming && !isFailed && !isLoading && ( + (currentFileDiffStats.additions > 0 || currentFileDiffStats.deletions > 0) + ) && ( + + {currentFileDiffStats.additions > 0 && ( + +{currentFileDiffStats.additions} + )} + {currentFileDiffStats.deletions > 0 && ( + -{currentFileDiffStats.deletions} + )} + + )} + } extra={ <> @@ -429,28 +425,6 @@ export const FileOperationToolCard: React.FC = ({ )} - {isDeleteTool && !isParamsStreaming && !isFailed && !isLoading && status === 'completed' && ( - {t('toolCards.file.deletedLabel')} - )} - - {!isDeleteTool && !isParamsStreaming && !isFailed && !isLoading && ( - (currentFileDiffStats.additions > 0 || currentFileDiffStats.deletions > 0 || oldStringContent || newStringContent || contentPreview) - ) && ( - - - - )} {!isDeleteTool && !isFailed && !isLoading && status === 'completed' && (
e.stopPropagation()}> @@ -606,17 +580,10 @@ export const FileOperationToolCard: React.FC = ({ }; const renderDeleteContent = () => { - const baseLabel = `${t('toolCards.file.delete')}: ${fileName}`; - - if (status === 'completed') { - return baseLabel; - } - if (status === 'error') { return `${t('toolCards.file.delete')}${t('toolCards.file.failed')}: ${fileName}`; } - - return baseLabel; + return <>{t('toolCards.file.delete')}: {fileName}; }; if (isDeleteTool) { @@ -630,7 +597,6 @@ export const FileOperationToolCard: React.FC = ({ } /> @@ -641,7 +607,7 @@ export const FileOperationToolCard: React.FC = ({
= ({ {title} } extra={ - <> - {status === 'completed' && ( -
- -
- )} - {isLoading && ( - - {(status === 'running' || status === 'streaming') && t('toolCards.diagram.creating')} - {status === 'pending' && t('toolCards.diagram.preparing')} - - )} - + isLoading ? ( + + {(status === 'running' || status === 'streaming') && t('toolCards.diagram.creating')} + {status === 'pending' && t('toolCards.diagram.preparing')} + + ) : null } statusIcon={renderStatusIcon()} /> diff --git a/src/web-ui/src/flow_chat/tool-cards/TaskToolDisplay.scss b/src/web-ui/src/flow_chat/tool-cards/TaskToolDisplay.scss index 2ea1bcc5..d266c2be 100644 --- a/src/web-ui/src/flow_chat/tool-cards/TaskToolDisplay.scss +++ b/src/web-ui/src/flow_chat/tool-cards/TaskToolDisplay.scss @@ -115,7 +115,7 @@ .task-header-extra { display: flex; align-items: center; - gap: 2px; + gap: 6px; margin-left: auto; flex-shrink: 0; } @@ -150,7 +150,7 @@ color: var(--color-text-secondary, #9ca3af); line-height: 1.4; border-radius: 4px; - cursor: default; + cursor: pointer; transition: background 0.15s ease; .task-prompt-content { @@ -158,6 +158,8 @@ min-width: 0; word-break: break-word; white-space: pre-wrap; + max-height: calc(1.4em * 3); + overflow-y: auto; } &.task-prompt-row--clickable { @@ -340,11 +342,54 @@ } -/* Square bottom corners when followed by subagent items. */ -/* FlowToolCard wraps in .flow-tool-card-wrapper, so select from parent. */ -.flow-tool-card-wrapper:has(+ .subagent-items-wrapper) { - .base-tool-card-wrapper.task-tool-display { - border-radius: 8px 8px 0 0 !important; +/** + * Make the entire header area show pointer cursor. + * BaseToolCard already sets cursor:pointer on .base-tool-card, + * but the header content area clips it — force it here. + */ +.task-tool-display { + .base-tool-card { + cursor: pointer; + } +} + +/** + * Unified task + subagent card wrapper. + * Wraps the task tool header card and the subagent items container so they + * behave as a single card with a shared hover/shadow effect. + */ +.task-with-subagent-wrapper { + position: relative; + border-radius: 8px; + margin-bottom: 10px; + transition: box-shadow 0.25s cubic-bezier(0.4, 0, 0.2, 1); + + // Expanded state: subtle default elevation. + &:has(.subagent-items-wrapper--expanded) { + box-shadow: + 0 2px 6px rgba(0, 0, 0, 0.15), + 0 1px 3px rgba(0, 0, 0, 0.1); + } + + // Hover over expanded card: stronger elevation. + &:has(.subagent-items-wrapper--expanded):hover { + box-shadow: + 0 4px 16px rgba(0, 0, 0, 0.25), + 0 2px 8px rgba(0, 0, 0, 0.15), + inset 0 1px 0 rgba(255, 255, 255, 0.04); + } + + // Square the bottom corners of the task header card when expanded. + &:has(.subagent-items-wrapper--expanded) { + .flow-tool-card-wrapper .base-tool-card-wrapper.task-tool-display { + border-radius: 8px 8px 0 0; + // Remove bottom margin so the subagent container connects flush. + margin-bottom: 0; + // Suppress the base card's own hover shadow; the wrapper handles it. + &:hover { + box-shadow: none; + } + } } } diff --git a/src/web-ui/src/flow_chat/tool-cards/TaskToolDisplay.tsx b/src/web-ui/src/flow_chat/tool-cards/TaskToolDisplay.tsx index cb7fa102..0808abe3 100644 --- a/src/web-ui/src/flow_chat/tool-cards/TaskToolDisplay.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/TaskToolDisplay.tsx @@ -4,12 +4,11 @@ import React, { useState, useEffect, useCallback, useRef } from 'react'; import { - ChevronDown, - ChevronUp, Split, Timer, PanelRightOpen } from 'lucide-react'; + import { useTranslation } from 'react-i18next'; import { CubeLoading, Button, IconButton } from '../../component-library'; import type { ToolCardProps } from '../types/flow-chat'; @@ -40,9 +39,6 @@ export const TaskToolDisplay: React.FC = ({ const isRunning = status === 'preparing' || status === 'streaming' || status === 'running'; - const [isPromptExpanded, setIsPromptExpanded] = useState(false); - const promptRef = useRef(null); - const [isPromptOverflow, setIsPromptOverflow] = useState(false); const { cardRootRef, applyExpandedState } = useToolCardHeightContract({ toolId, toolName: toolItem.toolName, @@ -57,10 +53,6 @@ export const TaskToolDisplay: React.FC = ({ applyExpandedState(isExpanded, nextExpanded, setIsExpanded, { reason }); }, [applyExpandedState, isExpanded, isRunning, status, toolId]); - const updatePromptExpandedState = useCallback((nextExpanded: boolean) => { - applyExpandedState(isPromptExpanded, nextExpanded, setIsPromptExpanded); - }, [applyExpandedState, isPromptExpanded]); - useEffect(() => { const prevStatus = prevStatusRef.current; @@ -75,17 +67,6 @@ export const TaskToolDisplay: React.FC = ({ } }, [isRunning, status, updateCardExpandedState]); - useEffect(() => { - const prompt = toolCall?.input?.prompt; - if (prompt) { - let visualWidth = 0; - for (const char of prompt) { - visualWidth += isFullWidth(char) ? 2 : 1; - } - setIsPromptOverflow(visualWidth > 100 || prompt.includes('\n')); - } - }, [toolCall?.input?.prompt]); - useEffect(() => { taskCollapseStateManager.setCollapsed(toolItem.id, !isExpanded); }, [isExpanded, toolItem.id]); @@ -180,11 +161,10 @@ export const TaskToolDisplay: React.FC = ({ const renderHeader = () => { const hasPromptContent = taskInput && taskInput.prompt && taskInput.prompt !== 'Not provided'; - const isPromptVisible = hasPromptContent && (!isPromptOverflow || isPromptExpanded); return (
-
+
{renderToolIcon()}
@@ -207,24 +187,10 @@ export const TaskToolDisplay: React.FC = ({ {t('toolCards.taskTool.failed')} )} - { - e.stopPropagation(); - updateCardExpandedState(!isExpanded); - }} - tooltip={isExpanded ? t('toolCards.common.collapse') : t('toolCards.common.expand')} - tooltipPlacement="top" - > - {isExpanded ? : } - - { e.stopPropagation(); const panelData = { toolItem, taskInput, sessionId }; @@ -243,7 +209,7 @@ export const TaskToolDisplay: React.FC = ({ tooltip={t('toolCards.taskTool.openInPanel')} tooltipPlacement="top" > - +
@@ -256,13 +222,6 @@ export const TaskToolDisplay: React.FC = ({
)}; - const handlePromptRowClick = useCallback((e: React.MouseEvent) => { - e.stopPropagation(); - if (isPromptOverflow) { - updatePromptExpandedState(!isPromptExpanded); - } - }, [isPromptExpanded, isPromptOverflow, updatePromptExpandedState]); - const renderPromptRow = () => { const hasPrompt = taskInput && taskInput.prompt && taskInput.prompt !== 'Not provided'; @@ -270,30 +229,11 @@ export const TaskToolDisplay: React.FC = ({ return null; } - const isPromptCollapsed = !isPromptExpanded && isPromptOverflow; - return ( -
-
+
+
{taskInput!.prompt}
- {isPromptExpanded && isPromptOverflow && ( - - - - )}
); }; @@ -333,22 +273,13 @@ export const TaskToolDisplay: React.FC = ({ ); }; - // Error details are shown in the side panel only. - const hasPrompt = taskInput && taskInput.prompt && taskInput.prompt !== 'Not provided'; - const isPromptRowExpanded = hasPrompt && isPromptExpanded; - - const cardClassName = [ - 'task-tool-display', - isPromptRowExpanded ? 'prompt-expanded' : '' - ].filter(Boolean).join(' '); - return (
= ({ applyExpandedState(newExpanded, true, 'manual'); }, [applyExpandedState, isExpanded]); - const handleToggleExpand = useCallback((e: React.MouseEvent) => { - e.stopPropagation(); - toggleExpand(); - }, [toggleExpand]); - const handleOpenInPanel = useCallback((e: React.MouseEvent) => { e.stopPropagation(); if (!terminalSessionId) { @@ -356,7 +351,7 @@ export const TerminalToolCard: React.FC = ({ switch (status) { case 'completed': - return {t('toolCards.terminal.completed')}; + return null; case 'cancelled': return {t('toolCards.terminal.cancelled')}; case 'error': @@ -432,16 +427,6 @@ export const TerminalToolCard: React.FC = ({ )} - - - {isExpanded ? : } - } statusIcon={renderStatusIcon()} @@ -470,7 +455,16 @@ export const TerminalToolCard: React.FC = ({ {status === 'completed' && (
-
+ {output && ( +
+ +
+ )} +
{workingDir && ( <> {t('toolCards.terminal.workingDirectory')} @@ -486,24 +480,11 @@ export const TerminalToolCard: React.FC = ({ )}
- - {output && ( -
- -
- )}
)} {status === 'cancelled' && accumulatedOutput && (
-
- {t('toolCards.terminal.commandInterrupted')} -
= ({ maxHeight={TERMINAL_OUTPUT_PREVIEW_MAX_HEIGHT} />
+
+ {t('toolCards.terminal.commandInterrupted')} +
)} diff --git a/src/web-ui/src/flow_chat/tool-cards/TodoWriteDisplay.scss b/src/web-ui/src/flow_chat/tool-cards/TodoWriteDisplay.scss index a97e1f68..409339dd 100644 --- a/src/web-ui/src/flow_chat/tool-cards/TodoWriteDisplay.scss +++ b/src/web-ui/src/flow_chat/tool-cards/TodoWriteDisplay.scss @@ -187,19 +187,12 @@ width: 6px; height: 6px; border-radius: 50%; - cursor: pointer; - transition: transform 0.15s ease, opacity 0.15s ease; + flex-shrink: 0; &--completed { background: var(--color-success); } &--in_progress { background: var(--color-info); } &--pending { background: var(--tool-card-text-muted); opacity: 0.35; } &--cancelled { background: var(--color-error); opacity: 0.5; } - - &:hover, - &--hovered { - transform: scale(1.6); - opacity: 1; - } } .track-stats { diff --git a/src/web-ui/src/flow_chat/tool-cards/TodoWriteDisplay.tsx b/src/web-ui/src/flow_chat/tool-cards/TodoWriteDisplay.tsx index d8d15671..61ce5119 100644 --- a/src/web-ui/src/flow_chat/tool-cards/TodoWriteDisplay.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/TodoWriteDisplay.tsx @@ -16,7 +16,6 @@ export const TodoWriteDisplay: React.FC = ({ const { t } = useTranslation('flow-chat'); const { status, toolResult, partialParams, isParamsStreaming } = toolItem; - const [hoveredIndex, setHoveredIndex] = useState(null); const [expandedState, setExpandedState] = useState(null); const toolId = toolItem.id; const { cardRootRef, applyExpandedState } = useToolCardHeightContract({ @@ -46,13 +45,6 @@ export const TodoWriteDisplay: React.FC = ({ return todosToDisplay.filter((t: any) => t.status === 'in_progress'); }, [todosToDisplay]); - const hoveredTask = useMemo(() => { - if (hoveredIndex !== null && todosToDisplay[hoveredIndex]) { - return todosToDisplay[hoveredIndex]; - } - return null; - }, [hoveredIndex, todosToDisplay]); - const isAllCompleted = useMemo(() => { return todosToDisplay.length > 0 && taskStats.completed === taskStats.total; }, [todosToDisplay.length, taskStats]); @@ -88,13 +80,10 @@ export const TodoWriteDisplay: React.FC = ({ const renderTrackDot = (todo: any, index: number) => { const statusClass = `track-dot--${todo.status}`; - const isHovered = hoveredIndex === index; return ( -
setHoveredIndex(index)} - onMouseLeave={() => setHoveredIndex(null)} +
); }; @@ -120,14 +109,11 @@ export const TodoWriteDisplay: React.FC = ({ ); const currentDisplayTask = useMemo(() => { - if (hoveredTask) { - return hoveredTask; - } if (inProgressTasks.length > 0) { return inProgressTasks[0]; } return null; - }, [hoveredTask, inProgressTasks]); + }, [inProgressTasks]); const handleToggleExpanded = useCallback(() => { if (todosToDisplay.length === 0) { @@ -166,7 +152,7 @@ export const TodoWriteDisplay: React.FC = ({ {!isExpanded && todosToDisplay.length > 0 && currentDisplayTask && (
{currentDisplayTask.content} - {inProgressTasks.length > 1 && !hoveredTask && ( + {inProgressTasks.length > 1 && ( +{inProgressTasks.length - 1} )}
diff --git a/src/web-ui/src/tools/editor/services/ThemeManager.ts b/src/web-ui/src/tools/editor/services/ThemeManager.ts index 88a8dadd..0f12a836 100644 --- a/src/web-ui/src/tools/editor/services/ThemeManager.ts +++ b/src/web-ui/src/tools/editor/services/ThemeManager.ts @@ -143,7 +143,8 @@ class ThemeManager { : (currentTheme.type === 'dark' ? this.getDefaultThemeId() : 'vs'); this.currentThemeId = themeId; - monaco.editor.setTheme(themeId); + const { monacoThemeSync } = await import('@/infrastructure/theme/integrations/MonacoThemeSync'); + monacoThemeSync.syncTheme(currentTheme); } themeService.on('theme:after-change', (event) => {