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
29 changes: 24 additions & 5 deletions src/apps/desktop/src/api/snapshot_service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
use bitfun_core::infrastructure::try_get_path_manager_arc;
use bitfun_core::service::remote_ssh::workspace_state::is_remote_path;
use bitfun_core::service::snapshot::{
ensure_snapshot_manager_for_workspace, get_snapshot_manager_for_workspace,
initialize_snapshot_manager_for_workspace, OperationType, SnapshotConfig, SnapshotManager,
OperationType, SnapshotConfig, SnapshotManager, ensure_snapshot_manager_for_workspace,
get_snapshot_manager_for_workspace, initialize_snapshot_manager_for_workspace,
};
use log::{info, warn};
use serde::{Deserialize, Serialize};
Expand Down Expand Up @@ -285,7 +285,7 @@ pub async fn record_file_change(
return Err(format!(
"Unknown operation type: {}",
request.operation_type
))
));
}
};

Expand Down Expand Up @@ -424,7 +424,10 @@ pub async fn rollback_to_turn(
deleted_turns_count = count;
}
Err(e) => {
warn!("Failed to delete conversation turns: session_id={}, turn_index={}, error={}", request.session_id, request.turn_index, e);
warn!(
"Failed to delete conversation turns: session_id={}, turn_index={}, error={}",
request.session_id, request.turn_index, e
);
}
}
}
Expand Down Expand Up @@ -546,6 +549,10 @@ pub async fn reject_file(

#[tauri::command]
pub async fn get_session_files(request: GetSessionFilesRequest) -> Result<Vec<String>, String> {
if is_remote_path(&request.workspace_path).await {
return Ok(vec![]);
}

let manager = ensure_snapshot_manager_ready(&request.workspace_path).await?;

let files = manager
Expand Down Expand Up @@ -580,7 +587,10 @@ pub async fn get_session_turns(
}
Ok(None) => {}
Err(e) => {
warn!("Failed to load conversation metadata: session_id={}, error={}, falling back to snapshot", request.session_id, e);
warn!(
"Failed to load conversation metadata: session_id={}, error={}, falling back to snapshot",
request.session_id, e
);
}
}
}
Expand Down Expand Up @@ -840,6 +850,15 @@ pub async fn reject_operation(
pub async fn get_session_stats(
request: GetSessionStatsRequest,
) -> Result<serde_json::Value, String> {
if is_remote_path(&request.workspace_path).await {
return Ok(serde_json::json!({
"session_id": request.session_id,
"total_files": 0,
"total_turns": 0,
"total_changes": 0
}));
}

let manager = ensure_snapshot_manager_ready(&request.workspace_path).await?;

let stats = manager
Expand Down
66 changes: 59 additions & 7 deletions src/apps/desktop/src/api/tool_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@ use std::collections::HashMap;
use std::path::PathBuf;

use bitfun_core::agentic::{
WorkspaceBinding,
tools::framework::ToolUseContext,
tools::{get_all_tools, get_readonly_tools},
WorkspaceBinding,
workspace::{local_workspace_services, remote_workspace_services},
};
use bitfun_core::service::remote_ssh::workspace_state::{
get_remote_workspace_manager, lookup_remote_connection, resolve_workspace_session_identity,
};
use bitfun_core::util::elapsed_ms_u64;

Expand Down Expand Up @@ -82,23 +86,71 @@ pub struct ToolConfirmationResponse {
pub message: String,
}

fn build_tool_context(workspace_path: Option<&str>) -> ToolUseContext {
async fn build_tool_context(workspace_path: Option<&str>) -> ToolUseContext {
let normalized_workspace_path = workspace_path
.map(str::trim)
.filter(|path| !path.is_empty());

let workspace = match normalized_workspace_path {
Some(path) => match resolve_workspace_session_identity(path, None, None).await {
Some(identity) => {
if let Some(connection_id) = identity.remote_connection_id.as_deref() {
let connection_name = lookup_remote_connection(path)
.await
.map(|entry| entry.connection_name)
.unwrap_or_else(|| connection_id.to_string());
Some(WorkspaceBinding::new_remote(
None,
PathBuf::from(path),
connection_id.to_string(),
connection_name,
identity,
))
} else {
Some(WorkspaceBinding::new(None, PathBuf::from(path)))
}
}
None => Some(WorkspaceBinding::new(None, PathBuf::from(path))),
},
None => None,
};

let workspace_services = match workspace.as_ref() {
Some(binding) if binding.is_remote() => {
let connection_id = binding.connection_id().map(str::to_string);
match (connection_id, get_remote_workspace_manager()) {
(Some(connection_id), Some(manager)) => {
match (
manager.get_file_service().await,
manager.get_ssh_manager().await,
) {
(Some(file_service), Some(ssh_manager)) => Some(remote_workspace_services(
connection_id,
file_service,
ssh_manager,
binding.root_path_string(),
)),
_ => None,
}
}
_ => None,
}
}
Some(binding) => Some(local_workspace_services(binding.root_path_string())),
None => None,
};

ToolUseContext {
tool_call_id: None,
agent_type: None,
session_id: None,
dialog_turn_id: None,
workspace: normalized_workspace_path
.map(|path| WorkspaceBinding::new(None, PathBuf::from(path))),
workspace,
custom_data: HashMap::new(),
computer_use_host: None,
cancellation_token: None,
runtime_tool_restrictions: Default::default(),
workspace_services: None,
workspace_services,
}
}

Expand Down Expand Up @@ -229,7 +281,7 @@ pub async fn validate_tool_input(
request.workspace_path.as_deref(),
)?;

let context = build_tool_context(request.workspace_path.as_deref());
let context = build_tool_context(request.workspace_path.as_deref()).await;

let validation_result = tool.validate_input(&request.input, Some(&context)).await;

Expand Down Expand Up @@ -260,7 +312,7 @@ pub async fn execute_tool(request: ToolExecutionRequest) -> Result<ToolExecution
request.workspace_path.as_deref(),
)?;

let context = build_tool_context(request.workspace_path.as_deref());
let context = build_tool_context(request.workspace_path.as_deref()).await;

let validation_result = tool.validate_input(&request.input, Some(&context)).await;
if !validation_result.result {
Expand Down
16 changes: 9 additions & 7 deletions src/crates/core/src/agentic/execution/execution_engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,21 @@
use super::round_executor::RoundExecutor;
use super::types::{ExecutionContext, ExecutionResult, RoundContext};
use crate::agentic::agents::{
get_agent_registry, PromptBuilder, PromptBuilderContext, RemoteExecutionHints,
PromptBuilder, PromptBuilderContext, RemoteExecutionHints, get_agent_registry,
};
use crate::agentic::core::{
render_system_reminder, Message, MessageContent, MessageHelper, MessageRole,
MessageSemanticKind, RequestReasoningTokenPolicy, Session,
Message, MessageContent, MessageHelper, MessageRole, MessageSemanticKind,
RequestReasoningTokenPolicy, Session, render_system_reminder,
};
use crate::agentic::events::{AgenticEvent, EventPriority, EventQueue};
use crate::agentic::execution::types::FinishReason;
use crate::agentic::image_analysis::{
build_multimodal_message_with_images, process_image_contexts_for_provider, ImageContextData,
ImageLimits,
ImageContextData, ImageLimits, build_multimodal_message_with_images,
process_image_contexts_for_provider,
};
use crate::agentic::session::{CompressionTailPolicy, ContextCompressor, SessionManager};
use crate::agentic::tools::{
get_all_registered_tools, SubagentParentInfo, ToolRuntimeRestrictions,
SubagentParentInfo, ToolRuntimeRestrictions, get_all_registered_tools,
};
use crate::agentic::util::build_remote_workspace_layout_preview;
use crate::agentic::{WorkspaceBackend, WorkspaceBinding};
Expand Down Expand Up @@ -1272,6 +1272,7 @@ impl ExecutionEngine {
self.get_available_tools_and_definitions(
&allowed_tools,
context.workspace.as_ref(),
context.workspace_services.as_ref(),
&agent_type,
primary_supports_image_understanding,
)
Expand Down Expand Up @@ -1870,6 +1871,7 @@ impl ExecutionEngine {
&self,
mode_allowed_tools: &[String],
workspace: Option<&crate::agentic::WorkspaceBinding>,
workspace_services: Option<&crate::agentic::workspace::WorkspaceServices>,
agent_type: &str,
primary_supports_image_understanding: bool,
) -> (Vec<String>, Option<Vec<ToolDefinition>>) {
Expand All @@ -1893,7 +1895,7 @@ impl ExecutionEngine {
computer_use_host: None,
cancellation_token: None,
runtime_tool_restrictions: ToolRuntimeRestrictions::default(),
workspace_services: None,
workspace_services: workspace_services.cloned(),
};
for tool in &all_tools {
if !tool.is_enabled().await {
Expand Down
37 changes: 33 additions & 4 deletions src/crates/core/src/agentic/execution/stream_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -430,8 +430,7 @@ impl StreamProcessor {
for tool_call in tool_calls {
trace!(
"Cleaning up tool: {} ({})",
tool_call.tool_name,
tool_call.tool_id
tool_call.tool_name, tool_call.tool_id
);

let tool_event = if is_user_cancellation {
Expand Down Expand Up @@ -583,8 +582,10 @@ impl StreamProcessor {

/// Handle text chunk
async fn handle_text_chunk(&self, ctx: &mut StreamContext, text: String) {
ctx.has_effective_output = true;
ctx.mark_first_visible_output();
if !text.trim().is_empty() {
ctx.has_effective_output = true;
ctx.mark_first_visible_output();
}
ctx.full_text.push_str(&text);
ctx.text_chunks_count += 1;

Expand Down Expand Up @@ -976,6 +977,34 @@ mod tests {
assert_eq!(result.usage.as_ref().map(|u| u.total_token_count), Some(7));
}

#[tokio::test]
async fn whitespace_only_text_is_not_effective_output() {
let processor = build_processor();
let stream = iter(vec![Ok(UnifiedResponse {
text: Some("\n\n ".to_string()),
..Default::default()
})])
.boxed();

let result = processor
.process_stream(
stream,
None,
None,
"session_1".to_string(),
"turn_1".to_string(),
"round_1".to_string(),
None,
&CancellationToken::new(),
)
.await
.expect("stream result");

assert_eq!(result.full_text, "\n\n ");
assert!(!result.has_effective_output);
assert_eq!(result.first_visible_output_ms, None);
}

#[tokio::test]
async fn finalizes_tool_after_same_chunk_finish_reason() {
let processor = build_processor();
Expand Down
Loading
Loading