From c27177e35e86b2c9778798b5ec400ca067358815 Mon Sep 17 00:00:00 2001 From: limit_yan Date: Wed, 29 Apr 2026 16:20:17 +0800 Subject: [PATCH 1/4] feat(web-search): support displaying summary when no search results - Add summary rendering in WebSearchCard when results are empty - Add i18n keys for summary availability in en-US, zh-CN, zh-TW - Refactor expandable logic to include summary-only state --- .../flow_chat/tool-cards/WebSearchCard.tsx | 87 ++++++++++++------- src/web-ui/src/locales/en-US/flow-chat.json | 3 +- src/web-ui/src/locales/zh-CN/flow-chat.json | 3 +- src/web-ui/src/locales/zh-TW/flow-chat.json | 3 +- 4 files changed, 64 insertions(+), 32 deletions(-) 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 06262607c..761cf5294 100644 --- a/src/web-ui/src/flow_chat/tool-cards/WebSearchCard.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/WebSearchCard.tsx @@ -87,20 +87,27 @@ export const WebSearchCard: React.FC = ({ const searchTerm = getSearchTerm(); const hasResultData = toolResult?.result !== undefined && toolResult?.result !== null; const hasResults = searchResults && searchResults.results.length > 0; + const hasSummary = !hasResults && searchResults && searchResults.summary; + const isExpandable = status === 'completed' && (hasResults || hasSummary); const handleClick = useCallback(() => { - if (status === 'completed' && hasResults) { + if (isExpandable) { applyExpandedState(isExpanded, !isExpanded, setIsExpanded, { onExpand, }); } - }, [applyExpandedState, hasResults, isExpanded, onExpand, status]); + }, [applyExpandedState, isExpandable, isExpanded, onExpand]); const renderContent = () => { if (status === 'completed') { - const resultsText = hasResultData && searchResults - ? ` (${t('toolCards.webSearch.resultsCount', { count: searchResults.total || 0 })})` - : ''; + let resultsText = ''; + if (hasResultData && searchResults) { + if (hasResults) { + resultsText = ` (${t('toolCards.webSearch.resultsCount', { count: searchResults.total })})`; + } else if (hasSummary) { + resultsText = ` (${t('toolCards.webSearch.summaryAvailable')})`; + } + } return `${t('toolCards.webSearch.searchTitle', { term: searchTerm })}${resultsText}`; } if (status === 'running' || status === 'streaming' || status === 'preparing') { @@ -112,30 +119,52 @@ export const WebSearchCard: React.FC = ({ return t('toolCards.webSearch.searchTitle', { term: searchTerm }); }; - const renderExpandedContent = () => ( -
- {searchResults?.results.map((result: any, index: number) => ( -
- -
{ - e.stopPropagation(); - handleOpenLink(result.url); - }} - > - - {result.title || t('toolCards.webSearch.noTitle')} + const renderExpandedContent = () => { + if (hasResults) { + return ( +
+ {searchResults?.results.map((result: any, index: number) => ( +
+ +
{ + e.stopPropagation(); + handleOpenLink(result.url); + }} + > + + {result.title || t('toolCards.webSearch.noTitle')} +
+
+ {result.snippet && ( +
{result.snippet}
+ )} +
{result.url}
- - {result.snippet && ( -
{result.snippet}
- )} -
{result.url}
+ ))}
- ))} -
- ); + ); + } + + if (hasSummary) { + return ( +
+
+            {searchResults!.summary}
+          
+
+ ); + } + + return undefined; + }; if (status === 'error') { return null; @@ -147,7 +176,7 @@ export const WebSearchCard: React.FC = ({ status={status} isExpanded={isExpanded} onClick={handleClick} - clickable={Boolean(status === 'completed' && hasResults)} + clickable={isExpandable} header={ } @@ -156,7 +185,7 @@ export const WebSearchCard: React.FC = ({ rightStatusIconWithDivider /> } - expandedContent={hasResults ? renderExpandedContent() : undefined} + expandedContent={isExpandable ? renderExpandedContent() : undefined} />
); 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 b7c715ac4..3b961ef19 100644 --- a/src/web-ui/src/locales/en-US/flow-chat.json +++ b/src/web-ui/src/locales/en-US/flow-chat.json @@ -797,7 +797,8 @@ "searching": "Searching \"{{term}}\"...", "preparingSearch": "Preparing to search \"{{term}}\"", "clickToOpenLink": "Click to open link", - "noTitle": "No title" + "noTitle": "No title", + "summaryAvailable": "summary available" }, "todoWrite": { "tasksCount": "{{count}} tasks", 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 102d3e9c4..28f02a6af 100644 --- a/src/web-ui/src/locales/zh-CN/flow-chat.json +++ b/src/web-ui/src/locales/zh-CN/flow-chat.json @@ -797,7 +797,8 @@ "searching": "正在搜索 \"{{term}}\"...", "preparingSearch": "准备搜索 \"{{term}}\"", "clickToOpenLink": "点击打开链接", - "noTitle": "无标题" + "noTitle": "无标题", + "summaryAvailable": "有搜索摘要" }, "todoWrite": { "tasksCount": "{{count}} 项任务", diff --git a/src/web-ui/src/locales/zh-TW/flow-chat.json b/src/web-ui/src/locales/zh-TW/flow-chat.json index 08baf7a58..85344b74e 100644 --- a/src/web-ui/src/locales/zh-TW/flow-chat.json +++ b/src/web-ui/src/locales/zh-TW/flow-chat.json @@ -752,7 +752,8 @@ "searching": "正在搜索 \"{{term}}\"...", "preparingSearch": "準備搜索 \"{{term}}\"", "clickToOpenLink": "點擊打開鏈接", - "noTitle": "無標題" + "noTitle": "無標題", + "summaryAvailable": "有搜索摘要" }, "todoWrite": { "tasksCount": "{{count}} 項任務", From 9022f96030b3a85d0c326ecd14233cc19ca03d1c Mon Sep 17 00:00:00 2001 From: limit_yan Date: Wed, 29 Apr 2026 16:33:59 +0800 Subject: [PATCH 2/4] fix(web-ui): inline-code color overrides link color in light theme --- .../component-library/components/Markdown/Markdown.scss | 7 +++++++ src/web-ui/src/flow_chat/components/FlowTextBlock.scss | 7 +++++++ .../src/flow_chat/tool-cards/ModelThinkingDisplay.scss | 7 +++++++ 3 files changed, 21 insertions(+) diff --git a/src/web-ui/src/component-library/components/Markdown/Markdown.scss b/src/web-ui/src/component-library/components/Markdown/Markdown.scss index ec9839f6c..ab8c34132 100644 --- a/src/web-ui/src/component-library/components/Markdown/Markdown.scss +++ b/src/web-ui/src/component-library/components/Markdown/Markdown.scss @@ -640,6 +640,13 @@ text-decoration-thickness: 1.5px; } +.markdown-renderer a .inline-code, +.file-link .inline-code, +.visualization-link .inline-code, +.tab-link .inline-code { + color: inherit; +} + .markdown-link-path-tooltip { display: inline-block; max-width: min(520px, 80vw); diff --git a/src/web-ui/src/flow_chat/components/FlowTextBlock.scss b/src/web-ui/src/flow_chat/components/FlowTextBlock.scss index 850003733..714524345 100644 --- a/src/web-ui/src/flow_chat/components/FlowTextBlock.scss +++ b/src/web-ui/src/flow_chat/components/FlowTextBlock.scss @@ -158,6 +158,13 @@ text-decoration-thickness: 1.5px; } + .markdown-renderer a .inline-code, + .markdown-renderer .file-link .inline-code, + .markdown-renderer .visualization-link .inline-code, + .markdown-renderer .tab-link .inline-code { + color: inherit !important; + } + /* Markdown.scss sets 0.875rem !important on fenced blocks — override with flow-chat scale. */ .markdown-renderer .code-block-wrapper pre[class*='language-'], .markdown-renderer .code-block-wrapper pre code, diff --git a/src/web-ui/src/flow_chat/tool-cards/ModelThinkingDisplay.scss b/src/web-ui/src/flow_chat/tool-cards/ModelThinkingDisplay.scss index c90f85100..762ae802a 100644 --- a/src/web-ui/src/flow_chat/tool-cards/ModelThinkingDisplay.scss +++ b/src/web-ui/src/flow_chat/tool-cards/ModelThinkingDisplay.scss @@ -324,6 +324,13 @@ color: var(--flowchat-link-hover-color, #93c5fd); } + a .inline-code, + .file-link .inline-code, + .visualization-link .inline-code, + .tab-link .inline-code { + color: inherit !important; + } + ul, ol { padding-left: 1.25rem; From 00bd80ad4547dd67668787d0618872cd6a061898 Mon Sep 17 00:00:00 2001 From: limit_yan Date: Wed, 29 Apr 2026 17:49:06 +0800 Subject: [PATCH 3/4] feat: increase max dialog rounds to 200 and add soft limit continuation - Increase default max_rounds from 50 to 200 in ExecutionEngineConfig - Add max_rounds field to AIConfig (user-configurable) - Read max_rounds from config in all execution entry points (desktop, server, core system) - Frontend: add 'Continue' button when max_rounds is reached - Add continueDialogTurn to FlowChatManager - Update i18n strings for en-US, zh-CN, zh-TW Generated with BitFun Co-Authored-By: BitFun --- src/apps/desktop/src/lib.rs | 18 +++++++++- src/apps/server/src/bootstrap.rs | 11 ++++++- .../src/agentic/execution/execution_engine.rs | 2 +- src/crates/core/src/agentic/system.rs | 20 ++++++++++- src/crates/core/src/service/config/types.rs | 11 +++++++ .../components/modern/FlowChatContext.tsx | 3 ++ .../modern/ModernFlowChatContainer.tsx | 13 ++++++++ .../modern/VirtualItemRenderer.scss | 27 +++++++++++++++ .../components/modern/VirtualItemRenderer.tsx | 23 +++++++++++-- .../src/flow_chat/services/FlowChatManager.ts | 15 +++++++++ .../config/components/SessionConfig.tsx | 33 +++++++++++++++++++ .../src/infrastructure/config/types/index.ts | 1 + src/web-ui/src/locales/en-US/errors.json | 3 +- .../en-US/settings/session-config.json | 4 +++ src/web-ui/src/locales/zh-CN/errors.json | 3 +- .../zh-CN/settings/session-config.json | 4 +++ src/web-ui/src/locales/zh-TW/errors.json | 3 +- .../zh-TW/settings/session-config.json | 4 +++ src/web-ui/src/shared/utils/logger.ts | 2 +- 19 files changed, 189 insertions(+), 11 deletions(-) diff --git a/src/apps/desktop/src/lib.rs b/src/apps/desktop/src/lib.rs index cb0cd81d1..7f9cbb923 100644 --- a/src/apps/desktop/src/lib.rs +++ b/src/apps/desktop/src/lib.rs @@ -814,6 +814,7 @@ async fn init_agentic_system() -> anyhow::Result<( Arc, )> { use bitfun_core::agentic::*; + use bitfun_core::service::config::get_global_config_service; let ai_client_factory = AIClientFactory::get_global().await?; @@ -851,12 +852,27 @@ async fn init_agentic_system() -> anyhow::Result<( event_queue.clone(), tool_pipeline.clone(), )); + + // Get execution config from global settings + let exec_config = match get_global_config_service().await { + Ok(config_service) => { + match config_service.get_config::(None).await { + Ok(global_config) => execution::ExecutionEngineConfig { + max_rounds: global_config.ai.max_rounds, + ..Default::default() + }, + Err(_) => Default::default(), + } + }, + Err(_) => Default::default(), + }; + let execution_engine = Arc::new(execution::ExecutionEngine::new( round_executor, event_queue.clone(), session_manager.clone(), context_compressor, - Default::default(), + exec_config, )); let coordinator = Arc::new(coordination::ConversationCoordinator::new( diff --git a/src/apps/server/src/bootstrap.rs b/src/apps/server/src/bootstrap.rs index c3263189a..4c5a101bf 100644 --- a/src/apps/server/src/bootstrap.rs +++ b/src/apps/server/src/bootstrap.rs @@ -83,12 +83,21 @@ pub async fn initialize(workspace: Option) -> anyhow::Result Self { Self { - max_rounds: 50, + max_rounds: crate::service::config::types::DEFAULT_MAX_ROUNDS, max_consecutive_same_tool: 3, } } diff --git a/src/crates/core/src/agentic/system.rs b/src/crates/core/src/agentic/system.rs index c12948722..8fcab412f 100644 --- a/src/crates/core/src/agentic/system.rs +++ b/src/crates/core/src/agentic/system.rs @@ -25,6 +25,9 @@ pub struct AgenticSystem { pub async fn init_agentic_system() -> Result { info!("Initializing agentic system"); + use crate::service::config::get_global_config_service; + use crate::service::config::types::GlobalConfig; + let _ai_client_factory = AIClientFactory::get_global().await?; let event_queue = Arc::new(events::EventQueue::new(Default::default())); @@ -56,12 +59,27 @@ pub async fn init_agentic_system() -> Result { event_queue.clone(), tool_pipeline.clone(), )); + + // Get execution config from global settings + let exec_config = match get_global_config_service().await { + Ok(config_service) => { + match config_service.get_config::(None).await { + Ok(global_config) => execution::ExecutionEngineConfig { + max_rounds: global_config.ai.max_rounds, + ..Default::default() + }, + Err(_) => Default::default(), + } + }, + Err(_) => Default::default(), + }; + let execution_engine = Arc::new(execution::ExecutionEngine::new( round_executor, event_queue.clone(), session_manager.clone(), context_compressor, - Default::default(), + exec_config, )); let coordinator = Arc::new(coordination::ConversationCoordinator::new( diff --git a/src/crates/core/src/service/config/types.rs b/src/crates/core/src/service/config/types.rs index 185c50841..59dcad292 100644 --- a/src/crates/core/src/service/config/types.rs +++ b/src/crates/core/src/service/config/types.rs @@ -503,6 +503,10 @@ pub struct AIConfig { /// Allow Computer use (desktop automation) when the desktop host is available (all session modes). #[serde(default)] pub computer_use_enabled: bool, + + /// Maximum number of rounds per dialog turn before soft-pausing. + #[serde(default = "default_max_rounds")] + pub max_rounds: usize, } impl AIConfig { @@ -646,6 +650,12 @@ fn default_subagent_max_concurrency() -> usize { 5 } +pub const DEFAULT_MAX_ROUNDS: usize = 200; + +fn default_max_rounds() -> usize { + DEFAULT_MAX_ROUNDS +} + impl Default for ModeConfig { fn default() -> Self { Self { @@ -1448,6 +1458,7 @@ impl Default for AIConfig { skip_tool_confirmation: true, debug_mode_config: DebugModeConfig::default(), computer_use_enabled: false, + max_rounds: default_max_rounds(), } } } 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 b7a19c8ed..982115352 100644 --- a/src/web-ui/src/flow_chat/components/modern/FlowChatContext.tsx +++ b/src/web-ui/src/flow_chat/components/modern/FlowChatContext.tsx @@ -57,6 +57,9 @@ export interface FlowChatContextValue { searchQuery?: string; searchMatchIndices?: ReadonlySet; searchCurrentMatchVirtualIndex?: number; + + // Continue dialog turn when max_rounds is reached + onContinueTurn?: (sessionId: string, turnId: string) => void; } export const FlowChatContext = createContext({}); 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 c589fb545..55353ede8 100644 --- a/src/web-ui/src/flow_chat/components/modern/ModernFlowChatContainer.tsx +++ b/src/web-ui/src/flow_chat/components/modern/ModernFlowChatContainer.tsx @@ -62,6 +62,7 @@ export const ModernFlowChatContainer: React.FC = ( onCollapseGroup: handleCollapseGroup, } = useExploreGroupState(virtualItems); const { handleToolConfirm, handleToolReject } = useFlowChatToolActions(); + const { handleFileViewRequest } = useFlowChatFileActions({ workspacePath, onFileViewRequest, @@ -87,6 +88,16 @@ export const ModernFlowChatContainer: React.FC = ( virtualListRef, }); + const handleContinueTurn = useCallback(async (sessionId: string, _turnId: string) => { + try { + const manager = FlowChatManager.getInstance(); + await manager.continueDialogTurn(sessionId); + } catch (_e) { + const { notificationService } = await import('@/shared/notification-system'); + notificationService.error('Failed to continue turn. Please try starting a new dialog.', { duration: 3000 }); + } + }, []); + const contextValue: FlowChatContextValue = useMemo(() => ({ onFileViewRequest: handleFileViewRequest, onTabOpen, @@ -114,6 +125,7 @@ export const ModernFlowChatContainer: React.FC = ( searchQuery, searchMatchIndices, searchCurrentMatchVirtualIndex, + onContinueTurn: handleContinueTurn, }), [ handleFileViewRequest, onTabOpen, @@ -132,6 +144,7 @@ export const ModernFlowChatContainer: React.FC = ( searchQuery, searchMatchIndices, searchCurrentMatchVirtualIndex, + handleContinueTurn, ]); const turnSummaries = useMemo(() => { 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 32cb0a18a..de27dacee 100644 --- a/src/web-ui/src/flow_chat/components/modern/VirtualItemRenderer.scss +++ b/src/web-ui/src/flow_chat/components/modern/VirtualItemRenderer.scss @@ -55,6 +55,7 @@ &__content { min-width: 0; + flex: 1; } &__title { @@ -69,4 +70,30 @@ color: var(--color-text-muted, #858585); line-height: 1.5; } + + &__continue-btn { + display: inline-flex; + align-items: center; + gap: 6px; + flex-shrink: 0; + margin-top: 2px; + padding: 5px 12px; + border: 1px solid color-mix(in srgb, var(--accent-primary, #6b8afb) 50%, transparent); + border-radius: 6px; + background: color-mix(in srgb, var(--accent-primary, #6b8afb) 15%, transparent); + color: var(--accent-primary, #6b8afb); + font-size: 12px; + font-weight: 500; + cursor: pointer; + transition: background 0.15s, border-color 0.15s; + + &:hover { + background: color-mix(in srgb, var(--accent-primary, #6b8afb) 25%, transparent); + border-color: var(--accent-primary, #6b8afb); + } + + &:active { + background: color-mix(in srgb, var(--accent-primary, #6b8afb) 35%, transparent); + } + } } 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 6cbed40d3..020db660c 100644 --- a/src/web-ui/src/flow_chat/components/modern/VirtualItemRenderer.tsx +++ b/src/web-ui/src/flow_chat/components/modern/VirtualItemRenderer.tsx @@ -3,8 +3,8 @@ * Renders user messages, model rounds, explore groups, or image-analyzing indicators by type. */ -import React from 'react'; -import { Loader2, AlertTriangle } from 'lucide-react'; +import React, { useCallback } from 'react'; +import { Loader2, AlertTriangle, Play } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import type { VirtualItem } from '../../store/modernFlowChatStore'; import { UserMessageItem } from './UserMessageItem'; @@ -21,7 +21,7 @@ interface VirtualItemRendererProps { export const VirtualItemRenderer = React.memo( ({ item, index }) => { - const { searchMatchIndices, searchCurrentMatchVirtualIndex } = useFlowChatContext(); + const { searchMatchIndices, searchCurrentMatchVirtualIndex, onContinueTurn, sessionId } = useFlowChatContext(); const { t } = useTranslation('errors'); const isSearchMatch = searchMatchIndices != null && searchMatchIndices.size > 0 ? searchMatchIndices.has(index) @@ -29,6 +29,12 @@ export const VirtualItemRenderer = React.memo( const isSearchCurrent = searchCurrentMatchVirtualIndex != null && searchCurrentMatchVirtualIndex >= 0 ? searchCurrentMatchVirtualIndex === index : false; + + const handleContinueTurn = useCallback(() => { + if (onContinueTurn && sessionId && item.turnId) { + onContinueTurn(sessionId, item.turnId); + } + }, [onContinueTurn, sessionId, item.turnId]); const content = (() => { switch (item.type) { case 'user-message': @@ -77,6 +83,7 @@ export const VirtualItemRenderer = React.memo( : item.finishReason === 'max_rounds' ? 'ai.maxRoundsSuggestion' : 'ai.loopDetectedSuggestion'; + const canContinue = item.finishReason === 'max_rounds' && onContinueTurn && sessionId; return (
@@ -86,6 +93,16 @@ export const VirtualItemRenderer = React.memo(
{t(titleKey)}
{t(suggestionKey)}
+ {canContinue && ( + + )}
); } diff --git a/src/web-ui/src/flow_chat/services/FlowChatManager.ts b/src/web-ui/src/flow_chat/services/FlowChatManager.ts index f4434580b..acb834c9f 100644 --- a/src/web-ui/src/flow_chat/services/FlowChatManager.ts +++ b/src/web-ui/src/flow_chat/services/FlowChatManager.ts @@ -382,6 +382,21 @@ export class FlowChatManager { return cancelCurrentTaskModule(this.context); } + /** + * Continue a dialog turn that was paused due to max_rounds being reached. + * Sends a continuation message that instructs the AI to resume where it left off. + */ + async continueDialogTurn(sessionId: string): Promise { + return sendMessageModule( + this.context, + '[Continue execution from where you left off. The previous turn was paused because the round limit was reached. Please continue the task.]', + sessionId, + undefined, + undefined, + undefined + ); + } + public async saveAllInProgressTurns(): Promise { return saveAllInProgressTurns(this.context); } diff --git a/src/web-ui/src/infrastructure/config/components/SessionConfig.tsx b/src/web-ui/src/infrastructure/config/components/SessionConfig.tsx index 2894bd034..dd0833ded 100644 --- a/src/web-ui/src/infrastructure/config/components/SessionConfig.tsx +++ b/src/web-ui/src/infrastructure/config/components/SessionConfig.tsx @@ -65,6 +65,7 @@ const SessionConfig: React.FC = () => { const [skipToolConfirmation, setSkipToolConfirmation] = useState(true); const [executionTimeout, setExecutionTimeout] = useState(''); const [confirmationTimeout, setConfirmationTimeout] = useState(''); + const [maxRounds, setMaxRounds] = useState(200); const [toolExecConfigLoading, setToolExecConfigLoading] = useState(false); const [computerUseEnabled, setComputerUseEnabled] = useState(false); @@ -160,6 +161,7 @@ const SessionConfig: React.FC = () => { confirmTimeout, debugConfigData, computerUseCfg, + maxRoundsValue, ] = await Promise.all([ aiExperienceConfigService.getSettingsAsync(), configManager.getConfig('ai.models') || [], @@ -169,6 +171,7 @@ const SessionConfig: React.FC = () => { configManager.getConfig('ai.tool_confirmation_timeout_secs'), configManager.getConfig('ai.debug_mode_config'), configManager.getConfig('ai.computer_use_enabled'), + configManager.getConfig('ai.max_rounds'), ]); setSettings(loadedSettings); @@ -177,6 +180,7 @@ const SessionConfig: React.FC = () => { setSkipToolConfirmation(skipConfirm ?? true); setExecutionTimeout(execTimeout != null ? String(execTimeout) : ''); setConfirmationTimeout(confirmTimeout != null ? String(confirmTimeout) : ''); + setMaxRounds(maxRoundsValue ?? 200); if (debugConfigData) setDebugConfig(debugConfigData); refreshDesktopStatus(computerUseCfg); @@ -381,6 +385,22 @@ const SessionConfig: React.FC = () => { } }; + const handleMaxRoundsChange = async (value: string) => { + const trimmedValue = value.trim(); + if (trimmedValue !== '') { + const numValue = parseInt(trimmedValue, 10); + if (Number.isNaN(numValue) || numValue < 1) return; + setMaxRounds(numValue); + try { + await configManager.setConfig('ai.max_rounds', numValue); + notificationService.success(t('messages.saveSuccess'), { duration: 2000 }); + } catch (error) { + log.error('Failed to save max_rounds config', error); + notificationService.error(t('messages.saveFailed')); + } + } + }; + // ── Debug config handlers ──────────────────────────────────────────────── const updateDebugConfig = useCallback((updates: Partial) => { @@ -630,6 +650,19 @@ const SessionConfig: React.FC = () => { />
+ +
+ handleMaxRoundsChange(String(val))} + min={1} + max={10000} + step={10} + size="small" + variant="compact" + /> +
+
{/* ── Computer use (desktop) ─────────────────────────────── */} diff --git a/src/web-ui/src/infrastructure/config/types/index.ts b/src/web-ui/src/infrastructure/config/types/index.ts index 62de5641c..a394e13de 100644 --- a/src/web-ui/src/infrastructure/config/types/index.ts +++ b/src/web-ui/src/infrastructure/config/types/index.ts @@ -175,6 +175,7 @@ export interface AIConfig { tool_confirmation_timeout_secs?: number | null; skip_tool_confirmation?: boolean; computer_use_enabled?: boolean; + max_rounds?: number; } export interface StoredModeConfigItem { diff --git a/src/web-ui/src/locales/en-US/errors.json b/src/web-ui/src/locales/en-US/errors.json index 5a4e43b59..85c82736e 100644 --- a/src/web-ui/src/locales/en-US/errors.json +++ b/src/web-ui/src/locales/en-US/errors.json @@ -64,7 +64,8 @@ "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", + "maxRoundsSuggestion": "The dialog has reached the maximum number of rounds and was paused. Click Continue to resume, or start a new dialog", + "continueTurn": "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/en-US/settings/session-config.json b/src/web-ui/src/locales/en-US/settings/session-config.json index 9c9469023..a4d96b7c6 100644 --- a/src/web-ui/src/locales/en-US/settings/session-config.json +++ b/src/web-ui/src/locales/en-US/settings/session-config.json @@ -75,6 +75,10 @@ "loading": { "text": "Loading configuration..." }, + "maxRounds": { + "label": "Maximum dialog rounds", + "description": "Maximum number of AI rounds per dialog turn before pausing. Increase for complex tasks." + }, "messages": { "saveSuccess": "Settings saved", "saveFailed": "Save failed", diff --git a/src/web-ui/src/locales/zh-CN/errors.json b/src/web-ui/src/locales/zh-CN/errors.json index cd79f7694..6dcd7b146 100644 --- a/src/web-ui/src/locales/zh-CN/errors.json +++ b/src/web-ui/src/locales/zh-CN/errors.json @@ -64,7 +64,8 @@ "loopDetected": "检测到重复操作", "loopDetectedSuggestion": "AI 连续执行了相同的操作,已自动停止。请尝试调整指令或手动完成剩余步骤", "maxRounds": "对话达到上限", - "maxRoundsSuggestion": "对话轮次已达到系统上限,已自动终止。如需继续,请发起新的对话", + "maxRoundsSuggestion": "对话轮次已达到系统上限,已暂停执行。点击继续可恢复执行,或发起新的对话", + "continueTurn": "继续执行", "rateLimit": "模型请求频率超限", "rateLimitSuggestion": "请稍后重试,或在模型设置中切换到其他模型", "authError": "API 认证失败", diff --git a/src/web-ui/src/locales/zh-CN/settings/session-config.json b/src/web-ui/src/locales/zh-CN/settings/session-config.json index 9ff7f56fe..824c23884 100644 --- a/src/web-ui/src/locales/zh-CN/settings/session-config.json +++ b/src/web-ui/src/locales/zh-CN/settings/session-config.json @@ -75,6 +75,10 @@ "loading": { "text": "加载配置中..." }, + "maxRounds": { + "label": "最大对话轮次", + "description": "单次对话中 AI 的最大执行轮次,达到后会暂停。复杂任务可适当提高。" + }, "messages": { "saveSuccess": "设置已保存", "saveFailed": "保存失败", diff --git a/src/web-ui/src/locales/zh-TW/errors.json b/src/web-ui/src/locales/zh-TW/errors.json index 07301b569..a745e1e13 100644 --- a/src/web-ui/src/locales/zh-TW/errors.json +++ b/src/web-ui/src/locales/zh-TW/errors.json @@ -64,7 +64,8 @@ "loopDetected": "檢測到重複操作", "loopDetectedSuggestion": "AI 連續執行了相同的操作,已自動停止。請嘗試調整指令或手動完成剩餘步驟", "maxRounds": "對話達到上限", - "maxRoundsSuggestion": "對話輪次已達到系統上限,已自動終止。如需繼續,請發起新的對話", + "maxRoundsSuggestion": "對話輪次已達到系統上限,已暫停執行。點擊繼續可恢復執行,或發起新的對話", + "continueTurn": "繼續執行", "rateLimit": "模型請求頻率超限", "rateLimitSuggestion": "請稍後重試,或在模型設置中切換到其他模型", "authError": "API 認證失敗", diff --git a/src/web-ui/src/locales/zh-TW/settings/session-config.json b/src/web-ui/src/locales/zh-TW/settings/session-config.json index 0ebb9ba6b..1bae8b527 100644 --- a/src/web-ui/src/locales/zh-TW/settings/session-config.json +++ b/src/web-ui/src/locales/zh-TW/settings/session-config.json @@ -75,6 +75,10 @@ "loading": { "text": "加載配置中..." }, + "maxRounds": { + "label": "最大對話輪次", + "description": "單次對話中 AI 的最大執行輪次,達到後會暫停。複雜任務可適當提高。" + }, "messages": { "saveSuccess": "設置已保存", "saveFailed": "保存失敗", diff --git a/src/web-ui/src/shared/utils/logger.ts b/src/web-ui/src/shared/utils/logger.ts index 97853e43e..fbf0bacaf 100644 --- a/src/web-ui/src/shared/utils/logger.ts +++ b/src/web-ui/src/shared/utils/logger.ts @@ -179,7 +179,7 @@ function formatData(data: unknown): string { const regularData: Record = {}; const errors: string[] = []; - for (const key in data as Record) { + for (const key of Object.keys(data as Record)) { const value = (data as Record)[key]; if (value instanceof Error) { errors.push(value.stack || `${value.name}: ${value.message}`); From 234c0af0bde567cdb03ee4c5553afeab53f20946 Mon Sep 17 00:00:00 2001 From: limit_yan Date: Wed, 29 Apr 2026 17:56:16 +0800 Subject: [PATCH 4/4] fix(core): handle git diff args starting with -- separator When model passes args like '-- file1 file2' (no leading space before --), the previous parser failed to find ' -- ' separator and treated all file paths as a single commit ref, causing 'unknown revision' error. Add an explicit branch to handle this case by detecting '-- ' prefix directly. --- src/crates/core/src/agentic/tools/implementations/git_tool.rs | 3 +++ 1 file changed, 3 insertions(+) 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 586ab474e..aa55c33bf 100644 --- a/src/crates/core/src/agentic/tools/implementations/git_tool.rs +++ b/src/crates/core/src/agentic/tools/implementations/git_tool.rs @@ -247,6 +247,9 @@ impl GitTool { let refs = args_str[..sep_pos].trim(); let files = args_str[sep_pos + GIT_DIFF_FILE_SEPARATOR.len()..].trim(); (refs, Some(files)) + } else if let Some(stripped) = args_str.strip_prefix("-- ") { + // Handle "-- file1 file2" (no leading space before --) + ("", Some(stripped.trim())) } else { (args_str.trim(), None) };