From 34a59162e1dca31605d9f0b948b68b8bcd6840a9 Mon Sep 17 00:00:00 2001 From: GCWing Date: Sun, 26 Apr 2026 16:08:43 +0800 Subject: [PATCH] feat(web-ui): refine file operation tool card expand and open-in-editor - Separate open full code from card expand; add canOpenFullCode and handleOpenFullCodeClick - Update diff pill / rail click handling and tool card styles - Add i18n strings for flow-chat tool card actions --- .../flow_chat/tool-cards/BaseToolCard.scss | 20 ++- .../tool-cards/FileOperationToolCard.scss | 99 +++++++------ .../tool-cards/FileOperationToolCard.tsx | 140 ++++++++---------- .../flow_chat/tool-cards/TaskToolDisplay.scss | 11 +- src/web-ui/src/locales/en-US/flow-chat.json | 1 + src/web-ui/src/locales/zh-CN/flow-chat.json | 1 + src/web-ui/src/locales/zh-TW/flow-chat.json | 1 + 7 files changed, 142 insertions(+), 131 deletions(-) diff --git a/src/web-ui/src/flow_chat/tool-cards/BaseToolCard.scss b/src/web-ui/src/flow_chat/tool-cards/BaseToolCard.scss index badf42a4..d1b74a6e 100644 --- a/src/web-ui/src/flow_chat/tool-cards/BaseToolCard.scss +++ b/src/web-ui/src/flow_chat/tool-cards/BaseToolCard.scss @@ -8,6 +8,7 @@ --tool-card-header-pad-y: 7px; --tool-card-header-pad-x: 0px; --tool-card-header-icon-rail: 24px; + --tool-card-header-icon-slot: calc(var(--tool-card-header-icon-rail) + 10px); } /* ========== Card wrapper ========== */ @@ -139,10 +140,10 @@ position: relative; display: flex; align-items: center; - justify-content: flex-start; + justify-content: center; flex-shrink: 0; align-self: stretch; - padding-right: 10px; + width: var(--tool-card-header-icon-slot); margin-right: 8px; box-sizing: border-box; min-height: 0; @@ -299,7 +300,7 @@ display: flex; align-items: center; gap: 4px; - flex: 1; + flex: 1 1 auto; min-width: 0; font-size: 12px; font-weight: 500; @@ -310,6 +311,14 @@ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; + + > * { + min-width: 0; + } + + svg { + flex-shrink: 0; + } } /* Right extra content */ @@ -318,6 +327,11 @@ align-items: center; gap: 2px; flex-shrink: 0; + min-width: 0; + + svg { + flex-shrink: 0; + } } /* ========== Expanded content area ========== */ 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 c3c532df..4528cc23 100644 --- a/src/web-ui/src/flow_chat/tool-cards/FileOperationToolCard.scss +++ b/src/web-ui/src/flow_chat/tool-cards/FileOperationToolCard.scss @@ -70,11 +70,7 @@ } } - /* - * Git 轨道在 ToolCardHeader 里位于 .tool-card-extra 内;默认 extra 高度跟子项走,轨道只有图标高, - * 竖线与悬停底无法贴齐头部上下内边距、也无法铺到右侧 padding。与头部行同高后再用负 inset 顶满。 - */ - .base-tool-card-header .tool-card-extra:has(.file-op-git-rail) { + .base-tool-card-header .tool-card-extra:has(.file-op-open-full-button) { align-self: stretch; display: flex; flex-direction: row; @@ -85,6 +81,11 @@ } .file-name { + display: block; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; font-weight: 500; color: var(--color-text-primary); @@ -130,16 +131,42 @@ color: var(--color-text-muted); } -.diff-preview-group { - display: flex; +.file-op-diff-pill { + display: inline-flex; align-items: center; - gap: 3px; + gap: 4px; flex-shrink: 0; + margin-left: 2px; + padding: 2px 5px; + border: none; + border-radius: 999px; + background: transparent; font-family: var(--tool-card-font-mono); font-size: 10px; font-weight: 600; color: var(--color-text-muted); line-height: 1; + cursor: pointer; + transition: + background-color 0.15s ease, + color 0.15s ease; + + &:hover:not(.file-op-diff-pill--disabled) { + background-color: var(--tool-card-bg-hover, var(--color-bg-hover, rgba(255, 255, 255, 0.09))); + color: var(--color-text-primary); + } + + &--disabled { + cursor: default; + opacity: 0.45; + } + + svg { + width: 12px; + height: 12px; + flex-shrink: 0; + stroke: currentColor; + } .additions { color: var(--color-success); @@ -150,18 +177,24 @@ } } -/* Right rail for “查看 Git diff” — aligned with Task tool header rail (TaskToolDisplay). */ -.file-op-git-rail { +.file-op-open-full-button { position: relative; display: flex; - flex-direction: column; align-items: center; justify-content: center; align-self: stretch; flex-shrink: 0; - min-width: 32px; - padding: 0 0 0 8px; + width: 40px; + padding: 0; margin-left: 4px; + border: none; + border-radius: 0; + background: transparent; + color: var(--color-text-muted); + cursor: pointer; + appearance: none; + font: inherit; + transition: color 0.15s ease; &::before { content: ''; @@ -177,11 +210,11 @@ z-index: 0; } - &:hover:not(.file-op-git-rail--disabled)::before { + &:hover::before { background-color: var(--tool-card-bg-hover, var(--color-bg-hover, rgba(255, 255, 255, 0.09))); } - &:hover:not(.file-op-git-rail--disabled) .file-op-git-rail__visual { + &:hover { color: var(--color-text-primary); } @@ -197,46 +230,14 @@ z-index: 1; } - &--disabled { - opacity: 0.45; - - .file-op-git-rail__visual { - color: var(--color-text-muted); - } - } -} - -.file-op-git-rail__hit { - position: absolute; - left: 0; - top: calc(-1 * var(--tool-card-header-pad-y, 10px)); - right: calc(-1 * var(--tool-card-header-pad-x, 10px)); - bottom: calc(-1 * var(--tool-card-header-pad-y, 10px)); - z-index: 2; - margin: 0; - padding: 0; - border: none; - background: transparent; - cursor: pointer; - appearance: none; - font: inherit; - &:focus-visible { outline: 2px solid var(--color-accent-500, #60a5fa); outline-offset: -2px; } -} - -.file-op-git-rail__visual { - position: relative; - z-index: 1; - display: flex; - align-items: center; - justify-content: center; - color: var(--color-text-muted); - pointer-events: none; svg { + position: relative; + z-index: 1; width: 16px; height: 16px; flex-shrink: 0; 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 8e4e15b8..17ed90f6 100644 --- a/src/web-ui/src/flow_chat/tool-cards/FileOperationToolCard.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/FileOperationToolCard.tsx @@ -19,11 +19,10 @@ import { XCircle, GitBranch, FileText, - ChevronDown, - ChevronUp, FileEdit, FilePlus, FileX2, + ChevronRight, Loader2, Clock, Check, @@ -425,13 +424,20 @@ export const FileOperationToolCard: React.FC = ({ } }, [sessionId, currentFilePath, toolCall?.id, fileName, toolItem.toolName]); + const canOpenFullCode = + !isFailed && + toolItem.toolName !== 'Delete' && + status === 'completed' && + Boolean(currentFilePath) && + Boolean(sessionId || onOpenInEditor); + const handleCardClick = useCallback((e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); if ( (e.target as HTMLElement).closest( - '.file-op-git-rail:not(.file-op-git-rail--disabled)', + '.file-op-diff-pill, .file-op-open-full-button', ) ) { return; @@ -445,32 +451,37 @@ export const FileOperationToolCard: React.FC = ({ if (toolItem.toolName === 'Delete') { return; } - - if (status !== 'completed') { - applyContentExpandedState(!isContentExpanded, 'manual'); + + applyContentExpandedState(!isContentExpanded, 'manual'); + }, [ + applyContentExpandedState, + applyErrorExpandedState, + isContentExpanded, + isErrorExpanded, + isFailed, + toolItem.toolName, + ]); + + const handleOpenFullCodeClick = useCallback((e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + + if (!canOpenFullCode || !currentFilePath) { return; } - if (currentFilePath && sessionId) { + if (sessionId) { handleOpenInCodeEditor(); return; } - if (currentFilePath && onOpenInEditor) { - onOpenInEditor(currentFilePath); - } + onOpenInEditor?.(currentFilePath); }, [ - applyContentExpandedState, - applyErrorExpandedState, + canOpenFullCode, currentFilePath, handleOpenInCodeEditor, - isContentExpanded, - isErrorExpanded, - isFailed, onOpenInEditor, sessionId, - status, - toolItem.toolName, ]); const handleOpenBaselineDiff = useCallback(async () => { @@ -711,16 +722,12 @@ export const FileOperationToolCard: React.FC = ({ !isFailed && isContentExpanded; - const opensPanelOnClick = - !isFailed && - !isDeleteTool && - (Boolean(currentFilePath && sessionId && status === 'completed') || - Boolean(currentFilePath && onOpenInEditor)); - const renderHeader = () => { const { className: iconClassName } = getToolIconInfo(); - const gitRailDisabled = + const gitDiffDisabled = !currentFilePath || !currentWorkspace || !sessionId; + const hasDiffStats = + currentFileDiffStats.additions > 0 || currentFileDiffStats.deletions > 0; const actionText = isDeleteTool ? '' @@ -730,13 +737,7 @@ export const FileOperationToolCard: React.FC = ({ applyContentExpandedState(!isContentExpanded, 'manual') @@ -755,17 +756,30 @@ export const FileOperationToolCard: React.FC = ({ {fileName} - {!isDeleteTool && !isParamsStreaming && !isLoading && ( - (currentFileDiffStats.additions > 0 || currentFileDiffStats.deletions > 0) - ) && ( - + {!isDeleteTool && !isParamsStreaming && !isLoading && hasDiffStats && ( + + + )} ) @@ -777,38 +791,18 @@ export const FileOperationToolCard: React.FC = ({ {currentFilePath ? t('toolCards.file.receivingParams') : t('toolCards.file.analyzing')} )} - {!isDeleteTool && - !isFailed && - !isLoading && - status === 'completed' && - currentFilePath && ( - -
- {!gitRailDisabled && ( -
-
- )} - - {isFailed && ( -
- {isErrorExpanded ? : } -
+ {canOpenFullCode && ( + + + )} } @@ -845,14 +839,8 @@ export const FileOperationToolCard: React.FC = ({ expandedContent={expandedContent} errorContent={isFailed && isErrorExpanded ? renderErrorContent() : null} isFailed={isFailed} - headerExpandAffordance={hasExpandableContent || opensPanelOnClick || isFailed} - headerAffordanceKind={ - hasExpandableContent - ? 'expand' - : opensPanelOnClick - ? 'open-panel-right' - : 'expand' - } + headerExpandAffordance={hasExpandableContent} + headerAffordanceKind="expand" /> ); 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 90b63ef2..99e45311 100644 --- a/src/web-ui/src/flow_chat/tool-cards/TaskToolDisplay.scss +++ b/src/web-ui/src/flow_chat/tool-cards/TaskToolDisplay.scss @@ -15,7 +15,7 @@ justify-content: center; flex-shrink: 0; align-self: stretch; - padding-right: 10px; + width: var(--tool-card-header-icon-slot, 34px); margin-right: 8px; box-sizing: border-box; min-height: 0; @@ -137,8 +137,8 @@ justify-content: center; align-self: stretch; flex-shrink: 0; - min-width: 40px; - padding: 0 0 0 8px; + width: 40px; + padding: 0; margin-left: 4px; /* Hover fill: full header height + extend into header right padding to card inner edge. */ @@ -279,6 +279,11 @@ margin-left: auto; margin-right: 10px; flex-shrink: 0; + min-width: 0; + + svg { + flex-shrink: 0; + } } .task-status-icon { 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 7425c5bd..97a6beae 100644 --- a/src/web-ui/src/locales/en-US/flow-chat.json +++ b/src/web-ui/src/locales/en-US/flow-chat.json @@ -564,6 +564,7 @@ "analyzing": "Analyzing...", "expandPreview": "Expand preview", "collapsePreview": "Collapse preview", + "openFullCodeHint": "Open full code", "viewGitDiff": "View Diff", "viewBaselineDiff": "View Baseline Diff" }, 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 817a8415..63d2e0b3 100644 --- a/src/web-ui/src/locales/zh-CN/flow-chat.json +++ b/src/web-ui/src/locales/zh-CN/flow-chat.json @@ -564,6 +564,7 @@ "analyzing": "分析中...", "expandPreview": "展开预览", "collapsePreview": "收起预览", + "openFullCodeHint": "打开全量代码", "viewGitDiff": "查看 Diff", "viewBaselineDiff": "查看基线 Diff" }, diff --git a/src/web-ui/src/locales/zh-TW/flow-chat.json b/src/web-ui/src/locales/zh-TW/flow-chat.json index aa50f7cb..3bb870c6 100644 --- a/src/web-ui/src/locales/zh-TW/flow-chat.json +++ b/src/web-ui/src/locales/zh-TW/flow-chat.json @@ -540,6 +540,7 @@ "analyzing": "分析中...", "expandPreview": "展開預覽", "collapsePreview": "收起預覽", + "openFullCodeHint": "打開全量代碼", "viewGitDiff": "查看 Diff", "viewBaselineDiff": "查看基線 Diff" },