pub mod a11y;
pub mod backend;
pub mod input;
pub mod ops;
pub mod schema;
pub mod state;
use super::extensions::ExtensionMode;
use crate::{
core::{
extensions::{
Extension, ExtensionCommand, ExtensionTool, ForkProjectInfo, ToolExecCtx,
is_deferred_tool_activated,
},
tools::{ToolError, ToolResult},
},
extensions::{
EXTENSION_FACTORIES, ExtensionFactory, PRIORITY_COMPUTER, command_arg, tool_search,
},
modes::interactive::{
app::{App, MsgLevel},
handlers::register_slash_command,
},
};
use futures_util::future::BoxFuture;
use serde_json::Value;
use std::sync::{Arc, OnceLock};
const EXT: &str = "computer";
const CMD: &str = "computer";
#[linkme::distributed_slice(EXTENSION_FACTORIES)]
static COMPUTER_FACTORY: ExtensionFactory = ExtensionFactory {
priority: PRIORITY_COMPUTER,
make: || -> Arc<dyn Extension> { Arc::new(Computer) },
};
pub struct Computer;
impl Extension for Computer {
fn name(&self) -> &str {
EXT
}
fn description(&self) -> &str {
"Drive the real desktop: screenshots, mouse/keyboard, windows and the accessibility tree (computer_* tools)."
}
fn fork_project(&self) -> Option<ForkProjectInfo> {
Some(ForkProjectInfo {
plugin_name: "dsh-computer-use".to_string(),
plugin_version: "1.39.5".to_string(),
url: "https://github.com/LJL454/dsh-computer-use".to_string(),
})
}
fn modes(&self) -> Vec<ExtensionMode> {
vec![ExtensionMode::Dev, ExtensionMode::Creator]
}
fn default_enabled(&self) -> bool {
false
}
fn prerequisite_tools(&self) -> Vec<String> {
vec![tool_search::TOOL_NAME.to_string()]
}
fn commands(&self) -> Vec<ExtensionCommand> {
vec![ExtensionCommand {
name: CMD.to_string(),
description:
"Show the desktop-control backend, displays and which computer_* tools are loaded"
.to_string(),
busy_safe: true,
subcommands: Vec::new(),
}]
}
fn on_registered(&self) {
register_slash_command(EXT, CMD, command_computer);
}
fn tools(&self) -> Vec<ExtensionTool> {
schema::tools()
}
fn on_enabled_changed(&self, enabled: bool) {
if !enabled {
state::reset();
_ = input::release_all();
}
}
fn execute_tool_async(
&self,
name: String,
args: Value,
ctx: ToolExecCtx,
) -> BoxFuture<'static, Result<ToolResult, ToolError>> {
Box::pin(async move {
if ctx.depth > 0 || ctx.agent_id.is_some() {
return Err(ToolError(
"computer control is not available to subagents: a delegated agent's scope is fixed when it starts and cannot be widened from inside. Hand this step back to the parent agent.".to_string(),
));
}
if !backend_available() {
return Err(ToolError(format!(
"computer control has no usable backend here ({}); on Linux this needs a Wayland session, a screenshot tool (grim) or xcap, and `swaymsg` for window enumeration",
backend::describe()
)));
}
ops::dispatch(&name, &args).await
})
}
}
fn backend_available() -> bool {
static AVAILABLE: OnceLock<bool> = OnceLock::new();
*AVAILABLE.get_or_init(backend::available)
}
fn command_computer(st: &mut App, raw: &str) -> bool {
let arg = command_arg(raw).trim();
match arg.to_ascii_lowercase().as_str() {
"" | "status" => st.push_msg(status_text(), MsgLevel::Info),
other => st.push_msg(
format!("usage: /computer (got {other:?})"),
MsgLevel::Warning,
),
}
false
}
fn status_text() -> String {
let mut lines = vec![format!(
"backend: {} (available: {})",
backend::describe(),
backend_available()
)];
if let Ok(displays) = backend::displays() {
lines.push(format!(
"displays: {}",
backend::render_displays(&displays).replace('\n', "; ")
));
}
lines.push(format!(
"accessibility: {}",
if a11y::available() {
"available"
} else {
"unavailable"
}
));
let tools = schema::tools();
let loaded: Vec<&str> = tools
.iter()
.map(|t| t.name.as_str())
.filter(|name| is_deferred_tool_activated(name))
.collect();
lines.push(if loaded.is_empty() {
format!(
"tools: 0/{} loaded; they are deferred tools, the model loads them with tool_search",
tools.len()
)
} else {
format!(
"tools: {}/{} loaded: {}",
loaded.len(),
tools.len(),
loaded.join(", ")
)
});
lines.join("\n")
}