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
5 changes: 1 addition & 4 deletions src/apps/cli/src/modes/chat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -794,10 +794,7 @@ impl ChatMode {
);
continue;
}
if let ToolEventData::ConfirmationNeeded {
identity, ..
} = tool_event
{
if let ToolEventData::ConfirmationNeeded { identity, .. } = tool_event {
if self
.runtime
.approval_controller()
Expand Down
4 changes: 1 addition & 3 deletions src/apps/cli/src/modes/exec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -811,9 +811,7 @@ impl ExecMode {
} if event_turn_id == &turn_id => {
use bitfun_events::ToolEventData;
match tool_event {
ToolEventData::ConfirmationNeeded {
identity, ..
} => {
ToolEventData::ConfirmationNeeded { identity, .. } => {
let tool_id = &identity.tool_id;
let tool_name = identity.effective_name();
if self.approval_mode.rejects_confirmation() {
Expand Down
9 changes: 6 additions & 3 deletions src/apps/cli/src/peer_host/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ pub(crate) fn optional_bool(obj: &Value, camel: &str) -> Option<bool> {

pub(crate) fn get_usize(obj: &Value, camel: &str) -> Result<usize, String> {
let snake = camel_to_snake(camel);
let value = field(obj, camel, &snake)
.ok_or_else(|| format!("Missing or invalid '{camel}' field"))?;
let value =
field(obj, camel, &snake).ok_or_else(|| format!("Missing or invalid '{camel}' field"))?;
value
.as_u64()
.map(|n| n as usize)
Expand Down Expand Up @@ -71,6 +71,9 @@ mod tests {
let snake = json!({ "workspace_path": "/b" });
assert_eq!(get_string(&camel, "workspacePath").unwrap(), "/a");
assert_eq!(get_string(&snake, "workspacePath").unwrap(), "/b");
assert_eq!(optional_string(&camel, "workspacePath").as_deref(), Some("/a"));
assert_eq!(
optional_string(&camel, "workspacePath").as_deref(),
Some("/a")
);
}
}
21 changes: 9 additions & 12 deletions src/apps/cli/src/peer_host/commands/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,7 @@ pub(crate) async fn get_config(args: &Value) -> Result<Value, String> {
.await
.map_err(|e| format!("Failed to get config service: {e}"))?;

match config_service
.get_config::<Value>(path.as_deref())
.await
{
match config_service.get_config::<Value>(path.as_deref()).await {
Ok(config) => Ok(config),
Err(e) => {
if skip_retry_on_not_found && is_expected_config_path_not_found(&e, path.as_deref()) {
Expand Down Expand Up @@ -67,7 +64,10 @@ pub(crate) async fn get_configs(args: &Value) -> Result<Value, String> {
if configs.contains_key(&path) {
continue;
}
match config_service.get_config::<Value>(Some(path.as_str())).await {
match config_service
.get_config::<Value>(Some(path.as_str()))
.await
{
Ok(config) => {
configs.insert(path, config);
}
Expand Down Expand Up @@ -103,13 +103,10 @@ pub(crate) async fn set_config(args: &Value) -> Result<Value, String> {
.await
.map_err(|e| format!("Failed to get config service: {e}"))?;

config_service
.set_config(&path, value)
.await
.map_err(|e| {
tracing::error!("Failed to set config: path={path}, error={e}");
format!("Failed to set config: {e}")
})?;
config_service.set_config(&path, value).await.map_err(|e| {
tracing::error!("Failed to set config: path={path}, error={e}");
format!("Failed to set config: {e}")
})?;

Ok(json!("Configuration set successfully"))
}
Expand Down
4 changes: 1 addition & 3 deletions src/apps/cli/src/peer_host/commands/git.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@ pub(crate) async fn git_is_repository(args: &Value) -> Result<Value, String> {
let is_repo = GitService::is_repository(&repository_path)
.await
.map_err(|e| {
tracing::error!(
"Failed to check Git repository: path={repository_path}, error={e}"
);
tracing::error!("Failed to check Git repository: path={repository_path}, error={e}");
format!("Failed to check Git repository: {e}")
})?;
Ok(json!(is_repo))
Expand Down
31 changes: 13 additions & 18 deletions src/apps/cli/src/peer_host/fanout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
use std::collections::HashSet;
use std::sync::OnceLock;

use bitfun_agent_tools::effective_tool_invocation;
use bitfun_core::service::remote_connect::encryption::encrypt_to_base64;
use bitfun_core::service::remote_connect::remote_server::RemoteCommand;
use bitfun_agent_tools::effective_tool_invocation;
use bitfun_events::{project_agentic_frontend_event, AgenticEvent, ToolEventData};
use tokio::sync::{broadcast, mpsc};

Expand Down Expand Up @@ -239,12 +239,9 @@ async fn handle_agentic_event(state: &PeerHostState, event: AgenticEvent) -> Res
if let AgenticEvent::ToolEvent {
session_id,
turn_id,
tool_event:
ToolEventData::Started {
identity,
params,
..
},
tool_event: ToolEventData::Started {
identity, params, ..
},
..
} = &event
{
Expand Down Expand Up @@ -284,9 +281,7 @@ async fn handle_agentic_event(state: &PeerHostState, event: AgenticEvent) -> Res
{
let terminal_task_call = match tool_event {
ToolEventData::Completed {
identity,
result,
..
identity, result, ..
} if identity.effective_name() == "Task" => Some((
identity.tool_id.as_str(),
result
Expand All @@ -296,12 +291,11 @@ async fn handle_agentic_event(state: &PeerHostState, event: AgenticEvent) -> Res
.get("cancelled_background_tasks")
.and_then(serde_json::Value::as_u64),
)),
ToolEventData::Failed {
identity, ..
ToolEventData::Failed { identity, .. } | ToolEventData::Cancelled { identity, .. }
if identity.effective_name() == "Task" =>
{
Some((identity.tool_id.as_str(), None, None))
}
| ToolEventData::Cancelled {
identity, ..
} if identity.effective_name() == "Task" => Some((identity.tool_id.as_str(), None, None)),
_ => None,
};
if let Some((tool_id, background_task_id, cancelled_background_tasks)) = terminal_task_call
Expand All @@ -322,9 +316,10 @@ async fn handle_agentic_event(state: &PeerHostState, event: AgenticEvent) -> Res
..
} = &event
{
state
.turns
.record_confirmation(&PeerTurnKey::new(session_id, turn_id), identity.tool_id.clone())?;
state.turns.record_confirmation(
&PeerTurnKey::new(session_id, turn_id),
identity.tool_id.clone(),
)?;
}

let Some(projected) = project_agentic_frontend_event(event) else {
Expand Down
11 changes: 6 additions & 5 deletions src/apps/desktop/src/computer_use/windows_ax_shortcuts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,19 +45,20 @@ const MENU_BAR_SEARCH_MAX_VISITED: usize = 4_000;
unsafe fn build_shortcuts_cache_request(
automation: &IUIAutomation,
) -> BitFunResult<IUIAutomationCacheRequest> {
let cache_req = automation
.CreateCacheRequest()
// SAFETY: `automation` is a live UI Automation COM interface and every
// property, pattern, and scope identifier below is a documented UIA value.
let cache_req = unsafe { automation.CreateCacheRequest() }
.map_err(|e| BitFunError::tool(format!("UI Automation CreateCacheRequest: {}.", e)))?;
for prop in [
UIA_ControlTypePropertyId,
UIA_NamePropertyId,
UIA_AcceleratorKeyPropertyId,
UIA_IsEnabledPropertyId,
] {
let _ = cache_req.AddProperty(prop);
let _ = unsafe { cache_req.AddProperty(prop) };
}
let _ = cache_req.AddPattern(UIA_TogglePatternId);
let _ = cache_req.SetTreeScope(TreeScope_Subtree);
let _ = unsafe { cache_req.AddPattern(UIA_TogglePatternId) };
let _ = unsafe { cache_req.SetTreeScope(TreeScope_Subtree) };
Ok(cache_req)
}

Expand Down
91 changes: 52 additions & 39 deletions src/apps/desktop/src/computer_use/windows_ax_ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,8 +140,9 @@ fn localized_control_type_string(elem: &IUIAutomationElement) -> String {
unsafe fn build_cache_request(
automation: &IUIAutomation,
) -> BitFunResult<IUIAutomationCacheRequest> {
let cache_req = automation
.CreateCacheRequest()
// SAFETY: `automation` is a live UI Automation COM interface and all ids
// supplied below are documented properties, patterns, scopes, or filters.
let cache_req = unsafe { automation.CreateCacheRequest() }
.map_err(|e| BitFunError::tool(format!("UI Automation CreateCacheRequest: {}.", e)))?;

// Properties to pre-fetch (typed cached accessors read these).
Expand All @@ -154,7 +155,7 @@ unsafe fn build_cache_request(
UIA_IsOffscreenPropertyId,
UIA_BoundingRectanglePropertyId,
] {
let _ = cache_req.AddProperty(prop);
let _ = unsafe { cache_req.AddProperty(prop) };
}

// Patterns to pre-fetch (for action detection + Value read).
Expand All @@ -168,16 +169,16 @@ unsafe fn build_cache_request(
UIA_TextPatternId,
UIA_ScrollPatternId,
] {
let _ = cache_req.AddPattern(pat);
let _ = unsafe { cache_req.AddPattern(pat) };
}

// Fetch the entire subtree in one bulk RPC.
let _ = cache_req.SetTreeScope(TreeScope_Subtree);
let _ = unsafe { cache_req.SetTreeScope(TreeScope_Subtree) };

// Control-view filter (same set ControlViewWalker would walk) — drops
// decorative / raw-view nodes that only add noise.
if let Ok(ctrl_cond) = automation.ControlViewCondition() {
let _ = cache_req.SetTreeFilter(&ctrl_cond);
if let Ok(ctrl_cond) = unsafe { automation.ControlViewCondition() } {
let _ = unsafe { cache_req.SetTreeFilter(&ctrl_cond) };
}

Ok(cache_req)
Expand All @@ -193,7 +194,9 @@ pub(crate) unsafe fn build_updated_cache_with_retry(
) -> BitFunResult<IUIAutomationElement> {
let mut attempt = 0u32;
loop {
match uncached.BuildUpdatedCache(cache_req) {
// SAFETY: both COM interfaces are live for the call and `cache_req`
// was constructed by the same UI Automation instance.
match unsafe { uncached.BuildUpdatedCache(cache_req) } {
Ok(e) => return Ok(e),
Err(e) => {
attempt += 1;
Expand Down Expand Up @@ -422,39 +425,45 @@ unsafe fn walk_tree_full(
max_elements: usize,
max_depth: usize,
) -> BitFunResult<(String, Vec<UiaNode>)> {
let _ = CoInitializeEx(None, COINIT_APARTMENTTHREADED);
// SAFETY: initializes COM for the current thread and creates the documented
// in-process UI Automation class; failures are handled below.
let _ = unsafe { CoInitializeEx(None, COINIT_APARTMENTTHREADED) };

let automation: IUIAutomation = CoCreateInstance(&CUIAutomation, None, CLSCTX_INPROC_SERVER)
.map_err(|e| {
let automation: IUIAutomation =
unsafe { CoCreateInstance(&CUIAutomation, None, CLSCTX_INPROC_SERVER) }.map_err(|e| {
BitFunError::tool(format!(
"UI Automation (CoCreateInstance CUIAutomation): {}.",
e
))
})?;

let cache_req = build_cache_request(&automation)?;
let cache_req = unsafe { build_cache_request(&automation) }?;

let uncached = automation.ElementFromHandle(hwnd).map_err(|e| {
// SAFETY: `hwnd` is the caller-provided target handle. UIA reports an
// error for an invalid or stale handle, which is propagated here.
let uncached = unsafe { automation.ElementFromHandle(hwnd) }.map_err(|e| {
BitFunError::tool(format!("UI Automation ElementFromHandle failed: {}.", e))
})?;

let root_elem = build_updated_cache_with_retry(&uncached, &cache_req)?;
let root_elem = unsafe { build_updated_cache_with_retry(&uncached, &cache_req) }?;

let mut nodes: Vec<UiaNode> = Vec::new();
let mut lines: Vec<(usize, String)> = Vec::new();
let mut counter = 0usize;
let mut total = 0usize;
walk_cached_bounded(
&root_elem,
0,
None,
&mut nodes,
&mut lines,
&mut counter,
&mut total,
max_elements,
max_depth,
);
unsafe {
walk_cached_bounded(
&root_elem,
0,
None,
&mut nodes,
&mut lines,
&mut counter,
&mut total,
max_elements,
max_depth,
)
};

let tree_text = render_lines(&lines);
Ok((tree_text, nodes))
Expand Down Expand Up @@ -555,21 +564,25 @@ unsafe fn walk_cached_bounded(
}

// Recurse using cached children — zero additional cross-process RPCs.
if let Ok(children) = element.GetCachedChildren() {
let len = children.Length().unwrap_or(0);
// SAFETY: `element` is a live cached UIA element, and child indices are
// bounded by the array length returned by UI Automation.
if let Ok(children) = unsafe { element.GetCachedChildren() } {
let len = unsafe { children.Length() }.unwrap_or(0);
for i in 0..len {
if let Ok(child) = children.GetElement(i) {
walk_cached_bounded(
&child,
depth + 1,
emitted_parent,
nodes,
lines,
counter,
total,
max_elements,
max_depth,
);
if let Ok(child) = unsafe { children.GetElement(i) } {
unsafe {
walk_cached_bounded(
&child,
depth + 1,
emitted_parent,
nodes,
lines,
counter,
total,
max_elements,
max_depth,
)
};
}
}
}
Expand Down
Loading
Loading