Add Open Tool (#27499)

Richard Feldman created

I've seen models try to run `open` in Bash. This is a cross-platform
version of that.

<img width="634" alt="Screenshot 2025-03-26 at 10 27 40 AM"
src="https://github.com/user-attachments/assets/b18cb50f-6e2f-4770-b15c-1040916a420a"
/>

Release Notes:

- N/A

Change summary

Cargo.lock                                          |  1 
Cargo.toml                                          |  1 
crates/assistant_tools/Cargo.toml                   |  1 
crates/assistant_tools/src/assistant_tools.rs       |  3 
crates/assistant_tools/src/open_tool.rs             | 68 +++++++++++++++
crates/assistant_tools/src/open_tool/description.md |  6 +
6 files changed, 80 insertions(+)

Detailed changes

Cargo.lock 🔗

@@ -724,6 +724,7 @@ dependencies = [
  "itertools 0.14.0",
  "language",
  "language_model",
+ "open",
  "project",
  "rand 0.8.5",
  "release_channel",

Cargo.toml 🔗

@@ -470,6 +470,7 @@ mlua = { version = "0.10", features = ["lua54", "vendored", "async", "send"] }
 nanoid = "0.4"
 nbformat = { version = "0.10.0" }
 nix = "0.29"
+open = "5.0.0"
 num-format = "0.4.4"
 ordered-float = "2.1.1"
 palette = { version = "0.7.5", default-features = false, features = ["std"] }

crates/assistant_tools/Cargo.toml 🔗

@@ -35,6 +35,7 @@ ui.workspace = true
 util.workspace = true
 workspace.workspace = true
 worktree.workspace = true
+open = { workspace = true }
 
 [dev-dependencies]
 collections = { workspace = true, features = ["test-support"] }

crates/assistant_tools/src/assistant_tools.rs 🔗

@@ -10,6 +10,7 @@ mod find_replace_file_tool;
 mod list_directory_tool;
 mod move_path_tool;
 mod now_tool;
+mod open_tool;
 mod path_search_tool;
 mod read_file_tool;
 mod regex_search_tool;
@@ -34,6 +35,7 @@ use crate::fetch_tool::FetchTool;
 use crate::find_replace_file_tool::FindReplaceFileTool;
 use crate::list_directory_tool::ListDirectoryTool;
 use crate::now_tool::NowTool;
+use crate::open_tool::OpenTool;
 use crate::path_search_tool::PathSearchTool;
 use crate::read_file_tool::ReadFileTool;
 use crate::regex_search_tool::RegexSearchTool;
@@ -55,6 +57,7 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
     registry.register_tool(EditFilesTool);
     registry.register_tool(ListDirectoryTool);
     registry.register_tool(NowTool);
+    registry.register_tool(OpenTool);
     registry.register_tool(PathSearchTool);
     registry.register_tool(ReadFileTool);
     registry.register_tool(RegexSearchTool);

crates/assistant_tools/src/open_tool.rs 🔗

@@ -0,0 +1,68 @@
+use anyhow::{anyhow, Context as _, Result};
+use assistant_tool::{ActionLog, Tool};
+use gpui::{App, AppContext, Entity, Task};
+use language_model::LanguageModelRequestMessage;
+use project::Project;
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+use std::sync::Arc;
+use ui::IconName;
+use util::markdown::MarkdownString;
+
+#[derive(Debug, Serialize, Deserialize, JsonSchema)]
+pub struct OpenToolInput {
+    /// The path or URL to open with the default application.
+    path_or_url: String,
+}
+
+pub struct OpenTool;
+
+impl Tool for OpenTool {
+    fn name(&self) -> String {
+        "open".to_string()
+    }
+
+    fn needs_confirmation(&self) -> bool {
+        true
+    }
+
+    fn description(&self) -> String {
+        include_str!("./open_tool/description.md").to_string()
+    }
+
+    fn icon(&self) -> IconName {
+        IconName::ExternalLink
+    }
+
+    fn input_schema(&self) -> serde_json::Value {
+        let schema = schemars::schema_for!(OpenToolInput);
+        serde_json::to_value(&schema).unwrap()
+    }
+
+    fn ui_text(&self, input: &serde_json::Value) -> String {
+        match serde_json::from_value::<OpenToolInput>(input.clone()) {
+            Ok(input) => format!("Open `{}`", MarkdownString::escape(&input.path_or_url)),
+            Err(_) => "Open file or URL".to_string(),
+        }
+    }
+
+    fn run(
+        self: Arc<Self>,
+        input: serde_json::Value,
+        _messages: &[LanguageModelRequestMessage],
+        _project: Entity<Project>,
+        _action_log: Entity<ActionLog>,
+        cx: &mut App,
+    ) -> Task<Result<String>> {
+        let input: OpenToolInput = match serde_json::from_value(input) {
+            Ok(input) => input,
+            Err(err) => return Task::ready(Err(anyhow!(err))),
+        };
+
+        cx.background_spawn(async move {
+            open::that(&input.path_or_url).context("Failed to open URL or file path")?;
+
+            Ok(format!("Successfully opened {}", input.path_or_url))
+        })
+    }
+}

crates/assistant_tools/src/open_tool/description.md 🔗

@@ -0,0 +1,6 @@
+This tool opens a file or URL with the default application associated with it on the user's operating system:
+- On macOS, it's equivalent to the `open` command
+- On Windows, it's equivalent to `start`
+- On Linux, it uses something like `xdg-open`, `gio open`, `gnome-open`, `kde-open`, `wslview` as appropriate
+
+For example, it can open a web browser with a URL, open a PDF file with the default PDF viewer, etc.