Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion src/apps/desktop/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -814,6 +814,7 @@ async fn init_agentic_system() -> anyhow::Result<(
Arc<bitfun_core::service::token_usage::TokenUsageService>,
)> {
use bitfun_core::agentic::*;
use bitfun_core::service::config::get_global_config_service;

let ai_client_factory = AIClientFactory::get_global().await?;

Expand Down Expand Up @@ -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::<bitfun_core::service::config::types::GlobalConfig>(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(
Expand Down
11 changes: 10 additions & 1 deletion src/apps/server/src/bootstrap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,21 @@ pub async fn initialize(workspace: Option<String>) -> anyhow::Result<Arc<ServerA
event_queue.clone(),
tool_pipeline.clone(),
));

// Get execution config from global settings
let global_config: bitfun_core::service::config::types::GlobalConfig =
config_service.get_config(None).await.map_err(|e| anyhow::anyhow!("Failed to load config: {}", e))?;
let exec_config = execution::ExecutionEngineConfig {
max_rounds: global_config.ai.max_rounds,
..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(
Expand Down
2 changes: 1 addition & 1 deletion src/crates/core/src/agentic/execution/execution_engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ pub struct ExecutionEngineConfig {
impl Default for ExecutionEngineConfig {
fn default() -> Self {
Self {
max_rounds: 50,
max_rounds: crate::service::config::types::DEFAULT_MAX_ROUNDS,
max_consecutive_same_tool: 3,
}
}
Expand Down
20 changes: 19 additions & 1 deletion src/crates/core/src/agentic/system.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ pub struct AgenticSystem {
pub async fn init_agentic_system() -> Result<AgenticSystem> {
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()));
Expand Down Expand Up @@ -56,12 +59,27 @@ pub async fn init_agentic_system() -> Result<AgenticSystem> {
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::<GlobalConfig>(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(
Expand Down
3 changes: 3 additions & 0 deletions src/crates/core/src/agentic/tools/implementations/git_tool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
};
Expand Down
11 changes: 11 additions & 0 deletions src/crates/core/src/service/config/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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(),
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
7 changes: 7 additions & 0 deletions src/web-ui/src/flow_chat/components/FlowTextBlock.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ export interface FlowChatContextValue {
searchQuery?: string;
searchMatchIndices?: ReadonlySet<number>;
searchCurrentMatchVirtualIndex?: number;

// Continue dialog turn when max_rounds is reached
onContinueTurn?: (sessionId: string, turnId: string) => void;
}

export const FlowChatContext = createContext<FlowChatContextValue>({});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export const ModernFlowChatContainer: React.FC<ModernFlowChatContainerProps> = (
onCollapseGroup: handleCollapseGroup,
} = useExploreGroupState(virtualItems);
const { handleToolConfirm, handleToolReject } = useFlowChatToolActions();

const { handleFileViewRequest } = useFlowChatFileActions({
workspacePath,
onFileViewRequest,
Expand All @@ -87,6 +88,16 @@ export const ModernFlowChatContainer: React.FC<ModernFlowChatContainerProps> = (
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,
Expand Down Expand Up @@ -114,6 +125,7 @@ export const ModernFlowChatContainer: React.FC<ModernFlowChatContainerProps> = (
searchQuery,
searchMatchIndices,
searchCurrentMatchVirtualIndex,
onContinueTurn: handleContinueTurn,
}), [
handleFileViewRequest,
onTabOpen,
Expand All @@ -132,6 +144,7 @@ export const ModernFlowChatContainer: React.FC<ModernFlowChatContainerProps> = (
searchQuery,
searchMatchIndices,
searchCurrentMatchVirtualIndex,
handleContinueTurn,
]);

const turnSummaries = useMemo<FlowChatHeaderTurnSummary[]>(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@

&__content {
min-width: 0;
flex: 1;
}

&__title {
Expand All @@ -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);
}
}
}
23 changes: 20 additions & 3 deletions src/web-ui/src/flow_chat/components/modern/VirtualItemRenderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -21,14 +21,20 @@ interface VirtualItemRendererProps {

export const VirtualItemRenderer = React.memo<VirtualItemRendererProps>(
({ 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)
: false;
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':
Expand Down Expand Up @@ -77,6 +83,7 @@ export const VirtualItemRenderer = React.memo<VirtualItemRendererProps>(
: item.finishReason === 'max_rounds'
? 'ai.maxRoundsSuggestion'
: 'ai.loopDetectedSuggestion';
const canContinue = item.finishReason === 'max_rounds' && onContinueTurn && sessionId;
return (
<div className="turn-stopped-banner">
<div className="turn-stopped-banner__icon">
Expand All @@ -86,6 +93,16 @@ export const VirtualItemRenderer = React.memo<VirtualItemRendererProps>(
<div className="turn-stopped-banner__title">{t(titleKey)}</div>
<div className="turn-stopped-banner__suggestion">{t(suggestionKey)}</div>
</div>
{canContinue && (
<button
className="turn-stopped-banner__continue-btn"
onClick={handleContinueTurn}
title={t('ai.continueTurn')}
>
<Play size={14} />
{t('ai.continueTurn')}
</button>
)}
</div>
);
}
Expand Down
15 changes: 15 additions & 0 deletions src/web-ui/src/flow_chat/services/FlowChatManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> {
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<void> {
return saveAllInProgressTurns(this.context);
}
Expand Down
7 changes: 7 additions & 0 deletions src/web-ui/src/flow_chat/tool-cards/ModelThinkingDisplay.scss
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading
Loading