diff --git a/src/crates/core/src/agentic/coordination/coordinator.rs b/src/crates/core/src/agentic/coordination/coordinator.rs index c8e079a24..02cd82c85 100644 --- a/src/crates/core/src/agentic/coordination/coordinator.rs +++ b/src/crates/core/src/agentic/coordination/coordinator.rs @@ -1256,6 +1256,8 @@ Update the persona files and delete BOOTSTRAP.md as soon as bootstrap is complet duration_ms: outcome.duration_ms, subagent_parent_info: None, partial_recovery_reason: None, + success: Some(true), + finish_reason: Some("complete".to_string()), }) .await; diff --git a/src/crates/core/src/agentic/execution/execution_engine.rs b/src/crates/core/src/agentic/execution/execution_engine.rs index 7688e6cff..b6ee7b65f 100644 --- a/src/crates/core/src/agentic/execution/execution_engine.rs +++ b/src/crates/core/src/agentic/execution/execution_engine.rs @@ -1747,6 +1747,17 @@ impl ExecutionEngine { context.dialog_turn_id, completed_rounds, total_tools ); + // Determine finish reason + let finish_reason = if loop_detected { + FinishReason::LoopDetected + } else if completed_rounds >= self.config.max_rounds { + FinishReason::MaxRounds + } else { + FinishReason::Complete + }; + + let success = !loop_detected && completed_rounds < self.config.max_rounds; + // Emit dialog turn completed event debug!("Preparing to send DialogTurnCompleted event"); @@ -1761,6 +1772,8 @@ impl ExecutionEngine { duration_ms, subagent_parent_info: event_subagent_parent_info, partial_recovery_reason: last_partial_recovery_reason, + success: Some(success), + finish_reason: Some(finish_reason.to_string()), }, None, ) @@ -1797,17 +1810,6 @@ impl ExecutionEngine { ); } - // Determine finish reason - let finish_reason = if loop_detected { - FinishReason::LoopDetected - } else if completed_rounds >= self.config.max_rounds { - FinishReason::MaxRounds - } else { - FinishReason::Complete - }; - - let success = !loop_detected && completed_rounds < self.config.max_rounds; - if loop_detected { warn!( "Dialog turn stopped due to loop detection: turn={}, rounds={}", diff --git a/src/crates/core/src/agentic/execution/types.rs b/src/crates/core/src/agentic/execution/types.rs index 928e48eae..39baed80e 100644 --- a/src/crates/core/src/agentic/execution/types.rs +++ b/src/crates/core/src/agentic/execution/types.rs @@ -82,6 +82,19 @@ pub enum FinishReason { LoopDetected, } +impl std::fmt::Display for FinishReason { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + FinishReason::Complete => write!(f, "complete"), + FinishReason::ToolCalls => write!(f, "tool_calls"), + FinishReason::MaxRounds => write!(f, "max_rounds"), + FinishReason::Cancelled => write!(f, "cancelled"), + FinishReason::Error => write!(f, "error"), + FinishReason::LoopDetected => write!(f, "loop_detected"), + } + } +} + /// Execution result #[derive(Debug, Clone)] pub struct ExecutionResult { diff --git a/src/crates/core/src/agentic/tools/implementations/git_tool.rs b/src/crates/core/src/agentic/tools/implementations/git_tool.rs index 96c8ab310..586ab474e 100644 --- a/src/crates/core/src/agentic/tools/implementations/git_tool.rs +++ b/src/crates/core/src/agentic/tools/implementations/git_tool.rs @@ -324,10 +324,19 @@ impl GitTool { .await .map_err(|e| BitFunError::tool(format!("Git diff failed: {}", e)))?; + // When there are no differences, git diff returns exit code 0 with an + // empty stdout. Return a friendly message so the model (and user) see + // a clear "no changes" indication instead of a bare empty string. + let stdout = if diff_output.trim().is_empty() { + "No differences found.".to_string() + } else { + diff_output + }; + Ok(json!({ "success": true, "exit_code": 0, - "stdout": diff_output, + "stdout": stdout, "stderr": "" })) } @@ -777,6 +786,12 @@ This tool provides a safe and convenient way to execute Git commands. It support {"operation": "switch", "args": "main"} ``` +## Important: `args` Field Rules + +- The `operation` field already specifies the Git subcommand (e.g. `diff`, `log`, `add`). +- The `args` field must contain **only additional arguments** for that subcommand. +- **Do NOT include the subcommand name itself in `args`.** For example, use `{"operation": "diff", "args": "HEAD~2..HEAD --stat"}` — NOT `{"operation": "diff", "args": "diff HEAD~2..HEAD --stat"}`. + ## Safety Notes - This tool validates operations to ensure only allowed Git commands are executed @@ -1022,6 +1037,20 @@ When creating commits, use this format for the commit message: let args = input.get("args").and_then(|v| v.as_str()); + // Tolerance: strip a leading operation name from args if the model + // mistakenly includes it (e.g. "diff HEAD~2..HEAD --stat" when + // operation is already "diff"). This prevents commands like + // "git diff diff HEAD~2..HEAD --stat". + let args = args.map(|a| { + let trimmed = a.trim(); + let prefix = format!("{} ", operation); + if trimmed.starts_with(&prefix) { + &trimmed[prefix.len()..] + } else { + trimmed + } + }); + let working_directory = input.get("working_directory").and_then(|v| v.as_str()); // Get repository path diff --git a/src/crates/events/src/agentic.rs b/src/crates/events/src/agentic.rs index 485cf1fde..94745785a 100644 --- a/src/crates/events/src/agentic.rs +++ b/src/crates/events/src/agentic.rs @@ -142,6 +142,13 @@ pub enum AgenticEvent { /// recovery (stream aborted mid-way). Contains a human-readable reason. #[serde(skip_serializing_if = "Option::is_none")] partial_recovery_reason: Option, + /// Whether the turn completed successfully (false for loop_detected or + /// max_rounds). + #[serde(skip_serializing_if = "Option::is_none")] + success: Option, + /// Why the turn finished: "complete", "loop_detected", or "max_rounds". + #[serde(skip_serializing_if = "Option::is_none")] + finish_reason: Option, }, DialogTurnCancelled { diff --git a/src/crates/transport/src/adapters/cli.rs b/src/crates/transport/src/adapters/cli.rs index 6fd53bd2b..b9e0b71e3 100644 --- a/src/crates/transport/src/adapters/cli.rs +++ b/src/crates/transport/src/adapters/cli.rs @@ -30,6 +30,8 @@ pub enum CliEvent { DialogTurnCompleted { session_id: String, turn_id: String, + success: Option, + finish_reason: Option, }, /// Generic event (for LSP, file watch, etc.) Generic { @@ -93,10 +95,14 @@ impl TransportAdapter for CliTransportAdapter { AgenticEvent::DialogTurnCompleted { session_id, turn_id, + success, + finish_reason, .. } => CliEvent::DialogTurnCompleted { session_id, turn_id, + success, + finish_reason, }, _ => return Ok(()), }; diff --git a/src/crates/transport/src/adapters/tauri.rs b/src/crates/transport/src/adapters/tauri.rs index f88bc9ab8..c5214484e 100644 --- a/src/crates/transport/src/adapters/tauri.rs +++ b/src/crates/transport/src/adapters/tauri.rs @@ -195,6 +195,8 @@ impl TransportAdapter for TauriTransportAdapter { turn_id, subagent_parent_info, partial_recovery_reason, + success, + finish_reason, .. } => { self.app_handle.emit( @@ -204,6 +206,8 @@ impl TransportAdapter for TauriTransportAdapter { "turnId": turn_id, "subagentParentInfo": subagent_parent_info, "partialRecoveryReason": partial_recovery_reason, + "success": success, + "finishReason": finish_reason, }), )?; } diff --git a/src/crates/transport/src/adapters/websocket.rs b/src/crates/transport/src/adapters/websocket.rs index 8b0916e78..a18dbcc5c 100644 --- a/src/crates/transport/src/adapters/websocket.rs +++ b/src/crates/transport/src/adapters/websocket.rs @@ -165,6 +165,8 @@ impl TransportAdapter for WebSocketTransportAdapter { turn_id, subagent_parent_info, partial_recovery_reason, + success, + finish_reason, .. } => { json!({ @@ -173,6 +175,8 @@ impl TransportAdapter for WebSocketTransportAdapter { "turnId": turn_id, "subagentParentInfo": subagent_parent_info, "partialRecoveryReason": partial_recovery_reason, + "success": success, + "finishReason": finish_reason, }) } _ => return Ok(()), diff --git a/src/web-ui/src/flow_chat/components/FlowTextBlock.scss b/src/web-ui/src/flow_chat/components/FlowTextBlock.scss index 2821fb7cf..850003733 100644 --- a/src/web-ui/src/flow_chat/components/FlowTextBlock.scss +++ b/src/web-ui/src/flow_chat/components/FlowTextBlock.scss @@ -262,7 +262,7 @@ gap: 0.5rem; min-height: 1.5rem; color: var(--color-text-muted); - font-size: var(--flowchat-font-size-sm); + font-size: var(--flowchat-font-size-base); line-height: 1.4; opacity: 0.86; } diff --git a/src/web-ui/src/flow_chat/components/FlowTextBlock.tsx b/src/web-ui/src/flow_chat/components/FlowTextBlock.tsx index 6785825e7..5c6385c03 100644 --- a/src/web-ui/src/flow_chat/components/FlowTextBlock.tsx +++ b/src/web-ui/src/flow_chat/components/FlowTextBlock.tsx @@ -8,11 +8,11 @@ import React, { useState, useEffect, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { MarkdownRenderer } from '@/component-library'; -import { TaskRunningIndicator } from '@/component-library'; +import { DotMatrixLoader } from '@/component-library'; import type { FlowTextItem } from '../types/flow-chat'; import { useFlowChatContext } from './modern/FlowChatContext'; import { useTypewriter } from '../hooks/useTypewriter'; -import { DEFAULT_MODEL_RESPONSE_STATUS_MESSAGE_KEY } from '../services/flow-chat-manager/RuntimeStatusModule'; +import { processingHintsZh, processingHintsEn } from '../constants/processingHints'; import './FlowTextBlock.scss'; // Idle timeout (ms) after content stops growing. @@ -34,7 +34,7 @@ export const FlowTextBlock = React.memo(({ replayStreamingOnMount = true }) => { const { onFileViewRequest, onTabOpen, onOpenVisualization } = useFlowChatContext(); - const { t } = useTranslation('flow-chat'); + const { i18n } = useTranslation(); // Normalize content to a string. const content = typeof textItem.content === 'string' @@ -79,17 +79,20 @@ export const FlowTextBlock = React.memo(({ } }, [textItem.status, textItem.isStreaming]); - const isActivelyStreaming = textItem.isStreaming && + const isActivelyStreaming = textItem.isStreaming && (textItem.status === 'streaming' || textItem.status === 'running') && isContentGrowing; const hasContent = content.length > 0; if (textItem.runtimeStatus) { - const messageKey = textItem.runtimeStatus.messageKey || DEFAULT_MODEL_RESPONSE_STATUS_MESSAGE_KEY; + const hints = i18n.language.startsWith('zh') ? processingHintsZh : processingHintsEn; + const hintIndex = Math.abs(textItem.id.split('').reduce((acc, ch) => acc + ch.charCodeAt(0), 0)) % hints.length; + const hint = hints[hintIndex]; + return (
- - {t(messageKey)} + + {hint}
); } diff --git a/src/web-ui/src/flow_chat/components/modern/ExploreRegion.scss b/src/web-ui/src/flow_chat/components/modern/ExploreRegion.scss index 63ae8d7c8..e192c203e 100644 --- a/src/web-ui/src/flow_chat/components/modern/ExploreRegion.scss +++ b/src/web-ui/src/flow_chat/components/modern/ExploreRegion.scss @@ -22,6 +22,8 @@ // should not push the first summary line downward when the round is // re-wrapped from `model-round` into `explore-group`. padding: 0; + // Hide scrollbar track by default; only show when content actually overflows. + overflow: hidden; // In the plain model-round layout, the first/last flow item margins can // collapse with the parent. Once wrapped by explore-group, that collapse no @@ -113,6 +115,8 @@ // Align nested tool cards with the summary text, not the group chevron. padding: 0 0 0 20px; box-sizing: border-box; + // Hide scrollbar track by default; only show when content actually overflows. + overflow: hidden; &::-webkit-scrollbar { width: 4px; @@ -201,11 +205,14 @@ } } -// Limit height and enable scroll during streaming. +// Limit height and enable scroll only when content actually overflows. +.explore-region--has-scroll .explore-region__content { + overflow-y: auto; +} + .explore-region--expanded.explore-region--streaming { .explore-region__content { max-height: 400px; - overflow-y: auto; } } diff --git a/src/web-ui/src/flow_chat/components/modern/VirtualItemRenderer.scss b/src/web-ui/src/flow_chat/components/modern/VirtualItemRenderer.scss index ccf19ae0f..32cb0a18a 100644 --- a/src/web-ui/src/flow_chat/components/modern/VirtualItemRenderer.scss +++ b/src/web-ui/src/flow_chat/components/modern/VirtualItemRenderer.scss @@ -37,4 +37,36 @@ } } +.turn-stopped-banner { + display: flex; + align-items: flex-start; + gap: 10px; + margin: 8px 0; + padding: 10px 14px; + border-radius: 8px; + background: color-mix(in srgb, var(--color-warning, #f59e0b) 10%, transparent); + border: 1px solid color-mix(in srgb, var(--color-warning, #f59e0b) 25%, transparent); + &__icon { + flex-shrink: 0; + color: var(--color-warning, #f59e0b); + margin-top: 1px; + } + + &__content { + min-width: 0; + } + + &__title { + font-weight: 600; + font-size: 13px; + color: var(--color-text-primary, #e8e8e8); + margin-bottom: 2px; + } + + &__suggestion { + font-size: 12px; + color: var(--color-text-muted, #858585); + line-height: 1.5; + } +} diff --git a/src/web-ui/src/flow_chat/components/modern/VirtualItemRenderer.tsx b/src/web-ui/src/flow_chat/components/modern/VirtualItemRenderer.tsx index f1b0af895..6cbed40d3 100644 --- a/src/web-ui/src/flow_chat/components/modern/VirtualItemRenderer.tsx +++ b/src/web-ui/src/flow_chat/components/modern/VirtualItemRenderer.tsx @@ -4,7 +4,8 @@ */ import React from 'react'; -import { Loader2 } from 'lucide-react'; +import { Loader2, AlertTriangle } from 'lucide-react'; +import { useTranslation } from 'react-i18next'; import type { VirtualItem } from '../../store/modernFlowChatStore'; import { UserMessageItem } from './UserMessageItem'; import { ModelRoundItem } from './ModelRoundItem'; @@ -21,6 +22,7 @@ interface VirtualItemRendererProps { export const VirtualItemRenderer = React.memo( ({ item, index }) => { const { searchMatchIndices, searchCurrentMatchVirtualIndex } = useFlowChatContext(); + const { t } = useTranslation('errors'); const isSearchMatch = searchMatchIndices != null && searchMatchIndices.size > 0 ? searchMatchIndices.has(index) : false; @@ -63,6 +65,30 @@ export const VirtualItemRenderer = React.memo( /> ); + + case 'turn-stopped': { + const titleKey = item.finishReason === 'loop_detected' + ? 'ai.loopDetected' + : item.finishReason === 'max_rounds' + ? 'ai.maxRounds' + : 'ai.loopDetected'; + const suggestionKey = item.finishReason === 'loop_detected' + ? 'ai.loopDetectedSuggestion' + : item.finishReason === 'max_rounds' + ? 'ai.maxRoundsSuggestion' + : 'ai.loopDetectedSuggestion'; + return ( +
+
+ +
+
+
{t(titleKey)}
+
{t(suggestionKey)}
+
+
+ ); + } default: return
; diff --git a/src/web-ui/src/flow_chat/services/flow-chat-manager/EventHandlerModule.ts b/src/web-ui/src/flow_chat/services/flow-chat-manager/EventHandlerModule.ts index db2224e00..5a7ffcf69 100644 --- a/src/web-ui/src/flow_chat/services/flow-chat-manager/EventHandlerModule.ts +++ b/src/web-ui/src/flow_chat/services/flow-chat-manager/EventHandlerModule.ts @@ -1527,6 +1527,8 @@ function handleDialogTurnComplete( const subagentParentInfo = normalizeSubagentParentInfo(event); // Partial recovery reason from backend (stream was interrupted mid-way) const partialRecoveryReason = event?.partialRecoveryReason ?? event?.partial_recovery_reason; + const success = event?.success; + const finishReason = event?.finishReason ?? event?.finish_reason; if (subagentParentInfo) { if (sessionId) { @@ -1561,6 +1563,8 @@ function handleDialogTurnComplete( return { ...turn, status: 'finishing' as const, + success: success ?? undefined, + finishReason: finishReason ?? undefined, }; }); diff --git a/src/web-ui/src/flow_chat/store/modernFlowChatStore.ts b/src/web-ui/src/flow_chat/store/modernFlowChatStore.ts index d3dbb86c4..430c6f746 100644 --- a/src/web-ui/src/flow_chat/store/modernFlowChatStore.ts +++ b/src/web-ui/src/flow_chat/store/modernFlowChatStore.ts @@ -41,7 +41,8 @@ export type VirtualItem = | { type: 'user-message'; data: DialogTurn['userMessage']; turnId: string } | { type: 'model-round'; data: ModelRound; turnId: string; isLastRound: boolean } | { type: 'explore-group'; data: ExploreGroupData; turnId: string } - | { type: 'image-analyzing'; turnId: string }; + | { type: 'image-analyzing'; turnId: string } + | { type: 'turn-stopped'; turnId: string; finishReason: string }; /** * Currently visible turn information @@ -270,6 +271,15 @@ export function sessionToVirtualItems(session: Session | null): VirtualItem[] { roundIndex++; } } + + // If the turn was stopped abnormally, add a turn-stopped indicator + if (turn.finishReason && turn.finishReason !== 'complete' && turn.status === 'completed') { + items.push({ + type: 'turn-stopped', + turnId: turn.id, + finishReason: turn.finishReason, + }); + } }); cachedVirtualItems = items; 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 7cb41d899..f01b39041 100644 --- a/src/web-ui/src/flow_chat/tool-cards/CompactToolCard.scss +++ b/src/web-ui/src/flow_chat/tool-cards/CompactToolCard.scss @@ -26,6 +26,7 @@ gap: 4px; } + /* Spacing-only overrides — icon size & text styling are inherited from shared variables */ .tool-card-icon-slot { align-self: center; width: 20px; @@ -37,29 +38,8 @@ height: 20px; } - .tool-card-icon.tool-identifier-icon.tool-card-icon-main svg { - width: 14px; - height: 14px; - } - - .compact-card-action { - font-size: var(--flowchat-font-size-xs); - line-height: 1.45; - font-weight: normal; - } - - .compact-card-content { - font-size: var(--flowchat-font-size-xs); - line-height: 1.45; - } - - .compact-card-extra { - font-size: var(--flowchat-font-size-xs); - line-height: 1.45; - } - .compact-card-right-status-icon { - min-height: calc(var(--flowchat-font-size-xs) * 1.45); + min-height: 24px; justify-content: flex-start; } @@ -106,19 +86,24 @@ /* ========== Loading state: A (border via text fade) + E (text fade) ========== */ -/* Hide left spinner/cube */ +/* Hide left spinner/cube when it is the ONLY child (legacy: status icon used as left icon) */ .compact-tool-card-wrapper--loading-shimmer .tool-card-icon-marks:has(.cube-loading), .compact-tool-card-wrapper--loading-shimmer .tool-card-icon-marks:has(svg.animate-spin) { display: none !important; } -/* E — status icon + action + content breathe */ -.compact-tool-card-wrapper--loading-shimmer .tool-card-icon-marks, +/* E — action + content breathe; keep tool-identifier-icon steady */ .compact-tool-card-wrapper--loading-shimmer .compact-card-action, .compact-tool-card-wrapper--loading-shimmer .compact-card-content { animation: tool-card-text-fade 1.6s ease-in-out infinite; } +/* Legacy compact cards that still use status icon as left icon (no .tool-identifier-icon child): + fade the icon-marks too for the original shimmer effect. */ +.compact-tool-card-wrapper--loading-shimmer .tool-card-icon-marks:not(:has(.tool-identifier-icon)) { + animation: tool-card-text-fade 1.6s ease-in-out infinite; +} + /* ========== Card body - transparent, no border ========== */ .compact-tool-card { background: transparent !important; @@ -190,9 +175,9 @@ * spans (line-height 1.65 × 14px = 23.1px), not by icon wrappers. */ .compact-card-action { - font-size: var(--flowchat-font-size-base); - line-height: 1.65; - font-weight: 500; + font-size: var(--tool-card-action-font-size); + line-height: var(--tool-card-action-line-height); + font-weight: var(--tool-card-action-font-weight); color: var(--color-text-muted); transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); flex-shrink: 0; @@ -200,8 +185,8 @@ .compact-card-content { flex: 1; - font-size: var(--flowchat-font-size-base); - line-height: 1.65; + font-size: var(--tool-card-action-font-size); + line-height: var(--tool-card-action-line-height); color: var(--color-text-muted); min-width: 0; transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); @@ -217,8 +202,8 @@ } .compact-card-extra { - font-size: var(--flowchat-font-size-base); - line-height: 1.65; + font-size: var(--tool-card-action-font-size); + line-height: var(--tool-card-action-line-height); color: var(--color-text-muted); display: flex; align-items: center; diff --git a/src/web-ui/src/flow_chat/tool-cards/GlobSearchDisplay.tsx b/src/web-ui/src/flow_chat/tool-cards/GlobSearchDisplay.tsx index 5db749258..c7632c8da 100644 --- a/src/web-ui/src/flow_chat/tool-cards/GlobSearchDisplay.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/GlobSearchDisplay.tsx @@ -3,7 +3,7 @@ */ import React, { useState, useMemo, useCallback } from 'react'; -import { Loader2, Clock, File, Folder, Check } from 'lucide-react'; +import { FolderSearch, Loader2, Clock, File, Folder, Check } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import type { ToolCardProps } from '../types/flow-chat'; import { CompactToolCard, CompactToolCardHeader } from './CompactToolCard'; @@ -25,11 +25,11 @@ export const GlobSearchDisplay: React.FC = ({ switch (status) { case 'running': case 'streaming': - return ; + return ; case 'completed': - return ; + return ; default: - return ; + return ; } }; @@ -195,8 +195,10 @@ export const GlobSearchDisplay: React.FC = ({ clickable={hasDetails} header={ } content={renderContent()} + rightStatusIcon={getStatusIcon()} + rightStatusIconWithDivider /> } expandedContent={hasDetails ? renderExpandedContent() : undefined} diff --git a/src/web-ui/src/flow_chat/tool-cards/GrepSearchDisplay.tsx b/src/web-ui/src/flow_chat/tool-cards/GrepSearchDisplay.tsx index f13815de0..a807ca005 100644 --- a/src/web-ui/src/flow_chat/tool-cards/GrepSearchDisplay.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/GrepSearchDisplay.tsx @@ -3,7 +3,7 @@ */ import React, { useState, useMemo, useCallback } from 'react'; -import { Loader2, Clock, Check } from 'lucide-react'; +import { Search, Loader2, Clock, Check } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import type { ToolCardProps } from '../types/flow-chat'; import { CompactToolCard, CompactToolCardHeader } from './CompactToolCard'; @@ -25,11 +25,11 @@ export const GrepSearchDisplay: React.FC = ({ switch (status) { case 'running': case 'streaming': - return ; + return ; case 'completed': - return ; + return ; default: - return ; + return ; } }; @@ -151,8 +151,10 @@ export const GrepSearchDisplay: React.FC = ({ clickable={hasDetails} header={ } content={renderContent()} + rightStatusIcon={getStatusIcon()} + rightStatusIconWithDivider /> } expandedContent={hasDetails ? renderExpandedContent() : undefined} diff --git a/src/web-ui/src/flow_chat/tool-cards/LSDisplay.tsx b/src/web-ui/src/flow_chat/tool-cards/LSDisplay.tsx index 4cc1cf821..3863a0b3d 100644 --- a/src/web-ui/src/flow_chat/tool-cards/LSDisplay.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/LSDisplay.tsx @@ -3,7 +3,7 @@ */ import React, { useState, useMemo, useCallback } from 'react'; -import { Loader2, Clock, File, Folder, Check } from 'lucide-react'; +import { FolderOpen, Loader2, Clock, File, Folder, Check } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import type { ToolCardProps } from '../types/flow-chat'; import { CompactToolCard, CompactToolCardHeader } from './CompactToolCard'; @@ -32,11 +32,11 @@ export const LSDisplay: React.FC = ({ switch (status) { case 'running': case 'streaming': - return ; + return ; case 'completed': - return ; + return ; default: - return ; + return ; } }; @@ -192,8 +192,10 @@ export const LSDisplay: React.FC = ({ clickable={hasDetails} header={ } content={renderContent()} + rightStatusIcon={getStatusIcon()} + rightStatusIconWithDivider /> } expandedContent={hasDetails ? renderExpandedContent() : undefined} diff --git a/src/web-ui/src/flow_chat/tool-cards/ReadFileDisplay.tsx b/src/web-ui/src/flow_chat/tool-cards/ReadFileDisplay.tsx index 32d03d0ec..bcc5ddd8d 100644 --- a/src/web-ui/src/flow_chat/tool-cards/ReadFileDisplay.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/ReadFileDisplay.tsx @@ -3,7 +3,7 @@ */ import React, { useMemo } from 'react'; -import { Loader2, Clock, Check } from 'lucide-react'; +import { FileText, Loader2, Clock, Check } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import type { ToolCardProps } from '../types/flow-chat'; import { CompactToolCard, CompactToolCardHeader } from './CompactToolCard'; @@ -19,12 +19,12 @@ export const ReadFileDisplay: React.FC = React.memo(({ switch (status) { case 'running': case 'streaming': - return ; + return ; case 'completed': - return ; + return ; case 'pending': default: - return ; + return ; } }; @@ -134,8 +134,10 @@ export const ReadFileDisplay: React.FC = React.memo(({ clickable={canOpenFile} header={ } content={renderContent()} + rightStatusIcon={getStatusIcon()} + rightStatusIconWithDivider /> } /> diff --git a/src/web-ui/src/flow_chat/tool-cards/WebSearchCard.tsx b/src/web-ui/src/flow_chat/tool-cards/WebSearchCard.tsx index c11cb2cc0..06262607c 100644 --- a/src/web-ui/src/flow_chat/tool-cards/WebSearchCard.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/WebSearchCard.tsx @@ -3,7 +3,7 @@ */ import React, { useState, useMemo, useCallback } from 'react'; -import { Loader2, Link, Clock, Check } from 'lucide-react'; +import { Globe, Loader2, Link, Clock, Check } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import type { ToolCardProps } from '../types/flow-chat'; import { systemAPI } from '../../infrastructure/api'; @@ -32,11 +32,11 @@ export const WebSearchCard: React.FC = ({ case 'running': case 'streaming': case 'preparing': - return ; + return ; case 'completed': - return ; + return ; default: - return ; + return ; } }; @@ -150,8 +150,10 @@ export const WebSearchCard: React.FC = ({ clickable={Boolean(status === 'completed' && hasResults)} header={ } content={renderContent()} + rightStatusIcon={getStatusIcon()} + rightStatusIconWithDivider /> } expandedContent={hasResults ? renderExpandedContent() : undefined} diff --git a/src/web-ui/src/flow_chat/tool-cards/_tool-card-common.scss b/src/web-ui/src/flow_chat/tool-cards/_tool-card-common.scss index de1086277..17d596243 100644 --- a/src/web-ui/src/flow_chat/tool-cards/_tool-card-common.scss +++ b/src/web-ui/src/flow_chat/tool-cards/_tool-card-common.scss @@ -71,6 +71,12 @@ --tool-card-header-pad-right: 10px; --tool-card-header-icon-rail: 24px; --tool-card-header-icon-slot: calc(var(--tool-card-header-icon-rail) + 10px); + + /* Shared icon & text sizing — dense-command variants only override spacing, not these */ + --tool-card-icon-size: 16px; + --tool-card-action-font-size: var(--flowchat-font-size-base); + --tool-card-action-line-height: 1.65; + --tool-card-action-font-weight: 500; } /* ========== Shared icon-slot (left rail) ========== */ @@ -140,8 +146,8 @@ transition: opacity 0.18s ease; svg { - width: 16px; - height: 16px; + width: var(--tool-card-icon-size); + height: var(--tool-card-icon-size); } } diff --git a/src/web-ui/src/flow_chat/types/flow-chat.ts b/src/web-ui/src/flow_chat/types/flow-chat.ts index 1958ddbcc..48adf94b3 100644 --- a/src/web-ui/src/flow_chat/types/flow-chat.ts +++ b/src/web-ui/src/flow_chat/types/flow-chat.ts @@ -155,6 +155,10 @@ export interface DialogTurn { tokenUsage?: TokenUsage; todos?: TodoItem[]; backendTurnIndex?: number; + /** Whether the turn completed successfully (false for loop_detected / max_rounds). */ + success?: boolean; + /** Why the turn finished: "complete", "loop_detected", or "max_rounds". */ + finishReason?: string; } export interface FlowChatState { diff --git a/src/web-ui/src/locales/en-US/errors.json b/src/web-ui/src/locales/en-US/errors.json index 09bbc245f..5a4e43b59 100644 --- a/src/web-ui/src/locales/en-US/errors.json +++ b/src/web-ui/src/locales/en-US/errors.json @@ -63,6 +63,8 @@ "networkErrorSuggestion": "The connection was unstable or the model server closed it prematurely. Check your network and retry", "loopDetected": "Repetitive operation detected", "loopDetectedSuggestion": "AI repeated the same operation and was stopped automatically. Try adjusting your instructions or completing remaining steps manually", + "maxRounds": "Maximum rounds reached", + "maxRoundsSuggestion": "The dialog has reached the maximum number of rounds and was stopped automatically. Start a new dialog to continue", "rateLimit": "Model rate limit exceeded", "rateLimitSuggestion": "Please retry later, or switch to a different model in settings", "authError": "API authentication failed", diff --git a/src/web-ui/src/locales/zh-CN/errors.json b/src/web-ui/src/locales/zh-CN/errors.json index f52e1e8c8..cd79f7694 100644 --- a/src/web-ui/src/locales/zh-CN/errors.json +++ b/src/web-ui/src/locales/zh-CN/errors.json @@ -63,6 +63,8 @@ "networkErrorSuggestion": "网络连接不稳定或模型服务端提前关闭了连接,请检查网络后重试", "loopDetected": "检测到重复操作", "loopDetectedSuggestion": "AI 连续执行了相同的操作,已自动停止。请尝试调整指令或手动完成剩余步骤", + "maxRounds": "对话达到上限", + "maxRoundsSuggestion": "对话轮次已达到系统上限,已自动终止。如需继续,请发起新的对话", "rateLimit": "模型请求频率超限", "rateLimitSuggestion": "请稍后重试,或在模型设置中切换到其他模型", "authError": "API 认证失败", diff --git a/src/web-ui/src/locales/zh-TW/errors.json b/src/web-ui/src/locales/zh-TW/errors.json index a5ba39fc8..07301b569 100644 --- a/src/web-ui/src/locales/zh-TW/errors.json +++ b/src/web-ui/src/locales/zh-TW/errors.json @@ -63,6 +63,8 @@ "networkErrorSuggestion": "網絡連接不穩定或模型服務端提前關閉了連接,請檢查網絡後重試", "loopDetected": "檢測到重複操作", "loopDetectedSuggestion": "AI 連續執行了相同的操作,已自動停止。請嘗試調整指令或手動完成剩餘步驟", + "maxRounds": "對話達到上限", + "maxRoundsSuggestion": "對話輪次已達到系統上限,已自動終止。如需繼續,請發起新的對話", "rateLimit": "模型請求頻率超限", "rateLimitSuggestion": "請稍後重試,或在模型設置中切換到其他模型", "authError": "API 認證失敗",