From 2e13338c1f20110c76b5bc7117360ea3dc55e6ca Mon Sep 17 00:00:00 2001 From: limityan Date: Sat, 25 Apr 2026 22:11:02 +0800 Subject: [PATCH 1/4] feat(flow-chat): compact terminal/git tool cards and group consecutive bash commands - TerminalToolCard: switch from BaseToolCard to CompactToolCard for tighter layout - GitToolDisplay: switch from BaseToolCard to CompactToolCard for consistent styling - TerminalGroupRenderer: new component to merge consecutive Bash commands into a collapsible group with status summary - ModelRoundItem: extend grouping logic to support terminal-type groups - FlowChatContext + ModernFlowChatContainer: add terminal group collapse state management - useTerminalGroupState: new hook for terminal group expansion state - CompactToolCard.scss: reduce tool wrapper margin-bottom from 0.6rem to 0.35rem - TerminalGroupRenderer.scss: tighter spacing for cards inside terminal groups - i18n: add terminalRegion translations (en-US + zh-CN) --- .../components/modern/FlowChatContext.tsx | 22 ++++++++++ .../components/modern/ModelRoundItem.tsx | 43 +++++++++++++++++-- .../modern/ModernFlowChatContainer.tsx | 15 +++++++ .../flow_chat/tool-cards/CompactToolCard.scss | 2 +- .../flow_chat/tool-cards/GitToolDisplay.scss | 43 +++++++++++-------- .../flow_chat/tool-cards/GitToolDisplay.tsx | 30 ++++++------- .../tool-cards/TerminalToolCard.scss | 37 ++++++++-------- .../flow_chat/tool-cards/TerminalToolCard.tsx | 18 +++----- src/web-ui/src/flow_chat/tool-cards/index.ts | 10 +++++ src/web-ui/src/locales/en-US/flow-chat.json | 12 ++++++ src/web-ui/src/locales/zh-CN/flow-chat.json | 12 ++++++ 11 files changed, 175 insertions(+), 69 deletions(-) diff --git a/src/web-ui/src/flow_chat/components/modern/FlowChatContext.tsx b/src/web-ui/src/flow_chat/components/modern/FlowChatContext.tsx index f504fe862..e48bba111 100644 --- a/src/web-ui/src/flow_chat/components/modern/FlowChatContext.tsx +++ b/src/web-ui/src/flow_chat/components/modern/FlowChatContext.tsx @@ -52,6 +52,28 @@ export interface FlowChatContextValue { */ onCollapseGroup?: (groupId: string) => void; + // ========== Terminal group collapse state ========== + /** + * Expanded/collapsed state for terminal groups. + * key: groupId, value: true means expanded. + */ + terminalGroupStates?: Map; + + /** + * Toggle terminal group expanded/collapsed state. + */ + onTerminalGroupToggle?: (groupId: string) => void; + + /** + * Expand the specified terminal group. + */ + onExpandTerminalGroup?: (groupId: string) => void; + + /** + * Collapse the specified terminal group. + */ + onCollapseTerminalGroup?: (groupId: string) => void; + // Message search state searchQuery?: string; searchMatchIndices?: ReadonlySet; diff --git a/src/web-ui/src/flow_chat/components/modern/ModelRoundItem.tsx b/src/web-ui/src/flow_chat/components/modern/ModelRoundItem.tsx index 4720cebfe..4b835f67a 100644 --- a/src/web-ui/src/flow_chat/components/modern/ModelRoundItem.tsx +++ b/src/web-ui/src/flow_chat/components/modern/ModelRoundItem.tsx @@ -14,11 +14,12 @@ import type { ModelRound, FlowItem, FlowTextItem, FlowToolItem, FlowThinkingItem import { FlowTextBlock } from '../FlowTextBlock'; import { FlowToolCard } from '../FlowToolCard'; import { ModelThinkingDisplay } from '../../tool-cards/ModelThinkingDisplay'; -import { isCollapsibleTool } from '../../tool-cards'; +import { isCollapsibleTool, isTerminalCollapsibleTool } from '../../tool-cards'; import { useFlowChatContext } from './FlowChatContext'; import { FlowChatStore } from '../../store/FlowChatStore'; import { taskCollapseStateManager } from '../../store/TaskCollapseStateManager'; import { ExportImageButton } from './ExportImageButton'; +import { TerminalGroupRenderer } from './TerminalGroupRenderer'; import { Tooltip } from '@/component-library'; import { createLogger } from '@/shared/utils/logger'; import './ModelRoundItem.scss'; @@ -72,6 +73,7 @@ export const ModelRoundItem = React.memo( type ItemGroup = | { type: 'explore'; items: FlowItem[]; isLast: boolean } + | { type: 'terminal'; items: FlowItem[]; isLast: boolean } | { type: 'critical'; item: FlowItem } | { type: 'subagent'; parentTaskToolId: string; items: FlowItem[] }; @@ -116,6 +118,7 @@ export const ModelRoundItem = React.memo( const finalGroups: ItemGroup[] = []; let exploreBuffer: FlowItem[] = []; + let terminalBuffer: FlowItem[] = []; let pendingBuffer: FlowItem[] = []; const normalItems: FlowItem[] = []; @@ -132,6 +135,13 @@ export const ModelRoundItem = React.memo( } }; + const flushTerminalBuffer = (isLast: boolean) => { + if (terminalBuffer.length > 0) { + finalGroups.push({ type: 'terminal', items: [...terminalBuffer], isLast }); + terminalBuffer = []; + } + }; + const flushPendingAsCritical = () => { for (const item of pendingBuffer) { finalGroups.push({ type: 'critical', item }); @@ -147,6 +157,7 @@ export const ModelRoundItem = React.memo( if (group.type === 'subagent') { flushExploreBuffer(false); + flushTerminalBuffer(false); flushPendingAsCritical(); finalGroups.push(group); } else { @@ -158,15 +169,18 @@ export const ModelRoundItem = React.memo( if (isLastNormalItem) { flushExploreBuffer(false); + flushTerminalBuffer(false); flushPendingAsCritical(); } } else if (item.type === 'tool') { const toolName = (item as FlowToolItem).toolName; const isExploreTool = isCollapsibleTool(toolName); + const isTerminalTool = isTerminalCollapsibleTool(toolName); if (isExploreTool) { if (deferExploreGrouping) { flushExploreBuffer(false); + flushTerminalBuffer(false); flushPendingAsCritical(); finalGroups.push({ type: 'critical', item }); normalItemIndex++; @@ -178,8 +192,16 @@ export const ModelRoundItem = React.memo( if (isLastNormalItem || isLastGroup) { flushExploreBuffer(true); } + } else if (isTerminalTool) { + terminalBuffer.push(...pendingBuffer, item); + pendingBuffer = []; + + if (isLastNormalItem || isLastGroup) { + flushTerminalBuffer(true); + } } else { flushExploreBuffer(false); + flushTerminalBuffer(false); flushPendingAsCritical(); finalGroups.push({ type: 'critical', item }); } @@ -190,6 +212,7 @@ export const ModelRoundItem = React.memo( } flushExploreBuffer(true); + flushTerminalBuffer(true); flushPendingAsCritical(); return finalGroups; @@ -292,7 +315,7 @@ export const ModelRoundItem = React.memo( switch (group.type) { case 'explore': return group.items.map((item, itemIdx) => ( - ( isLastItem={isLast && itemIdx === group.items.length - 1} /> )); - + + case 'terminal': { + const terminalItems = group.items.filter((it) => it.type === 'tool') as FlowToolItem[]; + return ( + + ); + } + case 'critical': { // If next group is the matching subagent, skip here — rendered by subagent case. const nextGroup = groupedItems[groupIndex + 1]; diff --git a/src/web-ui/src/flow_chat/components/modern/ModernFlowChatContainer.tsx b/src/web-ui/src/flow_chat/components/modern/ModernFlowChatContainer.tsx index df43ef360..8521809c0 100644 --- a/src/web-ui/src/flow_chat/components/modern/ModernFlowChatContainer.tsx +++ b/src/web-ui/src/flow_chat/components/modern/ModernFlowChatContainer.tsx @@ -12,6 +12,7 @@ import { FlowChatHeader, type FlowChatHeaderTurnSummary } from './FlowChatHeader import { WelcomePanel } from '../WelcomePanel'; import { FlowChatContext, FlowChatContextValue } from './FlowChatContext'; import { useExploreGroupState } from './useExploreGroupState'; +import { useTerminalGroupState } from './useTerminalGroupState'; import { useFlowChatFileActions } from './useFlowChatFileActions'; import { useFlowChatNavigation } from './useFlowChatNavigation'; import { useFlowChatCopyDialog } from './useFlowChatCopyDialog'; @@ -61,6 +62,12 @@ export const ModernFlowChatContainer: React.FC = ( onExpandAllInTurn: handleExpandAllInTurn, onCollapseGroup: handleCollapseGroup, } = useExploreGroupState(virtualItems); + const { + terminalGroupStates, + onTerminalGroupToggle: handleTerminalGroupToggle, + onExpandTerminalGroup: handleExpandTerminalGroup, + onCollapseTerminalGroup: handleCollapseTerminalGroup, + } = useTerminalGroupState(); const { handleToolConfirm, handleToolReject } = useFlowChatToolActions(); const { handleFileViewRequest } = useFlowChatFileActions({ workspacePath, @@ -110,6 +117,10 @@ export const ModernFlowChatContainer: React.FC = ( onExpandGroup: handleExpandGroup, onExpandAllInTurn: handleExpandAllInTurn, onCollapseGroup: handleCollapseGroup, + terminalGroupStates, + onTerminalGroupToggle: handleTerminalGroupToggle, + onExpandTerminalGroup: handleExpandTerminalGroup, + onCollapseTerminalGroup: handleCollapseTerminalGroup, searchQuery, searchMatchIndices, searchCurrentMatchVirtualIndex, @@ -127,6 +138,10 @@ export const ModernFlowChatContainer: React.FC = ( handleExpandGroup, handleExpandAllInTurn, handleCollapseGroup, + terminalGroupStates, + handleTerminalGroupToggle, + handleExpandTerminalGroup, + handleCollapseTerminalGroup, searchQuery, searchMatchIndices, searchCurrentMatchVirtualIndex, 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..4d32b6381 100644 --- a/src/web-ui/src/flow_chat/tool-cards/CompactToolCard.scss +++ b/src/web-ui/src/flow_chat/tool-cards/CompactToolCard.scss @@ -22,7 +22,7 @@ * inherit a different line-height from ancestors. */ .flowchat-flow-item, .flowchat-tool-wrapper { - margin: 0 0 0.6rem 0; + margin: 0 0 0.35rem 0; font-size: var(--flowchat-font-size-base); line-height: 1.65; } diff --git a/src/web-ui/src/flow_chat/tool-cards/GitToolDisplay.scss b/src/web-ui/src/flow_chat/tool-cards/GitToolDisplay.scss index 258b89502..7db412fa3 100644 --- a/src/web-ui/src/flow_chat/tool-cards/GitToolDisplay.scss +++ b/src/web-ui/src/flow_chat/tool-cards/GitToolDisplay.scss @@ -1,33 +1,38 @@ /** - * Git tool display styles based on BaseToolCard. + * Git tool display styles based on CompactToolCard. * Expanded layout aligns with TerminalToolCard (bash): scrollable output + full-width footer bar. */ -@use './_tool-card-common.scss'; +@use './CompactToolCard.scss'; +@use '../../component-library/styles/_extended-mixins' as mixins; -.base-tool-card-wrapper.git-tool-display .base-tool-card-header { +/* ========== Git card specific styles ========== */ + +.git-tool-display { + .git-card-icon { + color: var(--color-text-secondary); + flex-shrink: 0; + } +} + +/* Entire compact card shows pointer (card toggles expand); children must not force default cursor. */ +.compact-tool-card.git-tool-display { cursor: pointer; } -.base-tool-card-wrapper.git-tool-display .git-result-footer { - margin-left: -10px; - margin-right: -10px; - margin-bottom: -10px; - padding: 6px 10px; - border-radius: 0 0 7px 7px; +/* Footer bar: full width + background to bottom inner edge of expanded content (matches .compact-tool-card-expanded padding). */ +.compact-tool-card.git-tool-display .git-result-footer { + margin-left: -8px; + margin-right: -8px; + margin-bottom: -8px; + padding: 6px 8px; + border-radius: 0 0 6px 6px; } -.base-tool-card-wrapper.git-tool-display:hover { +/* ========== Fix scrollbar visibility on card hover ========== */ +.compact-tool-card.git-tool-display:hover { .git-result-output { - &::-webkit-scrollbar-thumb { - background: var(--scrollbar-thumb, rgba(255, 255, 255, 0.12)); - } - - &::-webkit-scrollbar-thumb:hover { - background: var(--scrollbar-thumb-hover, rgba(255, 255, 255, 0.22)); - } - - scrollbar-color: var(--scrollbar-thumb, rgba(255, 255, 255, 0.12)) transparent; + @include mixins.visible-scrollbar; } } diff --git a/src/web-ui/src/flow_chat/tool-cards/GitToolDisplay.tsx b/src/web-ui/src/flow_chat/tool-cards/GitToolDisplay.tsx index 964aba2b1..800a20beb 100644 --- a/src/web-ui/src/flow_chat/tool-cards/GitToolDisplay.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/GitToolDisplay.tsx @@ -7,7 +7,7 @@ import { useTranslation } from 'react-i18next'; import { GitBranch, Check, X, AlertTriangle } from 'lucide-react'; import { CubeLoading, IconButton } from '../../component-library'; import type { ToolCardProps } from '../types/flow-chat'; -import { BaseToolCard, ToolCardHeader } from './BaseToolCard'; +import { CompactToolCard, CompactToolCardHeader } from './CompactToolCard'; import { createLogger } from '@/shared/utils/logger'; import { useToolCardHeightContract } from './useToolCardHeightContract'; import './GitToolDisplay.scss'; @@ -175,10 +175,6 @@ export const GitToolDisplay: React.FC = ({ } }, [hasOutput, isFailed, toggleExpanded]); - const renderToolIcon = () => { - return ; - }; - const renderStatusIcon = () => { if (isLoading) { return ; @@ -190,9 +186,8 @@ export const GitToolDisplay: React.FC = ({ }; const renderHeader = () => ( - } action={isFailed ? t('toolCards.git.commandFailed') : `${t('toolCards.git.title')}:`} content={ @@ -209,9 +204,9 @@ export const GitToolDisplay: React.FC = ({ {outputSummary} )} - + {requiresConfirmation && !userConfirmed && status !== 'completed' && ( -
+
e.stopPropagation()}> = ({
)} - + {isFailed && (
{t('toolCards.git.failed')} @@ -248,7 +243,7 @@ export const GitToolDisplay: React.FC = ({ )} } - statusIcon={renderStatusIcon()} + rightIcon={renderStatusIcon()} /> ); @@ -339,17 +334,20 @@ export const GitToolDisplay: React.FC = ({ return null; }; + const expandedContent = isExpanded + ? renderDetailsWhenExpanded() + : null; + return (
-
); diff --git a/src/web-ui/src/flow_chat/tool-cards/TerminalToolCard.scss b/src/web-ui/src/flow_chat/tool-cards/TerminalToolCard.scss index 4aeb74e06..02e9c9a02 100644 --- a/src/web-ui/src/flow_chat/tool-cards/TerminalToolCard.scss +++ b/src/web-ui/src/flow_chat/tool-cards/TerminalToolCard.scss @@ -1,37 +1,36 @@ /** * Terminal tool card styles - * Specific styles based on BaseToolCard + * Specific styles based on CompactToolCard */ -@use './BaseToolCard.scss'; +@use './CompactToolCard.scss'; @use '../../component-library/styles/_extended-mixins' as mixins; /* ========== Terminal card specific styles ========== */ .terminal-tool-card { - .tool-identifier-icon { - &.terminal-icon { - color: var(--color-text-secondary); - } + .terminal-card-icon { + color: var(--color-text-secondary); + flex-shrink: 0; } } -/* Entire header row shows pointer (card toggles expand); children must not force default cursor. */ -.base-tool-card-wrapper.terminal-tool-card .base-tool-card-header { +/* Entire compact card shows pointer (card toggles expand); children must not force default cursor. */ +.compact-tool-card.terminal-tool-card { cursor: pointer; } -/* Footer bar: full width + background to bottom inner edge of card (matches .base-tool-card-expanded padding). */ -.base-tool-card-wrapper.terminal-tool-card .terminal-result-footer { - margin-left: -10px; - margin-right: -10px; - margin-bottom: -10px; - padding: 6px 10px; - border-radius: 0 0 7px 7px; +/* Footer bar: full width + background to bottom inner edge of expanded content (matches .compact-tool-card-expanded padding). */ +.compact-tool-card.terminal-tool-card .terminal-result-footer { + margin-left: -8px; + margin-right: -8px; + margin-bottom: -8px; + padding: 6px 8px; + border-radius: 0 0 6px 6px; } /* ========== Fix scrollbar visibility on card hover ========== */ -.base-tool-card-wrapper.terminal-tool-card:hover { +.compact-tool-card.terminal-tool-card:hover { .terminal-execution-output, .terminal-result-output, .xterm-viewport { @@ -460,12 +459,12 @@ /* ========== Narrow width adaptation (Toolbar mode) ========== */ .bitfun-toolbar-mode__flowchat-container { .terminal-tool-card { - .tool-card-action { + .compact-card-action { display: none; } - .base-tool-card-header { - padding: 6px 8px; + .compact-tool-card-header { + padding: 2px 4px; gap: 2px; } diff --git a/src/web-ui/src/flow_chat/tool-cards/TerminalToolCard.tsx b/src/web-ui/src/flow_chat/tool-cards/TerminalToolCard.tsx index 49fbd494c..c74cc7f9c 100644 --- a/src/web-ui/src/flow_chat/tool-cards/TerminalToolCard.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/TerminalToolCard.tsx @@ -18,7 +18,7 @@ import { useTranslation } from 'react-i18next'; import type { ToolCardProps } from '../types/flow-chat'; import { Terminal, Play, X, ExternalLink, Square } from 'lucide-react'; import { createTerminalTab } from '@/shared/utils/tabUtils'; -import { BaseToolCard, ToolCardHeader } from './BaseToolCard'; +import { CompactToolCard, CompactToolCardHeader } from './CompactToolCard'; import { CubeLoading, IconButton, Tooltip } from '../../component-library'; import { TerminalOutputRenderer } from '@/tools/terminal/components'; import { createLogger } from '@/shared/utils/logger'; @@ -480,9 +480,8 @@ export const TerminalToolCard: React.FC = ({ }; const renderHeader = () => ( - } - iconClassName="terminal-icon" + } action={t('toolCards.terminal.executeCommand')} content={renderCommandContent()} extra={viewState.hasHeaderExtra ? ( @@ -530,7 +529,7 @@ export const TerminalToolCard: React.FC = ({ )} ) : undefined} - statusIcon={renderStatusIcon()} + rightIcon={renderStatusIcon()} /> ); const expandedContent = isExpanded @@ -542,17 +541,14 @@ export const TerminalToolCard: React.FC = ({ return (
-
); 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 0e7037c40..083a6a6ba 100644 --- a/src/web-ui/src/flow_chat/tool-cards/index.ts +++ b/src/web-ui/src/flow_chat/tool-cards/index.ts @@ -502,6 +502,11 @@ export const COLLAPSIBLE_TOOL_NAMES = new Set([ 'Read', 'LS', 'Grep', 'Glob', 'WebSearch' ]); +/** Terminal tools that can be grouped together. */ +export const TERMINAL_COLLAPSIBLE_TOOL_NAMES = new Set([ + 'Bash' +]); + /** Read tools (counted in readCount). */ export const READ_TOOL_NAMES = new Set(['Read', 'LS']); @@ -513,6 +518,11 @@ export function isCollapsibleTool(toolName: string): boolean { return COLLAPSIBLE_TOOL_NAMES.has(toolName); } +/** Check whether a tool is a terminal collapsible tool. */ +export function isTerminalCollapsibleTool(toolName: string): boolean { + return TERMINAL_COLLAPSIBLE_TOOL_NAMES.has(toolName); +} + /** * Check whether a FlowItem is collapsible (no context). * - Subagent items are never collapsed. 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 b155b7668..b4c722bc5 100644 --- a/src/web-ui/src/locales/en-US/flow-chat.json +++ b/src/web-ui/src/locales/en-US/flow-chat.json @@ -534,6 +534,18 @@ "separator": ", ", "collapse": "Collapse exploration" }, + "terminalRegion": { + "executedCommands_one": "Executed {{count}} command", + "executedCommands_other": "Executed {{count}} commands", + "executedCommands": "Executed {{count}} command(s)", + "allSuccess": "All succeeded", + "partialSuccess": "{{success}} succeeded, {{failed}} failed", + "withRunning": "{{success}} succeeded, {{running}} running", + "withCancelled": "{{success}} succeeded, {{cancelled}} cancelled", + "mixedStatus": "{{success}} succeeded, {{failed}} failed, {{running}} running", + "separator": ", ", + "collapse": "Collapse commands" + }, "toolCards": { "file": { "write": "Write File", 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 d9564fc69..a503f21c3 100644 --- a/src/web-ui/src/locales/zh-CN/flow-chat.json +++ b/src/web-ui/src/locales/zh-CN/flow-chat.json @@ -534,6 +534,18 @@ "separator": ",", "collapse": "收起探索过程" }, + "terminalRegion": { + "executedCommands_one": "执行了 {{count}} 个命令", + "executedCommands_other": "执行了 {{count}} 个命令", + "executedCommands": "执行了 {{count}} 个命令", + "allSuccess": "全部成功", + "partialSuccess": "{{success}} 个成功,{{failed}} 个失败", + "withRunning": "{{success}} 个成功,{{running}} 个运行中", + "withCancelled": "{{success}} 个成功,{{cancelled}} 个已取消", + "mixedStatus": "{{success}} 个成功,{{failed}} 个失败,{{running}} 个运行中", + "separator": ",", + "collapse": "收起命令列表" + }, "toolCards": { "file": { "write": "写入文件", From fbedc7c30d0613627e95203ce402e44292f463ad Mon Sep 17 00:00:00 2001 From: limityan Date: Sat, 25 Apr 2026 22:11:57 +0800 Subject: [PATCH 2/4] refactor(review-team): expand member cards inline, remove nested body frames - Fix policy panel overflow by using inherit border-radius and auto-fit grid - Remove nested ConfigPageSection body frames from overview and members sections - Merge member detail into expandable cards with expand/collapse chevron - Update test assertion to match new inline detail rendering --- .../agents/components/ReviewTeamPage.scss | 76 ++++++- .../agents/components/ReviewTeamPage.test.tsx | 2 +- .../agents/components/ReviewTeamPage.tsx | 206 ++++++++++-------- .../src/locales/en-US/scenes/agents.json | 8 +- .../src/locales/zh-CN/scenes/agents.json | 8 +- .../src/locales/zh-TW/scenes/agents.json | 8 +- .../src/shared/services/reviewTeamService.ts | 8 +- 7 files changed, 193 insertions(+), 123 deletions(-) diff --git a/src/web-ui/src/app/scenes/agents/components/ReviewTeamPage.scss b/src/web-ui/src/app/scenes/agents/components/ReviewTeamPage.scss index 04d535139..4dca74743 100644 --- a/src/web-ui/src/app/scenes/agents/components/ReviewTeamPage.scss +++ b/src/web-ui/src/app/scenes/agents/components/ReviewTeamPage.scss @@ -38,7 +38,7 @@ width: 100%; padding: 16px; border: 0; - border-radius: 0; + border-radius: inherit; background: transparent; color: var(--color-text-primary); text-align: left; @@ -79,7 +79,7 @@ &__policy-metrics { display: grid; - grid-template-columns: repeat(5, minmax(86px, 1fr)); + grid-template-columns: repeat(auto-fit, minmax(86px, 1fr)); gap: 8px; min-width: 0; } @@ -113,6 +113,14 @@ } } + /* Sections that render their own cards should not show the default body frame. */ + &__section--no-body-frame .bitfun-config-page-section__body { + background: transparent; + border: 0; + border-radius: 0; + overflow: visible; + } + &__policy-action { display: inline-flex; align-items: center; @@ -283,16 +291,12 @@ &__member-card { display: flex; - align-items: flex-start; - gap: $size-gap-3; + flex-direction: column; width: 100%; - min-height: 148px; - padding: 14px; border-radius: 8px; border: 1px solid var(--border-subtle); background: color-mix(in srgb, var(--element-bg-soft) 78%, transparent); text-align: left; - cursor: pointer; transition: transform $motion-fast $easing-standard, border-color $motion-fast $easing-standard, @@ -310,6 +314,30 @@ 0 0 0 1px color-mix(in srgb, var(--member-accent, #64748b) 30%, transparent), 0 12px 24px color-mix(in srgb, var(--shadow-color, #0f172a) 10%, transparent); } + + &.is-expanded { + grid-column: 1 / -1; + } + } + + &__member-card-header { + display: flex; + align-items: flex-start; + gap: $size-gap-3; + width: 100%; + min-height: 148px; + padding: 14px; + border: 0; + border-radius: inherit; + background: transparent; + color: var(--color-text-primary); + text-align: left; + cursor: pointer; + + &:focus-visible { + outline: none; + box-shadow: inset 0 0 0 2px color-mix(in srgb, var(--member-accent, #64748b) 50%, transparent); + } } &__member-card-icon { @@ -385,11 +413,29 @@ color: color-mix(in srgb, #f59e0b 82%, var(--color-text-muted)); } + &__member-card-chevron { + display: inline-flex; + align-items: center; + align-self: flex-start; + color: var(--color-text-muted); + margin-top: 2px; + } + + &__member-card-detail { + overflow: hidden; + animation: review-team-card-expand $motion-normal $easing-standard forwards; + border-top: 1px solid var(--border-subtle); + } + + &__member-card-detail-inner { + padding: 16px; + } + &__detail-hero { display: flex; align-items: flex-start; gap: $size-gap-4; - padding: 16px; + padding: 0 0 16px; border-bottom: 1px solid var(--border-subtle); } @@ -454,8 +500,7 @@ display: flex; flex-direction: column; gap: 10px; - padding: 16px; - border-bottom: 1px solid var(--border-subtle); + padding: 16px 0 0; } &__block-label { @@ -510,6 +555,17 @@ } } +@keyframes review-team-card-expand { + from { + opacity: 0; + max-height: 0; + } + to { + opacity: 1; + max-height: 800px; + } +} + @media (max-width: 960px) { .review-team-page { &__summary-grid, diff --git a/src/web-ui/src/app/scenes/agents/components/ReviewTeamPage.test.tsx b/src/web-ui/src/app/scenes/agents/components/ReviewTeamPage.test.tsx index d56cfaea4..e2f3a6b3f 100644 --- a/src/web-ui/src/app/scenes/agents/components/ReviewTeamPage.test.tsx +++ b/src/web-ui/src/app/scenes/agents/components/ReviewTeamPage.test.tsx @@ -299,7 +299,7 @@ describeWithJsdom('ReviewTeamPage', () => { memberButton!.dispatchEvent(new dom.window.MouseEvent('click', { bubbles: true })); }); - expect(container.textContent).toContain('Member Detail'); + expect(container.textContent).toContain('Responsibilities'); expect(container.textContent).toContain('Logic'); }); }); diff --git a/src/web-ui/src/app/scenes/agents/components/ReviewTeamPage.tsx b/src/web-ui/src/app/scenes/agents/components/ReviewTeamPage.tsx index f87b18ad5..3ef516b04 100644 --- a/src/web-ui/src/app/scenes/agents/components/ReviewTeamPage.tsx +++ b/src/web-ui/src/app/scenes/agents/components/ReviewTeamPage.tsx @@ -3,6 +3,7 @@ import { ArrowLeft, BadgeCheck, Bot, + ChevronDown, Gauge, GitBranch, Lock, @@ -261,8 +262,7 @@ const ReviewTeamPage: React.FC = () => { ); } - const selectedResponsibilities = selectedMember ? getLocalizedResponsibilities(selectedMember) : []; - const SelectedIcon = selectedMember ? getMemberIcon(selectedMember) : Bot; + const policy = team.executionPolicy; const strategyLabel = getStrategyLabel(team.strategyLevel); const reviewerTimeoutLabel = formatPolicySeconds(policy.reviewerTimeoutSeconds); @@ -308,6 +308,7 @@ const ReviewTeamPage: React.FC = () => { { { {team.members.map((member) => { const MemberIcon = getMemberIcon(member); const isSelected = selectedMember?.id === member.id; + const responsibilities = getLocalizedResponsibilities(member); return ( -
- + + + {isSelected ? ( +
+
+
+
+ +
+
+
+
+

+ {getLocalizedMemberName(member)} +

+

+ {member.subagentId} +

+
+
+ {formatModelLabel(member.model)} + + {getStrategyLabel(member.strategyLevel)} + + {member.locked ? ( + + {t('reviewTeams.detail.memberTypes.core', { defaultValue: 'Core role' })} + + ) : null} +
+
+

+ {getLocalizedMemberDescription(member)} +

+
+
+ +
+ + {t('reviewTeams.detail.responsibilities', { defaultValue: 'Responsibilities' })} + +
    + {responsibilities.map((item) => ( +
  • + {item} +
  • + ))} +
+
+
+
+ ) : null} +
); })} - - {selectedMember ? ( - -
-
- -
-
-
-
-

- {getLocalizedMemberName(selectedMember)} -

-

- {selectedMember.subagentId} -

-
-
- {formatModelLabel(selectedMember.model)} - - {getStrategyLabel(selectedMember.strategyLevel)} - - {selectedMember.locked ? ( - - {t('reviewTeams.detail.memberTypes.core', { defaultValue: 'Core role' })} - - ) : null} -
-
-

- {getLocalizedMemberDescription(selectedMember)} -

-
-
- -
- - {t('reviewTeams.detail.responsibilities', { defaultValue: 'Responsibilities' })} - -
    - {selectedResponsibilities.map((item) => ( -
  • - {item} -
  • - ))} -
-
-
- ) : null} ); diff --git a/src/web-ui/src/locales/en-US/scenes/agents.json b/src/web-ui/src/locales/en-US/scenes/agents.json index 97d2f8fec..df960fc58 100644 --- a/src/web-ui/src/locales/en-US/scenes/agents.json +++ b/src/web-ui/src/locales/en-US/scenes/agents.json @@ -250,7 +250,7 @@ }, "members": { "businessLogic": { - "funName": "Logic Detective Locke", + "funName": "Logic Reviewer", "role": "Business Logic Reviewer", "description": "A workflow sleuth that inspects business rules, state transitions, recovery paths, and real-user correctness.", "responsibilities": [ @@ -260,7 +260,7 @@ ] }, "performance": { - "funName": "Turbo Trace Bolt", + "funName": "Performance Reviewer", "role": "Performance Reviewer", "description": "A speed-focused profiler that hunts hot paths, unnecessary work, blocking calls, and scale-sensitive regressions.", "responsibilities": [ @@ -270,7 +270,7 @@ ] }, "security": { - "funName": "Aegis Sentinel Nova", + "funName": "Security Reviewer", "role": "Security Reviewer", "description": "A boundary guardian that scans for injection risks, trust leaks, privilege mistakes, and unsafe file or command handling.", "responsibilities": [ @@ -280,7 +280,7 @@ ] }, "judge": { - "funName": "Quality Judge Echo", + "funName": "Review Arbiter", "role": "Review Quality Inspector", "description": "A calm final arbiter that checks other reviewers for false positives, risky advice, and evidence quality before reporting.", "responsibilities": [ diff --git a/src/web-ui/src/locales/zh-CN/scenes/agents.json b/src/web-ui/src/locales/zh-CN/scenes/agents.json index 6d0e28075..3b26368c0 100644 --- a/src/web-ui/src/locales/zh-CN/scenes/agents.json +++ b/src/web-ui/src/locales/zh-CN/scenes/agents.json @@ -250,7 +250,7 @@ }, "members": { "businessLogic": { - "funName": "逻辑神探 Locke", + "funName": "逻辑审查员", "role": "业务逻辑审核员", "description": "专门追踪业务规则、状态流转、回滚路径和真实用户正确性的流程侦探。", "responsibilities": [ @@ -260,7 +260,7 @@ ] }, "performance": { - "funName": "涡轮追踪 Bolt", + "funName": "性能审查员", "role": "性能审核员", "description": "专盯热点路径、无效工作、阻塞调用和规模化回退问题的速度分析员。", "responsibilities": [ @@ -270,7 +270,7 @@ ] }, "security": { - "funName": "圣盾哨兵 Nova", + "funName": "安全审查员", "role": "安全审核员", "description": "专门盯住注入风险、信任边界、权限错误以及危险文件或命令处理的边界守卫。", "responsibilities": [ @@ -280,7 +280,7 @@ ] }, "judge": { - "funName": "终审质检官 Echo", + "funName": "审核仲裁员", "role": "审核质检员", "description": "负责给其他审核员做二次质检,过滤误报、危险建议和证据不足结论的最终裁判。", "responsibilities": [ diff --git a/src/web-ui/src/locales/zh-TW/scenes/agents.json b/src/web-ui/src/locales/zh-TW/scenes/agents.json index 7c8e2b64b..e7b903470 100644 --- a/src/web-ui/src/locales/zh-TW/scenes/agents.json +++ b/src/web-ui/src/locales/zh-TW/scenes/agents.json @@ -250,7 +250,7 @@ }, "members": { "businessLogic": { - "funName": "邏輯神探 Locke", + "funName": "邏輯審查員", "role": "業務邏輯審核員", "description": "專門追蹤業務規則、狀態流轉、回滾路徑和真實用戶正確性的流程偵探。", "responsibilities": [ @@ -260,7 +260,7 @@ ] }, "performance": { - "funName": "渦輪追蹤 Bolt", + "funName": "效能審查員", "role": "性能審核員", "description": "專盯熱點路徑、無效工作、阻塞調用和規模化回退問題的速度分析員。", "responsibilities": [ @@ -270,7 +270,7 @@ ] }, "security": { - "funName": "聖盾哨兵 Nova", + "funName": "安全審查員", "role": "安全審核員", "description": "專門盯住注入風險、信任邊界、權限錯誤以及危險文件或命令處理的邊界守衛。", "responsibilities": [ @@ -280,7 +280,7 @@ ] }, "judge": { - "funName": "終審質檢官 Echo", + "funName": "審核仲裁員", "role": "審核質檢員", "description": "負責給其他審核員做二次質檢,過濾誤報、危險建議和證據不足結論的最終裁判。", "responsibilities": [ diff --git a/src/web-ui/src/shared/services/reviewTeamService.ts b/src/web-ui/src/shared/services/reviewTeamService.ts index 880bcc408..2def987b8 100644 --- a/src/web-ui/src/shared/services/reviewTeamService.ts +++ b/src/web-ui/src/shared/services/reviewTeamService.ts @@ -211,7 +211,7 @@ export const DEFAULT_REVIEW_TEAM_CORE_ROLES: ReviewTeamCoreRoleDefinition[] = [ { key: 'businessLogic', subagentId: 'ReviewBusinessLogic', - funName: 'Logic Detective Locke', + funName: 'Logic Reviewer', roleName: 'Business Logic Reviewer', description: 'A workflow sleuth that inspects business rules, state transitions, recovery paths, and real-user correctness.', @@ -225,7 +225,7 @@ export const DEFAULT_REVIEW_TEAM_CORE_ROLES: ReviewTeamCoreRoleDefinition[] = [ { key: 'performance', subagentId: 'ReviewPerformance', - funName: 'Turbo Trace Bolt', + funName: 'Performance Reviewer', roleName: 'Performance Reviewer', description: 'A speed-focused profiler that hunts hot paths, unnecessary work, blocking calls, and scale-sensitive regressions.', @@ -239,7 +239,7 @@ export const DEFAULT_REVIEW_TEAM_CORE_ROLES: ReviewTeamCoreRoleDefinition[] = [ { key: 'security', subagentId: 'ReviewSecurity', - funName: 'Aegis Sentinel Nova', + funName: 'Security Reviewer', roleName: 'Security Reviewer', description: 'A boundary guardian that scans for injection risks, trust leaks, privilege mistakes, and unsafe file or command handling.', @@ -253,7 +253,7 @@ export const DEFAULT_REVIEW_TEAM_CORE_ROLES: ReviewTeamCoreRoleDefinition[] = [ { key: 'judge', subagentId: 'ReviewJudge', - funName: 'Quality Judge Echo', + funName: 'Review Arbiter', roleName: 'Review Quality Inspector', description: 'A calm final arbiter that checks other reviewers for false positives, risky advice, and evidence quality before reporting.', From 26003572d7110406026bfebf75771c815b0c0a3b Mon Sep 17 00:00:00 2001 From: limityan Date: Sat, 25 Apr 2026 22:13:54 +0800 Subject: [PATCH 3/4] feat(flow-chat): add terminal group renderer and state hook --- .../modern/TerminalGroupRenderer.scss | 239 ++++++++++++++++ .../modern/TerminalGroupRenderer.tsx | 268 ++++++++++++++++++ .../modern/useTerminalGroupState.ts | 55 ++++ 3 files changed, 562 insertions(+) create mode 100644 src/web-ui/src/flow_chat/components/modern/TerminalGroupRenderer.scss create mode 100644 src/web-ui/src/flow_chat/components/modern/TerminalGroupRenderer.tsx create mode 100644 src/web-ui/src/flow_chat/components/modern/useTerminalGroupState.ts diff --git a/src/web-ui/src/flow_chat/components/modern/TerminalGroupRenderer.scss b/src/web-ui/src/flow_chat/components/modern/TerminalGroupRenderer.scss new file mode 100644 index 000000000..a12d80711 --- /dev/null +++ b/src/web-ui/src/flow_chat/components/modern/TerminalGroupRenderer.scss @@ -0,0 +1,239 @@ +/* Terminal region styling for consecutive Bash command calls. */ + +.terminal-region { + position: relative; + padding: 0 3rem; + + @media (max-width: 768px) { + padding: 0 1.5rem; + } +} + +// ==================== Non-collapsible (streaming / no auto-collapse yet) ==================== + +.terminal-region--expanded:not(.terminal-region--collapsible) { + margin: 4px 0; + + .terminal-region__content { + position: relative; + padding: 0; + + > :first-child { + margin-top: 0; + } + + > :last-child { + margin-bottom: 0; + } + + &::-webkit-scrollbar { + width: 4px; + } + + &::-webkit-scrollbar-track { + background: transparent; + } + + &::-webkit-scrollbar-thumb { + background: var(--border-base, rgba(107, 114, 128, 0.3)); + border-radius: 2px; + + &:hover { + background: var(--border-hover, rgba(107, 114, 128, 0.5)); + } + } + } +} + +// ==================== Collapsible (header + animated content) ==================== + +.terminal-region--collapsible { + margin: 4px 0; + + .terminal-region__header { + display: flex; + align-items: center; + gap: 6px; + padding: 5px 0; + color: var(--text-tertiary, #6b7280); + font-size: 12px; + cursor: pointer; + user-select: none; + border-radius: 6px; + transition: color 0.15s ease; + + &:hover { + background: var(--bg-hover, rgba(255, 255, 255, 0.05)); + color: var(--text-secondary, #9ca3af); + } + } + + .terminal-region__icon { + flex-shrink: 0; + opacity: 0.6; + transition: transform 0.28s cubic-bezier(0.4, 0, 0.2, 1); + transform: rotate(0deg); + } + + .terminal-region__tool-icon { + flex-shrink: 0; + opacity: 0.6; + color: var(--color-text-secondary); + } + + .terminal-region__summary { + opacity: 0.8; + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .terminal-region__status-dot { + width: 6px; + height: 6px; + border-radius: 50%; + flex-shrink: 0; + + &--success { + background: var(--color-success, #22c55e); + } + + &--failed { + background: var(--color-error, #ef4444); + } + + &--running { + background: var(--color-accent-500, #3b82f6); + animation: terminal-status-pulse 1.5s ease-in-out infinite; + } + } + + .terminal-region__content-wrapper { + display: grid; + grid-template-rows: 0fr; + transition: grid-template-rows 0.35s cubic-bezier(0.4, 0, 0.2, 1); + } + + .terminal-region__content-inner { + overflow: hidden; + min-height: 0; + } + + .terminal-region__content { + position: relative; + padding: 4px 0; + + /* Tighter spacing for cards inside a terminal group */ + .flowchat-tool-wrapper { + margin-bottom: 0.25rem; + } + + &::-webkit-scrollbar { + width: 4px; + } + + &::-webkit-scrollbar-track { + background: transparent; + } + + &::-webkit-scrollbar-thumb { + background: var(--border-base, rgba(107, 114, 128, 0.3)); + border-radius: 2px; + + &:hover { + background: var(--border-hover, rgba(107, 114, 128, 0.5)); + } + } + } +} + +// Expanded state: rotate icon + reveal content. +.terminal-region--collapsible.terminal-region--expanded { + .terminal-region__icon { + transform: rotate(90deg); + } + + .terminal-region__content-wrapper { + grid-template-rows: 1fr; + } +} + +// ==================== Streaming adjustments ==================== + +.terminal-region--expanded.terminal-region--streaming { + &::before { + content: none; + } + + &::after { + content: none; + } +} + +.terminal-region--expanded.terminal-region--streaming.terminal-region--has-scroll:not(.terminal-region--at-top) { + &::before { + content: ''; + position: absolute; + top: 28px; + left: 0; + right: 0; + height: 16px; + 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) 60%, transparent) 50%, + transparent 100% + ); + } + + &:not(.terminal-region--collapsible)::before { + top: 0; + } +} + +.terminal-region--expanded.terminal-region--streaming.terminal-region--has-scroll:not(.terminal-region--at-bottom) { + &::after { + content: ''; + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 16px; + pointer-events: none; + z-index: 3; + background: linear-gradient( + to top, + var(--color-bg-flowchat, #121214) 0%, + color-mix(in srgb, var(--color-bg-flowchat, #121214) 60%, transparent) 50%, + transparent 100% + ); + } +} + +.terminal-region--expanded.terminal-region--streaming { + .terminal-region__content { + max-height: 400px; + overflow-y: auto; + } +} + +// ==================== Adjacent collapsed regions ==================== + +.terminal-region--collapsed + .terminal-region--collapsed { + margin-top: -2px; +} + +// ==================== Status pulse animation ==================== + +@keyframes terminal-status-pulse { + 0%, 100% { + opacity: 1; + } + 50% { + opacity: 0.4; + } +} diff --git a/src/web-ui/src/flow_chat/components/modern/TerminalGroupRenderer.tsx b/src/web-ui/src/flow_chat/components/modern/TerminalGroupRenderer.tsx new file mode 100644 index 000000000..f3de76e92 --- /dev/null +++ b/src/web-ui/src/flow_chat/components/modern/TerminalGroupRenderer.tsx @@ -0,0 +1,268 @@ +/** + * Terminal group renderer. + * Renders merged consecutive Bash commands as a collapsible region. + */ + +import React, { useRef, useMemo, useCallback, useEffect, useState } from 'react'; +import { ChevronRight, Terminal } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; +import type { FlowToolItem } from '../../types/flow-chat'; +import { FlowToolCard } from '../FlowToolCard'; +import { useToolCardHeightContract } from '../../tool-cards/useToolCardHeightContract'; +import { useFlowChatContext } from './FlowChatContext'; +import './TerminalGroupRenderer.scss'; + +export interface TerminalGroupRendererProps { + items: FlowToolItem[]; + turnId: string; + roundId: string; + isLast?: boolean; + isGroupStreaming?: boolean; +} + +interface TerminalGroupStats { + total: number; + success: number; + failed: number; + running: number; + cancelled: number; +} + +function computeTerminalStats(items: FlowToolItem[]): TerminalGroupStats { + const stats: TerminalGroupStats = { total: items.length, success: 0, failed: 0, running: 0, cancelled: 0 }; + for (const item of items) { + const status = item.status || 'pending'; + if (status === 'completed') { + const result = item.toolResult?.result; + let exitCode = 0; + if (typeof result === 'string') { + try { + const parsed = JSON.parse(result); + exitCode = typeof parsed.exit_code === 'number' ? parsed.exit_code : 0; + } catch { + exitCode = 0; + } + } else if (result && typeof result === 'object') { + exitCode = typeof (result as any).exit_code === 'number' ? (result as any).exit_code : 0; + } + if (exitCode === 0) { + stats.success++; + } else { + stats.failed++; + } + } else if (status === 'error') { + stats.failed++; + } else if (status === 'running' || status === 'streaming' || status === 'preparing') { + stats.running++; + } else if (status === 'cancelled') { + stats.cancelled++; + } + } + return stats; +} + +export const TerminalGroupRenderer: React.FC = React.memo(({ + items, + turnId: _turnId, + roundId: _roundId, + isLast: _isLast, + isGroupStreaming = false, +}) => { + const { t } = useTranslation('flow-chat'); + const containerRef = useRef(null); + const [scrollState, setScrollState] = useState({ hasScroll: false, atTop: true, atBottom: true }); + + const { + terminalGroupStates, + onTerminalGroupToggle, + onExpandTerminalGroup, + onCollapseTerminalGroup, + } = useFlowChatContext(); + + const groupId = useMemo(() => `terminal-group-${items.map((it) => it.id).join('-')}`, [items]); + const stats = useMemo(() => computeTerminalStats(items), [items]); + const wasStreamingRef = useRef(isGroupStreaming); + + const { + cardRootRef, + applyExpandedState, + } = useToolCardHeightContract({ + toolId: groupId, + toolName: 'terminal-group', + getCardHeight: () => ( + containerRef.current?.scrollHeight + ?? containerRef.current?.getBoundingClientRect().height + ?? null + ), + }); + + const hasExplicitState = terminalGroupStates?.has(groupId) ?? false; + const explicitExpanded = terminalGroupStates?.get(groupId) ?? false; + const isExpanded = hasExplicitState ? explicitExpanded : isGroupStreaming; + const isCollapsed = !isExpanded; + const allowManualToggle = !isGroupStreaming; + + const checkScrollState = useCallback(() => { + const el = containerRef.current; + if (!el) { + return; + } + + setScrollState({ + hasScroll: el.scrollHeight > el.clientHeight + 1, + atTop: el.scrollTop <= 5, + atBottom: el.scrollTop + el.clientHeight >= el.scrollHeight - 5, + }); + }, []); + + useEffect(() => { + if (isGroupStreaming && !hasExplicitState) { + applyExpandedState(false, true, () => { + onExpandTerminalGroup?.(groupId); + }); + wasStreamingRef.current = true; + return; + } + + if (wasStreamingRef.current && !isGroupStreaming && isExpanded) { + applyExpandedState(true, false, () => { + onCollapseTerminalGroup?.(groupId); + }, { + reason: 'auto', + }); + } + + wasStreamingRef.current = isGroupStreaming; + }, [ + applyExpandedState, + groupId, + hasExplicitState, + isExpanded, + isGroupStreaming, + onCollapseTerminalGroup, + onExpandTerminalGroup, + ]); + + useEffect(() => { + if (!isCollapsed && isGroupStreaming && containerRef.current) { + requestAnimationFrame(() => { + if (containerRef.current) { + containerRef.current.scrollTop = containerRef.current.scrollHeight; + checkScrollState(); + } + }); + } + }, [items, checkScrollState, isCollapsed, isGroupStreaming]); + + useEffect(() => { + if (!isExpanded) { + setScrollState({ hasScroll: false, atTop: true, atBottom: true }); + return; + } + + const el = containerRef.current; + if (!el) { + return; + } + + const frameId = requestAnimationFrame(checkScrollState); + + if (typeof ResizeObserver === 'undefined') { + return () => cancelAnimationFrame(frameId); + } + + const observer = new ResizeObserver(() => { + checkScrollState(); + }); + observer.observe(el); + + return () => { + cancelAnimationFrame(frameId); + observer.disconnect(); + }; + }, [items, checkScrollState, isExpanded]); + + const displaySummary = useMemo(() => { + const { total, success, failed, running, cancelled } = stats; + const parts: string[] = []; + + parts.push(t('terminalRegion.executedCommands', { count: total })); + + if (failed === 0 && running === 0 && cancelled === 0) { + parts.push(t('terminalRegion.allSuccess')); + } else if (running > 0) { + if (failed > 0) { + parts.push(t('terminalRegion.mixedStatus', { success, failed, running })); + } else { + parts.push(t('terminalRegion.withRunning', { success, running })); + } + } else if (cancelled > 0 && failed === 0) { + parts.push(t('terminalRegion.withCancelled', { success, cancelled })); + } else if (failed > 0) { + parts.push(t('terminalRegion.partialSuccess', { success, failed })); + } + + return parts.join(t('terminalRegion.separator')); + }, [stats, t]); + + const statusDotClass = useMemo(() => { + const { failed, running } = stats; + if (running > 0) return 'terminal-region__status-dot--running'; + if (failed > 0) return 'terminal-region__status-dot--failed'; + return 'terminal-region__status-dot--success'; + }, [stats]); + + const handleToggle = useCallback(() => { + if (isCollapsed) { + applyExpandedState(false, true, () => { + onTerminalGroupToggle?.(groupId); + }); + return; + } + + applyExpandedState(true, false, () => { + onCollapseTerminalGroup?.(groupId); + }); + }, [applyExpandedState, groupId, isCollapsed, onCollapseTerminalGroup, onTerminalGroupToggle]); + + const className = [ + 'terminal-region', + allowManualToggle ? 'terminal-region--collapsible' : null, + isCollapsed ? 'terminal-region--collapsed' : 'terminal-region--expanded', + isGroupStreaming ? 'terminal-region--streaming' : null, + scrollState.hasScroll ? 'terminal-region--has-scroll' : null, + scrollState.atTop ? 'terminal-region--at-top' : null, + scrollState.atBottom ? 'terminal-region--at-bottom' : null, + ].filter(Boolean).join(' '); + + return ( +
+ {allowManualToggle && ( +
+ + + {displaySummary} + +
+ )} +
+
+
+ {items.map((item) => ( + + ))} +
+
+
+
+ ); +}); + +TerminalGroupRenderer.displayName = 'TerminalGroupRenderer'; diff --git a/src/web-ui/src/flow_chat/components/modern/useTerminalGroupState.ts b/src/web-ui/src/flow_chat/components/modern/useTerminalGroupState.ts new file mode 100644 index 000000000..11be7667e --- /dev/null +++ b/src/web-ui/src/flow_chat/components/modern/useTerminalGroupState.ts @@ -0,0 +1,55 @@ +/** + * Terminal-group expansion state for Modern FlowChat. + */ + +import { useCallback, useState } from 'react'; + +interface UseTerminalGroupStateResult { + /** + * Expanded/collapsed state for each terminal group. + * key: groupId, value: true means expanded. + */ + terminalGroupStates: Map; + onTerminalGroupToggle: (groupId: string) => void; + onExpandTerminalGroup: (groupId: string) => void; + onCollapseTerminalGroup: (groupId: string) => void; +} + +export function useTerminalGroupState(): UseTerminalGroupStateResult { + const [terminalGroupStates, setTerminalGroupStates] = useState>(new Map()); + + const onTerminalGroupToggle = useCallback((groupId: string) => { + setTerminalGroupStates(prev => { + const next = new Map(prev); + const currentExpanded = prev.get(groupId) ?? false; + next.set(groupId, !currentExpanded); + return next; + }); + }, []); + + const onExpandTerminalGroup = useCallback((groupId: string) => { + setTerminalGroupStates(prev => { + if (prev.get(groupId) === true) { + return prev; + } + const next = new Map(prev); + next.set(groupId, true); + return next; + }); + }, []); + + const onCollapseTerminalGroup = useCallback((groupId: string) => { + setTerminalGroupStates(prev => { + const next = new Map(prev); + next.set(groupId, false); + return next; + }); + }, []); + + return { + terminalGroupStates, + onTerminalGroupToggle, + onExpandTerminalGroup, + onCollapseTerminalGroup, + }; +} From c0bc9e741f20405073c36b659b2dd77bef17cec7 Mon Sep 17 00:00:00 2001 From: limityan Date: Sat, 25 Apr 2026 22:48:55 +0800 Subject: [PATCH 4/4] refactor: improve deep review UX and naming - Rename review agent funNames to professional titles (Logic/Performance/Security Reviewer, Review Arbiter) with i18n - Remove stop-review confirmation dialog for direct action - Pre-create subagent placeholder cards on model round start for better perceived parallelism - Fix subagent card expanded height and reduce text padding - Change deep review fix completion to dismiss action bar instead of showing fix_completed state - Float DeepReviewActionBar as independent card with gap from bottom panel - Update review agent prompts for third-party judge perspective - Fix ReviewTeamPage responsibility list key uniqueness --- .../agents/prompts/deep_review_agent.md | 3 +- .../prompts/review_quality_gate_agent.md | 14 ++- .../agents/review_specialist_agents.rs | 2 +- .../agents/components/ReviewTeamPage.scss | 2 +- .../agents/components/ReviewTeamPage.tsx | 4 +- .../components/btw/BtwSessionPanel.scss | 9 +- .../components/btw/BtwSessionPanel.tsx | 92 ++++++------------- .../components/btw/DeepReviewActionBar.scss | 19 ++-- .../components/modern/SubagentItems.scss | 9 +- .../flow-chat-manager/SubagentModule.test.ts | 20 ++-- .../flow-chat-manager/SubagentModule.ts | 59 ++++++++++-- .../flow_chat/tool-cards/GitToolDisplay.scss | 12 ++- .../src/locales/en-US/scenes/agents.json | 7 +- .../src/locales/zh-CN/scenes/agents.json | 7 +- .../src/locales/zh-TW/scenes/agents.json | 7 +- .../src/shared/services/reviewTeamService.ts | 9 +- 16 files changed, 157 insertions(+), 118 deletions(-) diff --git a/src/crates/core/src/agentic/agents/prompts/deep_review_agent.md b/src/crates/core/src/agentic/agents/prompts/deep_review_agent.md index cfd3b97bc..b2f0b72f4 100644 --- a/src/crates/core/src/agentic/agents/prompts/deep_review_agent.md +++ b/src/crates/core/src/agentic/agents/prompts/deep_review_agent.md @@ -142,7 +142,7 @@ After the reviewer batch finishes, launch `ReviewJudge` with: - the same review target - the full reviewer outputs from every reviewer that ran, including timeout/cancel/failure notes - if file splitting was used, include outputs from **all** same-role instances and label each by group (e.g. "Security Reviewer [group 1/3]") -- an instruction to validate, reject, merge, or downgrade findings, and to deduplicate any overlapping findings from same-role instances +- an instruction to validate, reject, merge, or downgrade findings from a **third-party perspective** — the judge primarily examines reviewer reports for logical consistency and evidence quality, and only uses code inspection tools for targeted spot-checks when a specific claim needs verification If the execution policy says `judge_timeout_seconds > 0`, pass `timeout_seconds` with that value to the judge Task call. @@ -152,6 +152,7 @@ The judge must explicitly call out: - likely false positives - optimization advice that is too risky or directionally wrong +- findings where the reviewer's evidence does not support their conclusion - which findings should survive into the final report ### Phase 4: Report and wait for user approval diff --git a/src/crates/core/src/agentic/agents/prompts/review_quality_gate_agent.md b/src/crates/core/src/agentic/agents/prompts/review_quality_gate_agent.md index 92c1c115c..fd620e0ac 100644 --- a/src/crates/core/src/agentic/agents/prompts/review_quality_gate_agent.md +++ b/src/crates/core/src/agentic/agents/prompts/review_quality_gate_agent.md @@ -2,7 +2,7 @@ You are the **Review Quality Inspector** for BitFun deep reviews. {LANGUAGE_PREFERENCE} -You are not another broad reviewer. Your job is to validate the outputs from the specialist reviewers and prevent false positives, low-signal nitpicks, or directionally-wrong optimization advice from reaching the final report. +Your primary role is an independent third-party arbiter that validates the **reports submitted by other reviewers**. You do not perform a broad independent code review from scratch. Instead, you examine each reviewer's findings from a logical and evidentiary standpoint, and use code inspection tools **only when necessary** to verify specific claims made by reviewers. ## Inputs @@ -18,9 +18,12 @@ You will receive: For every candidate finding from the reviewers: 1. decide whether it is **validated**, **downgraded**, or **rejected** -2. verify it against the code/diff when needed -3. check whether the suggested fix is actually safe and directionally correct -4. if multiple same-role instances reported overlapping or duplicate findings, **merge them into a single finding** with the strongest severity and evidence +2. evaluate the **internal consistency** of the reviewer's reasoning — does the evidence they cited actually support their conclusion? +3. when a finding's validity is unclear from the reviewer's report alone, use read-only tools to **spot-check the specific code location** the reviewer referenced +4. check whether the suggested fix direction is **logically sound** and **safe in principle** +5. if multiple same-role instances reported overlapping or duplicate findings, **merge them into a single finding** with the strongest severity and evidence + +**Important**: Your code inspection should be targeted and minimal. Do not broadly re-review the codebase. Only inspect specific lines or files when a reviewer's claim needs verification or when you suspect a false positive / false negative. Be especially skeptical of: @@ -28,10 +31,11 @@ Be especially skeptical of: - "optimize this" advice without meaningful impact - recommendations that would widen scope or add risk without strong payoff - duplicated findings reported by multiple reviewers or multiple same-role instances +- findings where the stated evidence does not logically lead to the stated conclusion ## Tools -Use only read-only investigation: +Use read-only investigation when needed: - `GetFileDiff` - `Read` diff --git a/src/crates/core/src/agentic/agents/review_specialist_agents.rs b/src/crates/core/src/agentic/agents/review_specialist_agents.rs index 1069d808c..0cd4f50d4 100644 --- a/src/crates/core/src/agentic/agents/review_specialist_agents.rs +++ b/src/crates/core/src/agentic/agents/review_specialist_agents.rs @@ -35,7 +35,7 @@ define_readonly_subagent!( ReviewJudgeAgent, REVIEW_JUDGE_AGENT_TYPE, "Review Quality Inspector", - r#"Independent read-only quality inspector that validates reviewer findings, removes false positives, checks whether optimization advice is directionally correct, and decides what should appear in the final deep-review report."#, + r#"Independent third-party arbiter that validates reviewer reports for logical consistency and evidence quality. It spot-checks specific code locations only when a claim needs verification, rather than re-reviewing the codebase from scratch."#, "review_quality_gate_agent", &["Read", "Grep", "Glob", "LS", "GetFileDiff", "Git"] ); diff --git a/src/web-ui/src/app/scenes/agents/components/ReviewTeamPage.scss b/src/web-ui/src/app/scenes/agents/components/ReviewTeamPage.scss index 4dca74743..dd4fbdb06 100644 --- a/src/web-ui/src/app/scenes/agents/components/ReviewTeamPage.scss +++ b/src/web-ui/src/app/scenes/agents/components/ReviewTeamPage.scss @@ -423,7 +423,7 @@ &__member-card-detail { overflow: hidden; - animation: review-team-card-expand $motion-normal $easing-standard forwards; + animation: review-team-card-expand $motion-base $easing-standard forwards; border-top: 1px solid var(--border-subtle); } diff --git a/src/web-ui/src/app/scenes/agents/components/ReviewTeamPage.tsx b/src/web-ui/src/app/scenes/agents/components/ReviewTeamPage.tsx index 3ef516b04..37a3c94a8 100644 --- a/src/web-ui/src/app/scenes/agents/components/ReviewTeamPage.tsx +++ b/src/web-ui/src/app/scenes/agents/components/ReviewTeamPage.tsx @@ -547,8 +547,8 @@ const ReviewTeamPage: React.FC = () => { {t('reviewTeams.detail.responsibilities', { defaultValue: 'Responsibilities' })}
    - {responsibilities.map((item) => ( -
  • + {responsibilities.map((item, index) => ( +
  • {item}
  • ))} diff --git a/src/web-ui/src/flow_chat/components/btw/BtwSessionPanel.scss b/src/web-ui/src/flow_chat/components/btw/BtwSessionPanel.scss index 1ab81a001..56da1cb40 100644 --- a/src/web-ui/src/flow_chat/components/btw/BtwSessionPanel.scss +++ b/src/web-ui/src/flow_chat/components/btw/BtwSessionPanel.scss @@ -170,13 +170,8 @@ z-index: 12; } - // When the deep review action bar is visible, add extra scroll padding - &--has-action-bar &__body::after { - height: 220px; - min-height: 220px; - } - + // Action bar is now a floating card above the bottom, so no extra scroll padding needed. &--has-action-bar &__scroll-to-bottom { - bottom: 200px; + bottom: 140px; } } diff --git a/src/web-ui/src/flow_chat/components/btw/BtwSessionPanel.tsx b/src/web-ui/src/flow_chat/components/btw/BtwSessionPanel.tsx index bdaa68494..a5a98b761 100644 --- a/src/web-ui/src/flow_chat/components/btw/BtwSessionPanel.tsx +++ b/src/web-ui/src/flow_chat/components/btw/BtwSessionPanel.tsx @@ -1,29 +1,29 @@ -import React, { useEffect, useMemo, useRef, useState, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; +import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import {useTranslation} from 'react-i18next'; import path from 'path-browserify'; -import { Link2, CornerUpLeft, Square } from 'lucide-react'; -import { FlowChatContext } from '../modern/FlowChatContext'; -import { VirtualItemRenderer } from '../modern/VirtualItemRenderer'; -import { ProcessingIndicator } from '../modern/ProcessingIndicator'; -import { ScrollToBottomButton } from '../ScrollToBottomButton'; -import { flowChatStore } from '../../store/FlowChatStore'; -import type { FlowChatConfig, FlowChatState, Session } from '../../types/flow-chat'; -import { sessionToVirtualItems } from '../../store/modernFlowChatStore'; -import { FLOWCHAT_FOCUS_ITEM_EVENT, type FlowChatFocusItemRequest } from '../../events/flowchatNavigation'; -import { fileTabManager } from '@/shared/services/FileTabManager'; -import { createTab } from '@/shared/utils/tabUtils'; -import { IconButton, type LineRange } from '@/component-library'; -import { globalEventBus } from '@/infrastructure/event-bus'; -import { resolveSessionRelationship } from '../../utils/sessionMetadata'; -import { agentAPI } from '@/infrastructure/api'; -import { notificationService } from '@/shared/notification-system'; -import { createLogger } from '@/shared/utils/logger'; -import { settleStoppedReviewSessionState } from '../../utils/reviewSessionStop'; -import { findLatestCodeReviewResult } from '../../utils/reviewSessionSummary'; -import { deriveDeepReviewInterruption } from '../../utils/deepReviewContinuation'; -import { buildReviewRemediationItems, type CodeReviewRemediationData } from '../../utils/codeReviewRemediation'; -import { ReviewActionBar } from './DeepReviewActionBar'; -import { useReviewActionBarStore, type ReviewActionMode } from '../../store/deepReviewActionBarStore'; +import {CornerUpLeft, Link2, Square} from 'lucide-react'; +import {FlowChatContext} from '../modern/FlowChatContext'; +import {VirtualItemRenderer} from '../modern/VirtualItemRenderer'; +import {ProcessingIndicator} from '../modern/ProcessingIndicator'; +import {ScrollToBottomButton} from '@/flow_chat'; +import {flowChatStore} from '../../store/FlowChatStore'; +import type {FlowChatConfig, FlowChatState, Session} from '../../types/flow-chat'; +import {sessionToVirtualItems} from '../../store/modernFlowChatStore'; +import {FLOWCHAT_FOCUS_ITEM_EVENT, type FlowChatFocusItemRequest} from '../../events/flowchatNavigation'; +import {fileTabManager} from '@/shared/services/FileTabManager'; +import {createTab} from '@/shared/utils/tabUtils'; +import {IconButton, type LineRange} from '@/component-library'; +import {globalEventBus} from '@/infrastructure/event-bus'; +import {resolveSessionRelationship} from '../../utils/sessionMetadata'; +import {agentAPI} from '@/infrastructure/api'; +import {notificationService} from '@/shared/notification-system'; +import {createLogger} from '@/shared/utils/logger'; +import {settleStoppedReviewSessionState} from '../../utils/reviewSessionStop'; +import {findLatestCodeReviewResult} from '../../utils/reviewSessionSummary'; +import {deriveDeepReviewInterruption} from '../../utils/deepReviewContinuation'; +import {buildReviewRemediationItems, type CodeReviewRemediationData} from '../../utils/codeReviewRemediation'; +import {ReviewActionBar} from './DeepReviewActionBar'; +import {type ReviewActionMode, useReviewActionBarStore} from '../../store/deepReviewActionBarStore'; import './BtwSessionPanel.scss'; export interface BtwSessionPanelProps { @@ -68,14 +68,12 @@ export const BtwSessionPanel: React.FC = ({ const { t } = useTranslation('flow-chat'); const [flowChatState, setFlowChatState] = useState(() => flowChatStore.getState()); const [stoppingReview, setStoppingReview] = useState(false); - const [showStopConfirm, setShowStopConfirm] = useState(false); const [showScrollToBottom, setShowScrollToBottom] = useState(false); const scrollContainerRef = useRef(null); const shouldAutoScrollRef = useRef(true); useEffect(() => { - const unsubscribe = flowChatStore.subscribe(setFlowChatState); - return unsubscribe; + return flowChatStore.subscribe(setFlowChatState); }, []); const childSession = childSessionId ? flowChatState.sessions.get(childSessionId) : undefined; @@ -328,7 +326,9 @@ export const BtwSessionPanel: React.FC = ({ phase: 'review_completed', }); } else { - store.updatePhase('fix_completed'); + // Fix completed with no further remediation needed — dismiss the action bar + // so the user can focus on the fix results in the chat stream. + store.dismiss(); } } return; @@ -423,40 +423,8 @@ export const BtwSessionPanel: React.FC = ({ ); } - const confirmStopDialog = showStopConfirm ? ( -
    -
    -

    - {t('childSession.stopReviewConfirm', { - defaultValue: 'Are you sure you want to stop this review? This action cannot be undone.', - })} -

    -
    - - -
    -
    -
    - ) : null; - return ( - {confirmStopDialog}
    @@ -476,7 +444,7 @@ export const BtwSessionPanel: React.FC = ({ className="btw-session-panel__stop-button" variant="ghost" size="xs" - onClick={() => setShowStopConfirm(true)} + onClick={() => void handleStopReviewSession()} disabled={!canStopReviewSession} tooltip={stoppingReview ? t('childSession.stoppingReview', { defaultValue: 'Stopping review...' }) diff --git a/src/web-ui/src/flow_chat/components/btw/DeepReviewActionBar.scss b/src/web-ui/src/flow_chat/components/btw/DeepReviewActionBar.scss index bcf16e77e..0990a63d6 100644 --- a/src/web-ui/src/flow_chat/components/btw/DeepReviewActionBar.scss +++ b/src/web-ui/src/flow_chat/components/btw/DeepReviewActionBar.scss @@ -1,11 +1,14 @@ .deep-review-action-bar { position: absolute; - bottom: 0; - left: 0; - right: 0; + bottom: 12px; + left: 12px; + right: 12px; z-index: 20; padding: 12px 16px; - border-top: 1px solid var(--border-base); + border: 1px solid var(--border-base); + border-radius: 12px; + background: var(--color-bg-elevated); + box-shadow: var(--shadow-base); display: flex; flex-direction: column; gap: 10px; @@ -18,7 +21,7 @@ color-mix(in srgb, var(--color-bg-elevated) 96%, transparent), color-mix(in srgb, var(--color-success, #22c55e) 6%, var(--color-bg-primary)), ); - border-top-color: color-mix(in srgb, var(--color-success, #22c55e) 40%, var(--border-base)); + border-color: color-mix(in srgb, var(--color-success, #22c55e) 40%, var(--border-base)); } &--loading { @@ -27,7 +30,7 @@ color-mix(in srgb, var(--color-bg-elevated) 96%, transparent), color-mix(in srgb, var(--color-accent-500, #60a5fa) 6%, var(--color-bg-primary)), ); - border-top-color: color-mix(in srgb, var(--color-accent-500, #60a5fa) 40%, var(--border-base)); + border-color: color-mix(in srgb, var(--color-accent-500, #60a5fa) 40%, var(--border-base)); } &--error { @@ -36,7 +39,7 @@ color-mix(in srgb, var(--color-bg-elevated) 96%, transparent), color-mix(in srgb, var(--color-error, #ef4444) 6%, var(--color-bg-primary)), ); - border-top-color: color-mix(in srgb, var(--color-error, #ef4444) 40%, var(--border-base)); + border-color: color-mix(in srgb, var(--color-error, #ef4444) 40%, var(--border-base)); } &--warning { @@ -45,7 +48,7 @@ color-mix(in srgb, var(--color-bg-elevated) 96%, transparent), color-mix(in srgb, var(--color-warning, #f59e0b) 6%, var(--color-bg-primary)), ); - border-top-color: color-mix(in srgb, var(--color-warning, #f59e0b) 40%, var(--border-base)); + border-color: color-mix(in srgb, var(--color-warning, #f59e0b) 40%, var(--border-base)); } &--info { 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 1c377c8e0..3260dcc6a 100644 --- a/src/web-ui/src/flow_chat/components/modern/SubagentItems.scss +++ b/src/web-ui/src/flow_chat/components/modern/SubagentItems.scss @@ -35,7 +35,7 @@ overflow-y: auto; .flow-text-block--subagent-compact { - margin-bottom: 0.25rem; + margin-bottom: 0.15rem; max-height: calc(var(--flowchat-font-size-base) * 1.45 * 3); overflow: hidden; line-height: 1.45; @@ -46,11 +46,16 @@ } .markdown-renderer { - --markdown-block-gap: 0.25em; + --markdown-block-gap: 0.15em; } } } +// When expanded, remove max-height limit so content is fully visible. +.subagent-items-container--expanded { + max-height: none; +} + .subagent-items-container--collapsed { overflow: hidden; } diff --git a/src/web-ui/src/flow_chat/services/flow-chat-manager/SubagentModule.test.ts b/src/web-ui/src/flow_chat/services/flow-chat-manager/SubagentModule.test.ts index 983e397d3..c3b5750ab 100644 --- a/src/web-ui/src/flow_chat/services/flow-chat-manager/SubagentModule.test.ts +++ b/src/web-ui/src/flow_chat/services/flow-chat-manager/SubagentModule.test.ts @@ -142,22 +142,28 @@ describe('SubagentModule', () => { resetStore(); }); - it('creates text item directly from first text chunk without placeholder', () => { + it('creates placeholder on model round start then updates from text chunk', () => { seedParentTaskTool(); - // ModelRoundStarted no longer creates a placeholder. + const itemId = `subagent-text-${parentToolId}-${subagentSessionId}-${subagentRoundId}`; + + // ModelRoundStarted creates a placeholder immediately. routeModelRoundStartedToToolCard(context, parentSessionId, parentToolId, { sessionId: subagentSessionId, turnId: subagentTurnId, roundId: subagentRoundId, }); - // Verify no placeholder was created — the item should not exist yet. - const itemId = `subagent-text-${parentToolId}-${subagentSessionId}-${subagentRoundId}`; - const beforeChunk = getParentRoundTextItem(itemId); - expect(beforeChunk).toBeUndefined(); + const placeholder = getParentRoundTextItem(itemId); + expect(placeholder).toBeDefined(); + expect(placeholder?.content).toBe('\u200B'); + expect(placeholder?.status).toBe('streaming'); + expect(placeholder?.isStreaming).toBe(true); + expect(placeholder?.isSubagentItem).toBe(true); + expect(placeholder?.parentTaskToolId).toBe(parentToolId); + expect(placeholder?.subagentSessionId).toBe(subagentSessionId); - // First text chunk creates the item directly. + // First text chunk updates the placeholder. routeTextChunkToToolCard(context, parentSessionId, parentToolId, { sessionId: subagentSessionId, turnId: subagentTurnId, diff --git a/src/web-ui/src/flow_chat/services/flow-chat-manager/SubagentModule.ts b/src/web-ui/src/flow_chat/services/flow-chat-manager/SubagentModule.ts index 8a45ba63b..ff9b35595 100644 --- a/src/web-ui/src/flow_chat/services/flow-chat-manager/SubagentModule.ts +++ b/src/web-ui/src/flow_chat/services/flow-chat-manager/SubagentModule.ts @@ -29,20 +29,63 @@ function findParentTurnId(parentSession: { dialogTurns: Array<{ id: string; mode } /** - * Subagent text items are now created on the first real text chunk only. - * The TaskDetailPanel loading state handles the waiting period. + * Create a placeholder text item when a subagent model round starts. + * This gives users immediate visual feedback that the subagent is running, + * rather than waiting for the first text chunk. */ export function routeModelRoundStartedToToolCard( _context: FlowChatContext, - _parentSessionId: string, - _parentToolId: string, - _data: { + parentSessionId: string, + parentToolId: string, + data: { sessionId: string; turnId: string; roundId: string; } ): void { - // No-op: placeholder items are no longer created. + const store = FlowChatStore.getInstance(); + const parentSession = store.getState().sessions.get(parentSessionId); + + if (!parentSession) { + log.debug('Parent session not found (Subagent ModelRoundStarted)', { parentSessionId }); + return; + } + + const parentTurnId = findParentTurnId(parentSession, parentToolId); + if (!parentTurnId) { + log.debug('Parent tool DialogTurn not found (ModelRoundStarted)', { parentSessionId, parentToolId }); + return; + } + + const itemId = getSubagentTextItemId(parentToolId, data.sessionId, data.roundId); + + // Check if placeholder already exists (e.g., from a previous call) + const parentTurn = parentSession.dialogTurns.find(turn => turn.id === parentTurnId); + if (parentTurn) { + for (const round of parentTurn.modelRounds) { + if (round.items.some(item => item.id === itemId)) { + return; + } + } + } + + const parentTool = store.findToolItem(parentSessionId, parentTurnId, parentToolId); + const parentTimestamp = parentTool?.timestamp || Date.now(); + + const placeholderItem: FlowTextItem = { + id: itemId, + type: 'text', + content: '\u200B', + timestamp: parentTimestamp + 1, + isStreaming: true, + status: 'streaming', + isMarkdown: true, + isSubagentItem: true, + parentTaskToolId: parentToolId, + subagentSessionId: data.sessionId, + }; + + store.insertModelRoundItemAfterTool(parentSessionId, parentTurnId, parentToolId, placeholderItem); } /** @@ -93,7 +136,9 @@ export function routeTextChunkToToolCard( } if (existingItem) { - const content = existingItem.content + textContent; + // Strip the zero-width-space placeholder when the first real text arrives. + const baseContent = existingItem.content === '\u200B' ? '' : existingItem.content; + const content = baseContent + textContent; if (isThinkingEnd) { store.updateModelRoundItem(parentSessionId, parentTurnId, itemId, { diff --git a/src/web-ui/src/flow_chat/tool-cards/GitToolDisplay.scss b/src/web-ui/src/flow_chat/tool-cards/GitToolDisplay.scss index 7db412fa3..777dcdb63 100644 --- a/src/web-ui/src/flow_chat/tool-cards/GitToolDisplay.scss +++ b/src/web-ui/src/flow_chat/tool-cards/GitToolDisplay.scss @@ -4,7 +4,6 @@ */ @use './CompactToolCard.scss'; -@use '../../component-library/styles/_extended-mixins' as mixins; /* ========== Git card specific styles ========== */ @@ -32,7 +31,16 @@ /* ========== Fix scrollbar visibility on card hover ========== */ .compact-tool-card.git-tool-display:hover { .git-result-output { - @include mixins.visible-scrollbar; + &::-webkit-scrollbar-thumb { + background: var(--scrollbar-thumb, rgba(255, 255, 255, 0.12)); + } + + &::-webkit-scrollbar-thumb:hover { + background: var(--scrollbar-thumb-hover, rgba(255, 255, 255, 0.22)); + } + + /* Firefox */ + scrollbar-color: var(--scrollbar-thumb, rgba(255, 255, 255, 0.12)) transparent; } } diff --git a/src/web-ui/src/locales/en-US/scenes/agents.json b/src/web-ui/src/locales/en-US/scenes/agents.json index df960fc58..198066d60 100644 --- a/src/web-ui/src/locales/en-US/scenes/agents.json +++ b/src/web-ui/src/locales/en-US/scenes/agents.json @@ -282,10 +282,11 @@ "judge": { "funName": "Review Arbiter", "role": "Review Quality Inspector", - "description": "A calm final arbiter that checks other reviewers for false positives, risky advice, and evidence quality before reporting.", + "description": "An independent third-party arbiter that validates reviewer reports for logical consistency and evidence quality. It spot-checks specific code locations only when a claim needs verification, rather than re-reviewing the codebase from scratch.", "responsibilities": [ - "Validate, merge, downgrade, or reject reviewer findings before they reach the final report.", - "Filter out false positives and directionally-wrong optimization advice.", + "Validate, merge, downgrade, or reject reviewer findings based on logical consistency and evidence quality.", + "Filter out false positives and directionally-wrong optimization advice by examining reviewer reasoning.", + "Spot-check specific code locations only when a reviewer's claim needs verification.", "Ensure every surviving issue has an actionable fix or follow-up plan." ] } diff --git a/src/web-ui/src/locales/zh-CN/scenes/agents.json b/src/web-ui/src/locales/zh-CN/scenes/agents.json index 3b26368c0..cd0558ada 100644 --- a/src/web-ui/src/locales/zh-CN/scenes/agents.json +++ b/src/web-ui/src/locales/zh-CN/scenes/agents.json @@ -282,10 +282,11 @@ "judge": { "funName": "审核仲裁员", "role": "审核质检员", - "description": "负责给其他审核员做二次质检,过滤误报、危险建议和证据不足结论的最终裁判。", + "description": "独立的第三方仲裁者,从逻辑一致性和证据质量角度校验审核员报告。仅在特定声明需要验证时才抽查代码位置,而非从头重新审查代码库。", "responsibilities": [ - "在结论进入最终报告前,对各审核员的发现进行验证、合并、降级或驳回。", - "过滤误报以及方向错误的优化建议。", + "基于逻辑一致性和证据质量,对各审核员的发现进行验证、合并、降级或驳回。", + "通过审查审核员的推理过程,过滤误报以及方向错误的优化建议。", + "仅在审核员的声明需要验证时,对特定代码位置进行抽查。", "确保每条保留的问题都带有明确修复方案或后续计划。" ] } diff --git a/src/web-ui/src/locales/zh-TW/scenes/agents.json b/src/web-ui/src/locales/zh-TW/scenes/agents.json index e7b903470..05c4fb00a 100644 --- a/src/web-ui/src/locales/zh-TW/scenes/agents.json +++ b/src/web-ui/src/locales/zh-TW/scenes/agents.json @@ -282,10 +282,11 @@ "judge": { "funName": "審核仲裁員", "role": "審核質檢員", - "description": "負責給其他審核員做二次質檢,過濾誤報、危險建議和證據不足結論的最終裁判。", + "description": "獨立的第三方仲裁者,從邏輯一致性和證據品質角度校驗審核員報告。僅在特定聲明需要驗證時才抽查程式碼位置,而非從頭重新審查程式碼庫。", "responsibilities": [ - "在結論進入最終報告前,對各審核員的發現進行驗證、合併、降級或駁回。", - "過濾誤報以及方向錯誤的優化建議。", + "基於邏輯一致性和證據品質,對各審核員的發現進行驗證、合併、降級或駁回。", + "透過審查審核員的推理過程,過濾誤報以及方向錯誤的優化建議。", + "僅在審核員的聲明需要驗證時,對特定程式碼位置進行抽查。", "確保每條保留的問題都帶有明確修復方案或後續計劃。" ] } diff --git a/src/web-ui/src/shared/services/reviewTeamService.ts b/src/web-ui/src/shared/services/reviewTeamService.ts index 2def987b8..2ad02eea6 100644 --- a/src/web-ui/src/shared/services/reviewTeamService.ts +++ b/src/web-ui/src/shared/services/reviewTeamService.ts @@ -256,10 +256,11 @@ export const DEFAULT_REVIEW_TEAM_CORE_ROLES: ReviewTeamCoreRoleDefinition[] = [ funName: 'Review Arbiter', roleName: 'Review Quality Inspector', description: - 'A calm final arbiter that checks other reviewers for false positives, risky advice, and evidence quality before reporting.', + 'An independent third-party arbiter that validates reviewer reports for logical consistency and evidence quality. It spot-checks specific code locations only when a claim needs verification, rather than re-reviewing the codebase from scratch.', responsibilities: [ - 'Validate, merge, downgrade, or reject reviewer findings before they reach the final report.', - 'Filter out false positives and directionally-wrong optimization advice.', + 'Validate, merge, downgrade, or reject reviewer findings based on logical consistency and evidence quality.', + 'Filter out false positives and directionally-wrong optimization advice by examining reviewer reasoning.', + 'Spot-check specific code locations only when a reviewer claim needs verification.', 'Ensure every surviving issue has an actionable fix or follow-up plan.', ], accentColor: '#7c3aed', @@ -963,7 +964,7 @@ export function buildReviewTeamPromptBlock( '- When file splitting is active, each same-role instance must only review its assigned file group. Label instances in the Task description (e.g. "Security review [group 1/3]").', '- Do not run ReviewFixer during the review pass.', '- Wait for explicit user approval before starting any remediation.', - '- The Review Quality Inspector must validate findings from every reviewer (including all same-role instances) before the final report.', + '- The Review Quality Inspector acts as a third-party arbiter: it primarily examines reviewer reports for logical consistency and evidence quality, and only uses code inspection tools for targeted spot-checks when a specific claim needs verification.', 'Review strategy rules:', `- Team strategy: ${team.strategyLevel}. ${formatStrategyImpact(team.strategyLevel)}`, commonStrategyRules,