Remove Lua scripting tool (#27388)

Richard Feldman created

We decided to take this out for now. It doesn't seem necessary, and it
complicates the code a lot. We can always put it back later if desired.

Release Notes:

- N/A

Change summary

Cargo.lock                                              |   90 
Cargo.toml                                              |    2 
crates/assistant2/Cargo.toml                            |    1 
crates/assistant2/src/active_thread.rs                  |  257 -
crates/assistant2/src/thread.rs                         |  216 -
crates/assistant2/src/tool_selector.rs                  |   25 
crates/assistant2/src/tool_use.rs                       |   15 
crates/assistant_tool/src/tool_working_set.rs           |   42 
crates/scripting_tool/Cargo.toml                        |   42 
crates/scripting_tool/LICENSE-GPL                       |    1 
crates/scripting_tool/src/sandbox_preamble.lua          |   55 
crates/scripting_tool/src/scripting_session.rs          | 1314 -----------
crates/scripting_tool/src/scripting_tool.rs             |   30 
crates/scripting_tool/src/scripting_tool_description.md |   21 
14 files changed, 52 insertions(+), 2,059 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -492,7 +492,6 @@ dependencies = [
  "proto",
  "rand 0.8.5",
  "rope",
- "scripting_tool",
  "serde",
  "serde_json",
  "settings",
@@ -4521,12 +4520,6 @@ dependencies = [
  "regex",
 ]
 
-[[package]]
-name = "env_home"
-version = "0.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe"
-
 [[package]]
 name = "env_logger"
 version = "0.10.2"
@@ -7931,25 +7924,6 @@ dependencies = [
  "url",
 ]
 
-[[package]]
-name = "lua-src"
-version = "547.0.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1edaf29e3517b49b8b746701e5648ccb5785cde1c119062cbabbc5d5cd115e42"
-dependencies = [
- "cc",
-]
-
-[[package]]
-name = "luajit-src"
-version = "210.5.12+a4f56a4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b3a8e7962a5368d5f264d045a5a255e90f9aa3fc1941ae15a8d2940d42cac671"
-dependencies = [
- "cc",
- "which 7.0.2",
-]
-
 [[package]]
 name = "lyon"
 version = "1.0.1"
@@ -8365,34 +8339,6 @@ dependencies = [
  "strum",
 ]
 
-[[package]]
-name = "mlua"
-version = "0.10.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d3f763c1041eff92ffb5d7169968a327e1ed2ebfe425dac0ee5a35f29082534b"
-dependencies = [
- "bstr",
- "either",
- "futures-util",
- "mlua-sys",
- "num-traits",
- "parking_lot",
- "rustc-hash 2.1.1",
-]
-
-[[package]]
-name = "mlua-sys"
-version = "0.6.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1901c1a635a22fe9250ffcc4fcc937c16b47c2e9e71adba8784af8bca1f69594"
-dependencies = [
- "cc",
- "cfg-if",
- "lua-src",
- "luajit-src",
- "pkg-config",
-]
-
 [[package]]
 name = "msvc_spectre_libs"
 version = "0.1.2"
@@ -12201,30 +12147,6 @@ version = "1.0.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a3cf7c11c38cb994f3d40e8a8cde3bbd1f72a435e4c49e85d6553d8312306152"
 
-[[package]]
-name = "scripting_tool"
-version = "0.1.0"
-dependencies = [
- "anyhow",
- "buffer_diff",
- "clock",
- "collections",
- "futures 0.3.31",
- "gpui",
- "language",
- "log",
- "mlua",
- "parking_lot",
- "project",
- "rand 0.8.5",
- "regex",
- "schemars",
- "serde",
- "serde_json",
- "settings",
- "util",
-]
-
 [[package]]
 name = "scrypt"
 version = "0.11.0"
@@ -16186,18 +16108,6 @@ dependencies = [
  "winsafe",
 ]
 
-[[package]]
-name = "which"
-version = "7.0.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2774c861e1f072b3aadc02f8ba886c26ad6321567ecc294c935434cad06f1283"
-dependencies = [
- "either",
- "env_home",
- "rustix",
- "winsafe",
-]
-
 [[package]]
 name = "whoami"
 version = "1.5.2"

Cargo.toml 🔗

@@ -124,7 +124,6 @@ members = [
     "crates/rope",
     "crates/rpc",
     "crates/schema_generator",
-    "crates/scripting_tool",
     "crates/search",
     "crates/semantic_index",
     "crates/semantic_version",
@@ -329,7 +328,6 @@ reqwest_client = { path = "crates/reqwest_client" }
 rich_text = { path = "crates/rich_text" }
 rope = { path = "crates/rope" }
 rpc = { path = "crates/rpc" }
-scripting_tool = { path = "crates/scripting_tool" }
 search = { path = "crates/search" }
 semantic_index = { path = "crates/semantic_index" }
 semantic_version = { path = "crates/semantic_version" }

crates/assistant2/Cargo.toml 🔗

@@ -62,7 +62,6 @@ prompt_library.workspace = true
 prompt_store.workspace = true
 proto.workspace = true
 rope.workspace = true
-scripting_tool.workspace = true
 serde.workspace = true
 serde_json.workspace = true
 settings.workspace = true

crates/assistant2/src/active_thread.rs 🔗

@@ -3,8 +3,9 @@ use crate::thread::{
     ThreadEvent, ThreadFeedback,
 };
 use crate::thread_store::ThreadStore;
-use crate::tool_use::{PendingToolUseStatus, ToolType, ToolUse, ToolUseStatus};
+use crate::tool_use::{PendingToolUseStatus, ToolUse, ToolUseStatus};
 use crate::ui::ContextPill;
+
 use collections::HashMap;
 use editor::{Editor, MultiBuffer};
 use gpui::{
@@ -16,7 +17,6 @@ use gpui::{
 use language::{Buffer, LanguageRegistry};
 use language_model::{LanguageModelRegistry, LanguageModelToolUseId, Role};
 use markdown::{Markdown, MarkdownStyle};
-use scripting_tool::{ScriptingTool, ScriptingToolInput};
 use settings::Settings as _;
 use std::sync::Arc;
 use std::time::Duration;
@@ -37,7 +37,6 @@ pub struct ActiveThread {
     messages: Vec<MessageId>,
     list_state: ListState,
     rendered_messages_by_id: HashMap<MessageId, RenderedMessage>,
-    rendered_scripting_tool_uses: HashMap<LanguageModelToolUseId, Entity<Markdown>>,
     rendered_tool_use_labels: HashMap<LanguageModelToolUseId, Entity<Markdown>>,
     editing_message: Option<(MessageId, EditMessageState)>,
     expanded_tool_uses: HashMap<LanguageModelToolUseId, bool>,
@@ -233,7 +232,6 @@ impl ActiveThread {
             save_thread_task: None,
             messages: Vec::new(),
             rendered_messages_by_id: HashMap::default(),
-            rendered_scripting_tool_uses: HashMap::default(),
             rendered_tool_use_labels: HashMap::default(),
             expanded_tool_uses: HashMap::default(),
             expanded_thinking_segments: HashMap::default(),
@@ -260,26 +258,6 @@ impl ActiveThread {
                     cx,
                 );
             }
-
-            for tool_use in thread
-                .read(cx)
-                .scripting_tool_uses_for_message(message.id, cx)
-            {
-                this.render_tool_use_label_markdown(
-                    tool_use.id.clone(),
-                    tool_use.ui_text.clone(),
-                    window,
-                    cx,
-                );
-
-                this.render_scripting_tool_use_markdown(
-                    tool_use.id.clone(),
-                    tool_use.ui_text.as_ref(),
-                    tool_use.input.clone(),
-                    window,
-                    cx,
-                );
-            }
         }
 
         this
@@ -360,36 +338,6 @@ impl ActiveThread {
         self.rendered_messages_by_id.remove(id);
     }
 
-    /// Renders the input of a scripting tool use to Markdown.
-    ///
-    /// Does nothing if the tool use does not correspond to the scripting tool.
-    fn render_scripting_tool_use_markdown(
-        &mut self,
-        tool_use_id: LanguageModelToolUseId,
-        tool_name: &str,
-        tool_input: serde_json::Value,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        if tool_name != ScriptingTool::NAME {
-            return;
-        }
-
-        let lua_script = serde_json::from_value::<ScriptingToolInput>(tool_input)
-            .map(|input| input.lua_script)
-            .unwrap_or_default();
-
-        let lua_script = render_markdown(
-            format!("```lua\n{lua_script}\n```").into(),
-            self.language_registry.clone(),
-            window,
-            cx,
-        );
-
-        self.rendered_scripting_tool_uses
-            .insert(tool_use_id, lua_script);
-    }
-
     fn render_tool_use_label_markdown(
         &mut self,
         tool_use_id: LanguageModelToolUseId,
@@ -476,13 +424,6 @@ impl ActiveThread {
                         window,
                         cx,
                     );
-                    self.render_scripting_tool_use_markdown(
-                        tool_use.id,
-                        tool_use.name.as_ref(),
-                        tool_use.input.clone(),
-                        window,
-                        cx,
-                    );
                 }
             }
             ThreadEvent::ToolFinished {
@@ -725,13 +666,9 @@ impl ActiveThread {
         let checkpoint = thread.checkpoint_for_message(message_id);
         let context = thread.context_for_message(message_id);
         let tool_uses = thread.tool_uses_for_message(message_id, cx);
-        let scripting_tool_uses = thread.scripting_tool_uses_for_message(message_id, cx);
 
         // Don't render user messages that are just there for returning tool results.
-        if message.role == Role::User
-            && (thread.message_has_tool_results(message_id)
-                || thread.message_has_scripting_tool_results(message_id))
-        {
+        if message.role == Role::User && thread.message_has_tool_results(message_id) {
             return Empty.into_any();
         }
 
@@ -996,32 +933,23 @@ impl ActiveThread {
                         )
                         .child(div().p_2().child(message_content)),
                 ),
-            Role::Assistant => {
-                v_flex()
-                    .id(("message-container", ix))
-                    .ml_2()
-                    .pl_2()
-                    .pr_4()
-                    .border_l_1()
-                    .border_color(cx.theme().colors().border_variant)
-                    .child(message_content)
-                    .when(
-                        !tool_uses.is_empty() || !scripting_tool_uses.is_empty(),
-                        |parent| {
-                            parent.child(
-                                v_flex()
-                                    .children(
-                                        tool_uses
-                                            .into_iter()
-                                            .map(|tool_use| self.render_tool_use(tool_use, cx)),
-                                    )
-                                    .children(scripting_tool_uses.into_iter().map(|tool_use| {
-                                        self.render_scripting_tool_use(tool_use, cx)
-                                    })),
-                            )
-                        },
+            Role::Assistant => v_flex()
+                .id(("message-container", ix))
+                .ml_2()
+                .pl_2()
+                .pr_4()
+                .border_l_1()
+                .border_color(cx.theme().colors().border_variant)
+                .child(message_content)
+                .when(!tool_uses.is_empty(), |parent| {
+                    parent.child(
+                        v_flex().children(
+                            tool_uses
+                                .into_iter()
+                                .map(|tool_use| self.render_tool_use(tool_use, cx)),
+                        ),
                     )
-            }
+                }),
             Role::System => div().id(("message-container", ix)).py_1().px_2().child(
                 v_flex()
                     .bg(colors.editor_background)
@@ -1537,145 +1465,6 @@ impl ActiveThread {
         )
     }
 
-    fn render_scripting_tool_use(
-        &self,
-        tool_use: ToolUse,
-        cx: &mut Context<Self>,
-    ) -> impl IntoElement {
-        let is_open = self
-            .expanded_tool_uses
-            .get(&tool_use.id)
-            .copied()
-            .unwrap_or_default();
-
-        div().px_2p5().child(
-            v_flex()
-                .gap_1()
-                .rounded_lg()
-                .border_1()
-                .border_color(cx.theme().colors().border)
-                .child(
-                    h_flex()
-                        .justify_between()
-                        .py_0p5()
-                        .pl_1()
-                        .pr_2()
-                        .bg(cx.theme().colors().editor_foreground.opacity(0.02))
-                        .map(|element| {
-                            if is_open {
-                                element.border_b_1().rounded_t_md()
-                            } else {
-                                element.rounded_md()
-                            }
-                        })
-                        .border_color(cx.theme().colors().border)
-                        .child(
-                            h_flex()
-                                .gap_1()
-                                .child(Disclosure::new("tool-use-disclosure", is_open).on_click(
-                                    cx.listener({
-                                        let tool_use_id = tool_use.id.clone();
-                                        move |this, _event, _window, _cx| {
-                                            let is_open = this
-                                                .expanded_tool_uses
-                                                .entry(tool_use_id.clone())
-                                                .or_insert(false);
-
-                                            *is_open = !*is_open;
-                                        }
-                                    }),
-                                ))
-                                .child(
-                                    h_flex()
-                                        .gap_1p5()
-                                        .child(
-                                            Icon::new(IconName::Terminal)
-                                                .size(IconSize::XSmall)
-                                                .color(Color::Muted),
-                                        )
-                                        .child(
-                                            div()
-                                                .text_ui_sm(cx)
-                                                .children(
-                                                    self.rendered_tool_use_labels
-                                                        .get(&tool_use.id)
-                                                        .cloned(),
-                                                )
-                                                .truncate(),
-                                        ),
-                                ),
-                        )
-                        .child(
-                            Label::new(match tool_use.status {
-                                ToolUseStatus::Pending => "Pending",
-                                ToolUseStatus::Running => "Running",
-                                ToolUseStatus::Finished(_) => "Finished",
-                                ToolUseStatus::Error(_) => "Error",
-                                ToolUseStatus::NeedsConfirmation => "Asking Permission",
-                            })
-                            .size(LabelSize::XSmall)
-                            .buffer_font(cx),
-                        ),
-                )
-                .map(|parent| {
-                    if !is_open {
-                        return parent;
-                    }
-
-                    let lua_script_markdown =
-                        self.rendered_scripting_tool_uses.get(&tool_use.id).cloned();
-
-                    parent.child(
-                        v_flex()
-                            .child(
-                                v_flex()
-                                    .gap_0p5()
-                                    .py_1()
-                                    .px_2p5()
-                                    .border_b_1()
-                                    .border_color(cx.theme().colors().border)
-                                    .child(Label::new("Input:"))
-                                    .map(|parent| {
-                                        if let Some(markdown) = lua_script_markdown {
-                                            parent.child(markdown)
-                                        } else {
-                                            parent.child(Label::new(
-                                                "Failed to render script input to Markdown",
-                                            ))
-                                        }
-                                    }),
-                            )
-                            .map(|parent| match tool_use.status {
-                                ToolUseStatus::Finished(output) => parent.child(
-                                    v_flex()
-                                        .gap_0p5()
-                                        .py_1()
-                                        .px_2p5()
-                                        .child(Label::new("Result:"))
-                                        .child(Label::new(output)),
-                                ),
-                                ToolUseStatus::Error(err) => parent.child(
-                                    v_flex()
-                                        .gap_0p5()
-                                        .py_1()
-                                        .px_2p5()
-                                        .child(Label::new("Error:"))
-                                        .child(Label::new(err)),
-                                ),
-                                ToolUseStatus::Pending | ToolUseStatus::Running => parent,
-                                ToolUseStatus::NeedsConfirmation => parent.child(
-                                    v_flex()
-                                        .gap_0p5()
-                                        .py_1()
-                                        .px_2p5()
-                                        .child(Label::new("Asking Permission")),
-                                ),
-                            }),
-                    )
-                }),
-        )
-    }
-
     fn render_rules_item(&self, cx: &Context<Self>) -> AnyElement {
         let Some(system_prompt_context) = self.thread.read(cx).system_prompt_context().as_ref()
         else {
@@ -1751,7 +1540,7 @@ impl ActiveThread {
                     c.ui_text.clone(),
                     c.input.clone(),
                     &c.messages,
-                    c.tool_type.clone(),
+                    c.tool.clone(),
                     cx,
                 );
             });
@@ -1761,13 +1550,12 @@ impl ActiveThread {
     fn handle_deny_tool(
         &mut self,
         tool_use_id: LanguageModelToolUseId,
-        tool_type: ToolType,
         _: &ClickEvent,
         _window: &mut Window,
         cx: &mut Context<Self>,
     ) {
         self.thread.update(cx, |thread, cx| {
-            thread.deny_tool_use(tool_use_id, tool_type, cx);
+            thread.deny_tool_use(tool_use_id, cx);
         });
     }
 
@@ -1802,7 +1590,7 @@ impl ActiveThread {
 
         thread
             .tools_needing_confirmation()
-            .map(|(tool_type, tool)| {
+            .map(|tool| {
                 div()
                     .m_3()
                     .p_2()
@@ -1844,7 +1632,6 @@ impl ActiveThread {
                                             move |this, event, window, cx| {
                                                 this.handle_deny_tool(
                                                     tool_id.clone(),
-                                                    tool_type.clone(),
                                                     event,
                                                     window,
                                                     cx,

crates/assistant2/src/thread.rs 🔗

@@ -23,7 +23,6 @@ use project::{Project, Worktree};
 use prompt_store::{
     AssistantSystemPromptContext, PromptBuilder, RulesFile, WorktreeInfoForSystemPrompt,
 };
-use scripting_tool::{ScriptingSession, ScriptingTool};
 use serde::{Deserialize, Serialize};
 use settings::Settings;
 use util::{maybe, post_inc, ResultExt as _, TryFutureExt as _};
@@ -34,7 +33,7 @@ use crate::thread_store::{
     SerializedMessage, SerializedMessageSegment, SerializedThread, SerializedToolResult,
     SerializedToolUse,
 };
-use crate::tool_use::{PendingToolUse, PendingToolUseStatus, ToolType, ToolUse, ToolUseState};
+use crate::tool_use::{PendingToolUse, ToolUse, ToolUseState};
 
 #[derive(Debug, Clone, Copy)]
 pub enum RequestKind {
@@ -188,8 +187,6 @@ pub struct Thread {
     action_log: Entity<ActionLog>,
     last_restore_checkpoint: Option<LastRestoreCheckpoint>,
     pending_checkpoint: Option<ThreadCheckpoint>,
-    scripting_session: Entity<ScriptingSession>,
-    scripting_tool_use: ToolUseState,
     initial_project_snapshot: Shared<Task<Option<Arc<ProjectSnapshot>>>>,
     cumulative_token_usage: TokenUsage,
     feedback: Option<ThreadFeedback>,
@@ -221,8 +218,6 @@ impl Thread {
             last_restore_checkpoint: None,
             pending_checkpoint: None,
             tool_use: ToolUseState::new(tools.clone()),
-            scripting_session: cx.new(|cx| ScriptingSession::new(project.clone(), cx)),
-            scripting_tool_use: ToolUseState::new(tools),
             action_log: cx.new(|_| ActionLog::new()),
             initial_project_snapshot: {
                 let project_snapshot = Self::project_snapshot(project, cx);
@@ -251,14 +246,7 @@ impl Thread {
                 .unwrap_or(0),
         );
         let tool_use =
-            ToolUseState::from_serialized_messages(tools.clone(), &serialized.messages, |name| {
-                name != ScriptingTool::NAME
-            });
-        let scripting_tool_use =
-            ToolUseState::from_serialized_messages(tools.clone(), &serialized.messages, |name| {
-                name == ScriptingTool::NAME
-            });
-        let scripting_session = cx.new(|cx| ScriptingSession::new(project.clone(), cx));
+            ToolUseState::from_serialized_messages(tools.clone(), &serialized.messages, |_| true);
 
         Self {
             id,
@@ -297,8 +285,6 @@ impl Thread {
             tools,
             tool_use,
             action_log: cx.new(|_| ActionLog::new()),
-            scripting_session,
-            scripting_tool_use,
             initial_project_snapshot: Task::ready(serialized.initial_project_snapshot).shared(),
             // TODO: persist token usage?
             cumulative_token_usage: TokenUsage::default(),
@@ -357,37 +343,13 @@ impl Thread {
             .pending_tool_uses()
             .into_iter()
             .find(|tool_use| &tool_use.id == id)
-            .or_else(|| {
-                self.scripting_tool_use
-                    .pending_tool_uses()
-                    .into_iter()
-                    .find(|tool_use| &tool_use.id == id)
-            })
     }
 
-    pub fn tools_needing_confirmation(&self) -> impl Iterator<Item = (ToolType, &PendingToolUse)> {
+    pub fn tools_needing_confirmation(&self) -> impl Iterator<Item = &PendingToolUse> {
         self.tool_use
             .pending_tool_uses()
             .into_iter()
-            .filter_map(|tool_use| {
-                if let PendingToolUseStatus::NeedsConfirmation(confirmation) = &tool_use.status {
-                    Some((confirmation.tool_type.clone(), tool_use))
-                } else {
-                    None
-                }
-            })
-            .chain(
-                self.scripting_tool_use
-                    .pending_tool_uses()
-                    .into_iter()
-                    .filter_map(|tool_use| {
-                        if tool_use.status.needs_confirmation() {
-                            Some((ToolType::ScriptingTool, tool_use))
-                        } else {
-                            None
-                        }
-                    }),
-            )
+            .filter(|tool_use| tool_use.status.needs_confirmation())
     }
 
     pub fn checkpoint_for_message(&self, id: MessageId) -> Option<ThreadCheckpoint> {
@@ -520,25 +482,18 @@ impl Thread {
 
     /// Returns whether all of the tool uses have finished running.
     pub fn all_tools_finished(&self) -> bool {
-        let mut all_pending_tool_uses = self
-            .tool_use
-            .pending_tool_uses()
-            .into_iter()
-            .chain(self.scripting_tool_use.pending_tool_uses());
-
         // If the only pending tool uses left are the ones with errors, then
         // that means that we've finished running all of the pending tools.
-        all_pending_tool_uses.all(|tool_use| tool_use.status.is_error())
+        self.tool_use
+            .pending_tool_uses()
+            .iter()
+            .all(|tool_use| tool_use.status.is_error())
     }
 
     pub fn tool_uses_for_message(&self, id: MessageId, cx: &App) -> Vec<ToolUse> {
         self.tool_use.tool_uses_for_message(id, cx)
     }
 
-    pub fn scripting_tool_uses_for_message(&self, id: MessageId, cx: &App) -> Vec<ToolUse> {
-        self.scripting_tool_use.tool_uses_for_message(id, cx)
-    }
-
     pub fn tool_results_for_message(&self, id: MessageId) -> Vec<&LanguageModelToolResult> {
         self.tool_use.tool_results_for_message(id)
     }
@@ -547,21 +502,10 @@ impl Thread {
         self.tool_use.tool_result(id)
     }
 
-    pub fn scripting_tool_results_for_message(
-        &self,
-        id: MessageId,
-    ) -> Vec<&LanguageModelToolResult> {
-        self.scripting_tool_use.tool_results_for_message(id)
-    }
-
     pub fn message_has_tool_results(&self, message_id: MessageId) -> bool {
         self.tool_use.message_has_tool_results(message_id)
     }
 
-    pub fn message_has_scripting_tool_results(&self, message_id: MessageId) -> bool {
-        self.scripting_tool_use.message_has_tool_results(message_id)
-    }
-
     pub fn insert_user_message(
         &mut self,
         text: impl Into<String>,
@@ -682,7 +626,6 @@ impl Thread {
                         tool_uses: this
                             .tool_uses_for_message(message.id, cx)
                             .into_iter()
-                            .chain(this.scripting_tool_uses_for_message(message.id, cx))
                             .map(|tool_use| SerializedToolUse {
                                 id: tool_use.id,
                                 name: tool_use.name,
@@ -692,7 +635,6 @@ impl Thread {
                         tool_results: this
                             .tool_results_for_message(message.id)
                             .into_iter()
-                            .chain(this.scripting_tool_results_for_message(message.id))
                             .map(|tool_result| SerializedToolResult {
                                 tool_use_id: tool_result.tool_use_id.clone(),
                                 is_error: tool_result.is_error,
@@ -825,15 +767,6 @@ impl Thread {
         let mut request = self.to_completion_request(request_kind, cx);
         request.tools = {
             let mut tools = Vec::new();
-
-            if self.tools.is_scripting_tool_enabled() {
-                tools.push(LanguageModelRequestTool {
-                    name: ScriptingTool::NAME.into(),
-                    description: ScriptingTool::DESCRIPTION.into(),
-                    input_schema: ScriptingTool::input_schema(),
-                });
-            }
-
             tools.extend(self.tools().enabled_tools(cx).into_iter().map(|tool| {
                 LanguageModelRequestTool {
                     name: tool.name(),
@@ -894,8 +827,6 @@ impl Thread {
                 RequestKind::Chat => {
                     self.tool_use
                         .attach_tool_results(message.id, &mut request_message);
-                    self.scripting_tool_use
-                        .attach_tool_results(message.id, &mut request_message);
                 }
                 RequestKind::Summarize => {
                     // We don't care about tool use during summarization.
@@ -912,8 +843,6 @@ impl Thread {
                 RequestKind::Chat => {
                     self.tool_use
                         .attach_tool_uses(message.id, &mut request_message);
-                    self.scripting_tool_use
-                        .attach_tool_uses(message.id, &mut request_message);
                 }
                 RequestKind::Summarize => {
                     // We don't care about tool use during summarization.
@@ -1060,19 +989,11 @@ impl Thread {
                                     .iter()
                                     .rfind(|message| message.role == Role::Assistant)
                                 {
-                                    if tool_use.name.as_ref() == ScriptingTool::NAME {
-                                        thread.scripting_tool_use.request_tool_use(
-                                            last_assistant_message.id,
-                                            tool_use,
-                                            cx,
-                                        );
-                                    } else {
-                                        thread.tool_use.request_tool_use(
-                                            last_assistant_message.id,
-                                            tool_use,
-                                            cx,
-                                        );
-                                    }
+                                    thread.tool_use.request_tool_use(
+                                        last_assistant_message.id,
+                                        tool_use,
+                                        cx,
+                                    );
                                 }
                             }
                         }
@@ -1237,7 +1158,7 @@ impl Thread {
                         tool_use.ui_text.clone(),
                         tool_use.input.clone(),
                         messages.clone(),
-                        ToolType::NonScriptingTool(tool),
+                        tool,
                     );
                 } else {
                     self.run_tool(
@@ -1245,7 +1166,7 @@ impl Thread {
                         tool_use.ui_text.clone(),
                         tool_use.input.clone(),
                         &messages,
-                        ToolType::NonScriptingTool(tool),
+                        tool,
                         cx,
                     );
                 }
@@ -1255,33 +1176,13 @@ impl Thread {
                     tool_use.ui_text.clone(),
                     tool_use.input.clone(),
                     &messages,
-                    ToolType::NonScriptingTool(tool),
+                    tool,
                     cx,
                 );
             }
         }
 
-        let pending_scripting_tool_uses = self
-            .scripting_tool_use
-            .pending_tool_uses()
-            .into_iter()
-            .filter(|tool_use| tool_use.status.is_idle())
-            .cloned()
-            .collect::<Vec<_>>();
-
-        for scripting_tool_use in pending_scripting_tool_uses.iter() {
-            self.scripting_tool_use.confirm_tool_use(
-                scripting_tool_use.id.clone(),
-                scripting_tool_use.ui_text.clone(),
-                scripting_tool_use.input.clone(),
-                messages.clone(),
-                ToolType::ScriptingTool,
-            );
-        }
-
         pending_tool_uses
-            .into_iter()
-            .chain(pending_scripting_tool_uses)
     }
 
     pub fn run_tool(
@@ -1290,21 +1191,12 @@ impl Thread {
         ui_text: impl Into<SharedString>,
         input: serde_json::Value,
         messages: &[LanguageModelRequestMessage],
-        tool_type: ToolType,
+        tool: Arc<dyn Tool>,
         cx: &mut Context<'_, Thread>,
     ) {
-        match tool_type {
-            ToolType::ScriptingTool => {
-                let task = self.spawn_scripting_tool_use(tool_use_id.clone(), input, cx);
-                self.scripting_tool_use
-                    .run_pending_tool(tool_use_id, ui_text.into(), task);
-            }
-            ToolType::NonScriptingTool(tool) => {
-                let task = self.spawn_tool_use(tool_use_id.clone(), messages, input, tool, cx);
-                self.tool_use
-                    .run_pending_tool(tool_use_id, ui_text.into(), task);
-            }
-        }
+        let task = self.spawn_tool_use(tool_use_id.clone(), messages, input, tool, cx);
+        self.tool_use
+            .run_pending_tool(tool_use_id, ui_text.into(), task);
     }
 
     fn spawn_tool_use(
@@ -1344,60 +1236,6 @@ impl Thread {
         })
     }
 
-    fn spawn_scripting_tool_use(
-        &mut self,
-        tool_use_id: LanguageModelToolUseId,
-        input: serde_json::Value,
-        cx: &mut Context<Thread>,
-    ) -> Task<()> {
-        let task = match ScriptingTool::deserialize_input(input) {
-            Err(err) => Task::ready(Err(err.into())),
-            Ok(input) => {
-                let (script_id, script_task) =
-                    self.scripting_session.update(cx, move |session, cx| {
-                        session.run_script(input.lua_script, cx)
-                    });
-
-                let session = self.scripting_session.clone();
-                cx.spawn(async move |_, cx| {
-                    script_task.await;
-
-                    let message = session.read_with(cx, |session, _cx| {
-                        // Using a id to get the script output seems impractical.
-                        // Why not just include it in the Task result?
-                        // This is because we'll later report the script state as it runs,
-                        session
-                            .get(script_id)
-                            .output_message_for_llm()
-                            .expect("Script shouldn't still be running")
-                    })?;
-
-                    Ok(message)
-                })
-            }
-        };
-
-        cx.spawn({
-            let tool_use_id = tool_use_id.clone();
-            async move |thread, cx| {
-                let output = task.await;
-                thread
-                    .update(cx, |thread, cx| {
-                        let pending_tool_use = thread
-                            .scripting_tool_use
-                            .insert_tool_output(tool_use_id.clone(), output);
-
-                        cx.emit(ThreadEvent::ToolFinished {
-                            tool_use_id,
-                            pending_tool_use,
-                            canceled: false,
-                        });
-                    })
-                    .ok();
-            }
-        })
-    }
-
     pub fn attach_tool_results(
         &mut self,
         updated_context: Vec<ContextSnapshot>,
@@ -1654,22 +1492,12 @@ impl Thread {
         self.cumulative_token_usage.clone()
     }
 
-    pub fn deny_tool_use(
-        &mut self,
-        tool_use_id: LanguageModelToolUseId,
-        tool_type: ToolType,
-        cx: &mut Context<Self>,
-    ) {
+    pub fn deny_tool_use(&mut self, tool_use_id: LanguageModelToolUseId, cx: &mut Context<Self>) {
         let err = Err(anyhow::anyhow!(
             "Permission to run tool action denied by user"
         ));
 
-        if let ToolType::ScriptingTool = tool_type {
-            self.scripting_tool_use
-                .insert_tool_output(tool_use_id.clone(), err);
-        } else {
-            self.tool_use.insert_tool_output(tool_use_id.clone(), err);
-        }
+        self.tool_use.insert_tool_output(tool_use_id.clone(), err);
 
         cx.emit(ThreadEvent::ToolFinished {
             tool_use_id,

crates/assistant2/src/tool_selector.rs 🔗

@@ -4,7 +4,6 @@ use assistant_settings::{AgentProfile, AssistantSettings};
 use assistant_tool::{ToolSource, ToolWorkingSet};
 use gpui::{Entity, Subscription};
 use indexmap::IndexMap;
-use scripting_tool::ScriptingTool;
 use settings::{Settings as _, SettingsStore};
 use ui::{prelude::*, ContextMenu, PopoverMenu, Tooltip};
 
@@ -52,7 +51,6 @@ impl ToolSelector {
                     let tools = tool_set.clone();
                     move |_window, cx| {
                         tools.disable_source(ToolSource::Native, cx);
-                        tools.disable_scripting_tool();
                         tools.enable(
                             ToolSource::Native,
                             &profile
@@ -61,10 +59,6 @@ impl ToolSelector {
                                 .filter_map(|(tool, enabled)| enabled.then(|| tool.clone()))
                                 .collect::<Vec<_>>(),
                         );
-
-                        if profile.tools.contains_key(ScriptingTool::NAME) {
-                            tools.enable_scripting_tool();
-                        }
                     }
                 });
             }
@@ -98,11 +92,6 @@ impl ToolSelector {
                     .collect::<Vec<_>>();
 
                 if ToolSource::Native == source {
-                    tools.push((
-                        ToolSource::Native,
-                        ScriptingTool::NAME.into(),
-                        tool_set.is_scripting_tool_enabled(),
-                    ));
                     tools.sort_by(|(_, name_a, _), (_, name_b, _)| name_a.cmp(name_b));
                 }
 
@@ -136,18 +125,10 @@ impl ToolSelector {
                     menu = menu.toggleable_entry(name.clone(), is_enabled, icon_position, None, {
                         let tools = tool_set.clone();
                         move |_window, _cx| {
-                            if name.as_ref() == ScriptingTool::NAME {
-                                if is_enabled {
-                                    tools.disable_scripting_tool();
-                                } else {
-                                    tools.enable_scripting_tool();
-                                }
+                            if is_enabled {
+                                tools.disable(source.clone(), &[name.clone()]);
                             } else {
-                                if is_enabled {
-                                    tools.disable(source.clone(), &[name.clone()]);
-                                } else {
-                                    tools.enable(source.clone(), &[name.clone()]);
-                                }
+                                tools.enable(source.clone(), &[name.clone()]);
                             }
                         }
                     });

crates/assistant2/src/tool_use.rs 🔗

@@ -10,7 +10,6 @@ use language_model::{
     LanguageModelRequestMessage, LanguageModelToolResult, LanguageModelToolUse,
     LanguageModelToolUseId, MessageContent, Role,
 };
-use scripting_tool::ScriptingTool;
 
 use crate::thread::MessageId;
 use crate::thread_store::SerializedMessage;
@@ -200,8 +199,6 @@ impl ToolUseState {
     ) -> SharedString {
         if let Some(tool) = self.tools.tool(tool_name, cx) {
             tool.ui_text(input).into()
-        } else if tool_name == ScriptingTool::NAME {
-            "Run Lua Script".into()
         } else {
             "Unknown tool".into()
         }
@@ -285,7 +282,7 @@ impl ToolUseState {
         ui_text: impl Into<Arc<str>>,
         input: serde_json::Value,
         messages: Arc<Vec<LanguageModelRequestMessage>>,
-        tool_type: ToolType,
+        tool: Arc<dyn Tool>,
     ) {
         if let Some(tool_use) = self.pending_tool_uses_by_id.get_mut(&tool_use_id) {
             let ui_text = ui_text.into();
@@ -294,7 +291,7 @@ impl ToolUseState {
                 tool_use_id,
                 input,
                 messages,
-                tool_type,
+                tool,
                 ui_text,
             };
             tool_use.status = PendingToolUseStatus::NeedsConfirmation(Arc::new(confirmation));
@@ -398,19 +395,13 @@ pub struct PendingToolUse {
     pub status: PendingToolUseStatus,
 }
 
-#[derive(Debug, Clone)]
-pub enum ToolType {
-    ScriptingTool,
-    NonScriptingTool(Arc<dyn Tool>),
-}
-
 #[derive(Debug, Clone)]
 pub struct Confirmation {
     pub tool_use_id: LanguageModelToolUseId,
     pub input: serde_json::Value,
     pub ui_text: Arc<str>,
     pub messages: Arc<Vec<LanguageModelRequestMessage>>,
-    pub tool_type: ToolType,
+    pub tool: Arc<dyn Tool>,
 }
 
 #[derive(Debug, Clone)]

crates/assistant_tool/src/tool_working_set.rs 🔗

@@ -15,26 +15,14 @@ pub struct ToolWorkingSet {
     state: Mutex<WorkingSetState>,
 }
 
+#[derive(Default)]
 struct WorkingSetState {
     context_server_tools_by_id: HashMap<ToolId, Arc<dyn Tool>>,
     context_server_tools_by_name: HashMap<String, Arc<dyn Tool>>,
     disabled_tools_by_source: HashMap<ToolSource, HashSet<Arc<str>>>,
-    is_scripting_tool_disabled: bool,
     next_tool_id: ToolId,
 }
 
-impl Default for WorkingSetState {
-    fn default() -> Self {
-        Self {
-            context_server_tools_by_id: HashMap::default(),
-            context_server_tools_by_name: HashMap::default(),
-            disabled_tools_by_source: HashMap::default(),
-            is_scripting_tool_disabled: true,
-            next_tool_id: ToolId::default(),
-        }
-    }
-}
-
 impl ToolWorkingSet {
     pub fn tool(&self, name: &str, cx: &App) -> Option<Arc<dyn Tool>> {
         self.state
@@ -55,7 +43,7 @@ impl ToolWorkingSet {
 
     pub fn are_all_tools_enabled(&self) -> bool {
         let state = self.state.lock();
-        state.disabled_tools_by_source.is_empty() && !state.is_scripting_tool_disabled
+        state.disabled_tools_by_source.is_empty()
     }
 
     pub fn are_all_tools_from_source_enabled(&self, source: &ToolSource) -> bool {
@@ -70,7 +58,6 @@ impl ToolWorkingSet {
     pub fn enable_all_tools(&self) {
         let mut state = self.state.lock();
         state.disabled_tools_by_source.clear();
-        state.enable_scripting_tool();
     }
 
     pub fn disable_all_tools(&self, cx: &App) {
@@ -124,21 +111,6 @@ impl ToolWorkingSet {
             .retain(|id, _| !tool_ids_to_remove.contains(id));
         state.tools_changed();
     }
-
-    pub fn is_scripting_tool_enabled(&self) -> bool {
-        let state = self.state.lock();
-        !state.is_scripting_tool_disabled
-    }
-
-    pub fn enable_scripting_tool(&self) {
-        let mut state = self.state.lock();
-        state.enable_scripting_tool();
-    }
-
-    pub fn disable_scripting_tool(&self) {
-        let mut state = self.state.lock();
-        state.disable_scripting_tool();
-    }
 }
 
 impl WorkingSetState {
@@ -240,15 +212,5 @@ impl WorkingSetState {
 
             self.disable(source, &tool_names);
         }
-
-        self.disable_scripting_tool();
-    }
-
-    fn enable_scripting_tool(&mut self) {
-        self.is_scripting_tool_disabled = false;
-    }
-
-    fn disable_scripting_tool(&mut self) {
-        self.is_scripting_tool_disabled = true;
     }
 }

crates/scripting_tool/Cargo.toml 🔗

@@ -1,42 +0,0 @@
-[package]
-name = "scripting_tool"
-version = "0.1.0"
-edition.workspace = true
-publish.workspace = true
-license = "GPL-3.0-or-later"
-
-[lints]
-workspace = true
-
-[lib]
-path = "src/scripting_tool.rs"
-doctest = false
-
-[dependencies]
-anyhow.workspace = true
-buffer_diff.workspace = true
-clock.workspace = true
-collections.workspace = true
-futures.workspace = true
-gpui.workspace = true
-language.workspace = true
-log.workspace = true
-mlua.workspace = true
-parking_lot.workspace = true
-project.workspace = true
-regex.workspace = true
-schemars.workspace = true
-serde.workspace = true
-serde_json.workspace = true
-settings.workspace = true
-util.workspace = true
-
-[dev-dependencies]
-buffer_diff = { workspace = true, features = ["test-support"] }
-clock = { workspace = true, features = ["test-support"] }
-collections = { workspace = true, features = ["test-support"] }
-gpui = { workspace = true, features = ["test-support"] }
-language = { workspace = true, features = ["test-support"] }
-project = { workspace = true, features = ["test-support"] }
-rand.workspace = true
-settings = { workspace = true, features = ["test-support"] }

crates/scripting_tool/src/sandbox_preamble.lua 🔗

@@ -1,55 +0,0 @@
----@diagnostic disable: undefined-global
-
--- Create a sandbox environment
-local sandbox = {}
-
--- For now, add all globals to `sandbox` (so there effectively is no sandbox).
--- We still need the logic below so that we can do things like overriding print() to write
--- to our in-memory log rather than to stdout, we will delete this loop (and re-enable
--- the I/O module being sandboxed below) to have things be sandboxed again.
-for k, v in pairs(_G) do
-    if sandbox[k] == nil then
-        sandbox[k] = v
-    end
-end
-
--- Allow access to standard libraries (safe subset)
-sandbox.string = string
-sandbox.table = table
-sandbox.math = math
-sandbox.print = sb_print
-sandbox.type = type
-sandbox.tostring = tostring
-sandbox.tonumber = tonumber
-sandbox.pairs = pairs
-sandbox.ipairs = ipairs
-
--- Access to custom functions
-sandbox.search = search
-sandbox.outline = outline
-
--- Create a sandboxed version of LuaFileIO
--- local io = {};
---
--- For now we are using unsandboxed io
-local io = _G.io;
-
--- File functions
-io.open = sb_io_open
-
--- Add the sandboxed io library to the sandbox environment
-sandbox.io = io
-
--- Load the script with the sandbox environment
-local user_script_fn, err = load(user_script, nil, "t", sandbox)
-
-if not user_script_fn then
-    error("Failed to load user script: " .. tostring(err))
-end
-
--- Execute the user script within the sandbox
-local success, result = pcall(user_script_fn)
-
-if not success then
-    error("Error executing user script: " .. tostring(result))
-end

crates/scripting_tool/src/scripting_session.rs 🔗

@@ -1,1314 +0,0 @@
-use anyhow::anyhow;
-use buffer_diff::BufferDiff;
-use collections::{HashMap, HashSet};
-use futures::{
-    channel::{mpsc, oneshot},
-    pin_mut, SinkExt, StreamExt,
-};
-use gpui::{AppContext, AsyncApp, Context, Entity, Task, WeakEntity};
-use language::Buffer;
-use mlua::{ExternalResult, Lua, MultiValue, ObjectLike, Table, UserData, UserDataMethods};
-use parking_lot::Mutex;
-use project::{search::SearchQuery, Fs, Project, ProjectPath, WorktreeId};
-use regex::Regex;
-use std::{
-    path::{Path, PathBuf},
-    sync::Arc,
-};
-use util::{paths::PathMatcher, ResultExt};
-
-struct ForegroundFn(Box<dyn FnOnce(WeakEntity<ScriptingSession>, AsyncApp) + Send>);
-
-struct BufferChanges {
-    diff: Entity<BufferDiff>,
-    edit_ids: Vec<clock::Lamport>,
-}
-
-pub struct ScriptingSession {
-    project: Entity<Project>,
-    scripts: Vec<Script>,
-    changes_by_buffer: HashMap<Entity<Buffer>, BufferChanges>,
-    foreground_fns_tx: mpsc::Sender<ForegroundFn>,
-    _invoke_foreground_fns: Task<()>,
-}
-
-impl ScriptingSession {
-    pub fn new(project: Entity<Project>, cx: &mut Context<Self>) -> Self {
-        let (foreground_fns_tx, mut foreground_fns_rx) = mpsc::channel(128);
-        ScriptingSession {
-            project,
-            scripts: Vec::new(),
-            changes_by_buffer: HashMap::default(),
-            foreground_fns_tx,
-            _invoke_foreground_fns: cx.spawn(async move |this, cx| {
-                while let Some(foreground_fn) = foreground_fns_rx.next().await {
-                    foreground_fn.0(this.clone(), cx.clone());
-                }
-            }),
-        }
-    }
-
-    pub fn changed_buffers(&self) -> impl ExactSizeIterator<Item = &Entity<Buffer>> {
-        self.changes_by_buffer.keys()
-    }
-
-    pub fn run_script(
-        &mut self,
-        script_src: String,
-        cx: &mut Context<Self>,
-    ) -> (ScriptId, Task<()>) {
-        let id = ScriptId(self.scripts.len() as u32);
-
-        let stdout = Arc::new(Mutex::new(String::new()));
-
-        let script = Script {
-            state: ScriptState::Running {
-                stdout: stdout.clone(),
-            },
-        };
-        self.scripts.push(script);
-
-        let task = self.run_lua(script_src, stdout, cx);
-
-        let task = cx.spawn(async move |session, cx| {
-            let result = task.await;
-
-            session
-                .update(cx, |session, _cx| {
-                    let script = session.get_mut(id);
-                    let stdout = script.stdout_snapshot();
-
-                    script.state = match result {
-                        Ok(()) => ScriptState::Succeeded { stdout },
-                        Err(error) => ScriptState::Failed { stdout, error },
-                    };
-                })
-                .log_err();
-        });
-
-        (id, task)
-    }
-
-    fn run_lua(
-        &mut self,
-        script: String,
-        stdout: Arc<Mutex<String>>,
-        cx: &mut Context<Self>,
-    ) -> Task<anyhow::Result<()>> {
-        const SANDBOX_PREAMBLE: &str = include_str!("sandbox_preamble.lua");
-
-        // TODO Honor all worktrees instead of the first one
-        let worktree_info = self
-            .project
-            .read(cx)
-            .visible_worktrees(cx)
-            .next()
-            .map(|worktree| {
-                let worktree = worktree.read(cx);
-                (worktree.id(), worktree.abs_path())
-            });
-
-        let root_dir = worktree_info.as_ref().map(|(_, root)| root.clone());
-
-        let fs = self.project.read(cx).fs().clone();
-        let foreground_fns_tx = self.foreground_fns_tx.clone();
-
-        let task = cx.background_spawn({
-            let stdout = stdout.clone();
-
-            async move {
-                let lua = Lua::new();
-                lua.set_memory_limit(2 * 1024 * 1024 * 1024)?; // 2 GB
-                let globals = lua.globals();
-
-                // Use the project root dir as the script's current working dir.
-                if let Some(root_dir) = &root_dir {
-                    if let Some(root_dir) = root_dir.to_str() {
-                        globals.set("cwd", root_dir)?;
-                    }
-                }
-
-                globals.set(
-                    "sb_print",
-                    lua.create_function({
-                        let stdout = stdout.clone();
-                        move |_, args: MultiValue| Self::print(args, &stdout)
-                    })?,
-                )?;
-                globals.set(
-                    "search",
-                    lua.create_async_function({
-                        let foreground_fns_tx = foreground_fns_tx.clone();
-                        let fs = fs.clone();
-                        move |lua, regex| {
-                            let mut foreground_fns_tx = foreground_fns_tx.clone();
-                            let fs = fs.clone();
-                            async move {
-                                Self::search(&lua, &mut foreground_fns_tx, fs, regex)
-                                    .await
-                                    .into_lua_err()
-                            }
-                        }
-                    })?,
-                )?;
-                globals.set(
-                    "outline",
-                    lua.create_async_function({
-                        let root_dir = root_dir.clone();
-                        let foreground_fns_tx = foreground_fns_tx.clone();
-                        move |_lua, path| {
-                            let mut foreground_fns_tx = foreground_fns_tx.clone();
-                            let root_dir = root_dir.clone();
-                            async move {
-                                Self::outline(root_dir, &mut foreground_fns_tx, path)
-                                    .await
-                                    .into_lua_err()
-                            }
-                        }
-                    })?,
-                )?;
-                globals.set(
-                    "sb_io_open",
-                    lua.create_async_function({
-                        let worktree_info = worktree_info.clone();
-                        let foreground_fns_tx = foreground_fns_tx.clone();
-                        move |lua, (path_str, mode)| {
-                            let worktree_info = worktree_info.clone();
-                            let mut foreground_fns_tx = foreground_fns_tx.clone();
-                            let fs = fs.clone();
-                            async move {
-                                Self::io_open(
-                                    &lua,
-                                    worktree_info,
-                                    &mut foreground_fns_tx,
-                                    fs,
-                                    path_str,
-                                    mode,
-                                )
-                                .await
-                            }
-                        }
-                    })?,
-                )?;
-                globals.set("user_script", script)?;
-
-                lua.load(SANDBOX_PREAMBLE).exec_async().await?;
-
-                anyhow::Ok(())
-            }
-        });
-
-        task
-    }
-
-    pub fn get(&self, script_id: ScriptId) -> &Script {
-        &self.scripts[script_id.0 as usize]
-    }
-
-    fn get_mut(&mut self, script_id: ScriptId) -> &mut Script {
-        &mut self.scripts[script_id.0 as usize]
-    }
-
-    /// Sandboxed print() function in Lua.
-    fn print(args: MultiValue, stdout: &Mutex<String>) -> mlua::Result<()> {
-        for (index, arg) in args.into_iter().enumerate() {
-            // Lua's `print()` prints tab characters between each argument.
-            if index > 0 {
-                stdout.lock().push('\t');
-            }
-
-            // If the argument's to_string() fails, have the whole function call fail.
-            stdout.lock().push_str(&arg.to_string()?);
-        }
-        stdout.lock().push('\n');
-
-        Ok(())
-    }
-
-    /// Sandboxed io.open() function in Lua.
-    async fn io_open(
-        lua: &Lua,
-        worktree_info: Option<(WorktreeId, Arc<Path>)>,
-        foreground_tx: &mut mpsc::Sender<ForegroundFn>,
-        fs: Arc<dyn Fs>,
-        path_str: String,
-        mode: Option<String>,
-    ) -> mlua::Result<(Option<Table>, String)> {
-        let (worktree_id, root_dir) = worktree_info
-            .ok_or_else(|| mlua::Error::runtime("cannot open file without a root directory"))?;
-
-        let mode = mode.unwrap_or_else(|| "r".to_string());
-
-        // Parse the mode string to determine read/write permissions
-        let read_perm = mode.contains('r');
-        let write_perm = mode.contains('w') || mode.contains('a') || mode.contains('+');
-        let append = mode.contains('a');
-        let truncate = mode.contains('w');
-
-        // This will be the Lua value returned from the `open` function.
-        let file = lua.create_table()?;
-
-        // Store file metadata in the file
-        file.set("__mode", mode.clone())?;
-        file.set("__read_perm", read_perm)?;
-        file.set("__write_perm", write_perm)?;
-
-        let path = match Self::parse_abs_path_in_root_dir(&root_dir, &path_str) {
-            Ok(path) => path,
-            Err(err) => return Ok((None, format!("{err}"))),
-        };
-
-        let project_path = ProjectPath {
-            worktree_id,
-            path: Path::new(&path_str).into(),
-        };
-
-        // flush / close method
-        let flush_fn = {
-            let project_path = project_path.clone();
-            let foreground_tx = foreground_tx.clone();
-            lua.create_async_function(move |_lua, file_userdata: mlua::Table| {
-                let project_path = project_path.clone();
-                let mut foreground_tx = foreground_tx.clone();
-                async move {
-                    Self::io_file_flush(file_userdata, project_path, &mut foreground_tx).await
-                }
-            })?
-        };
-        file.set("flush", flush_fn.clone())?;
-        // We don't really hold files open, so we only need to flush on close
-        file.set("close", flush_fn)?;
-
-        // If it's a directory, give it a custom read() and return early.
-        if fs.is_dir(&path).await {
-            return Self::io_file_dir(lua, fs, file, &path).await;
-        }
-
-        let mut file_content = Vec::new();
-
-        if !truncate {
-            // Try to read existing content if we're not truncating
-            match Self::read_buffer(project_path.clone(), foreground_tx).await {
-                Ok(content) => file_content = content.into_bytes(),
-                Err(e) => return Ok((None, format!("Error reading file: {}", e))),
-            }
-        }
-
-        // If in append mode, position should be at the end
-        let position = if append { file_content.len() } else { 0 };
-        file.set("__position", position)?;
-        file.set(
-            "__content",
-            lua.create_userdata(FileContent(Arc::new(Mutex::new(file_content))))?,
-        )?;
-
-        // Create file methods
-
-        // read method
-        let read_fn = lua.create_function(Self::io_file_read)?;
-        file.set("read", read_fn)?;
-
-        // lines method
-        let lines_fn = lua.create_function(Self::io_file_lines)?;
-        file.set("lines", lines_fn)?;
-
-        // write method
-        let write_fn = lua.create_function(Self::io_file_write)?;
-        file.set("write", write_fn)?;
-
-        // If we got this far, the file was opened successfully
-        Ok((Some(file), String::new()))
-    }
-
-    async fn read_buffer(
-        project_path: ProjectPath,
-        foreground_tx: &mut mpsc::Sender<ForegroundFn>,
-    ) -> anyhow::Result<String> {
-        Self::run_foreground_fn(
-            "read file from buffer",
-            foreground_tx,
-            Box::new(move |session, mut cx| {
-                session.update(&mut cx, |session, cx| {
-                    let open_buffer_task = session
-                        .project
-                        .update(cx, |project, cx| project.open_buffer(project_path, cx));
-
-                    cx.spawn(async move |_, cx| {
-                        let buffer = open_buffer_task.await?;
-
-                        let text = buffer.read_with(cx, |buffer, _cx| buffer.text())?;
-                        Ok(text)
-                    })
-                })
-            }),
-        )
-        .await??
-        .await
-    }
-
-    async fn io_file_flush(
-        file_userdata: mlua::Table,
-        project_path: ProjectPath,
-        foreground_tx: &mut mpsc::Sender<ForegroundFn>,
-    ) -> mlua::Result<bool> {
-        let write_perm = file_userdata.get::<bool>("__write_perm")?;
-
-        if write_perm {
-            let content = file_userdata.get::<mlua::AnyUserData>("__content")?;
-            let content_ref = content.borrow::<FileContent>()?;
-            let text = {
-                let mut content_vec = content_ref.0.lock();
-                let content_vec = std::mem::take(&mut *content_vec);
-                String::from_utf8(content_vec).into_lua_err()?
-            };
-
-            Self::write_to_buffer(project_path, text, foreground_tx)
-                .await
-                .into_lua_err()?;
-        }
-
-        Ok(true)
-    }
-
-    async fn write_to_buffer(
-        project_path: ProjectPath,
-        text: String,
-        foreground_tx: &mut mpsc::Sender<ForegroundFn>,
-    ) -> anyhow::Result<()> {
-        Self::run_foreground_fn(
-            "write to buffer",
-            foreground_tx,
-            Box::new(move |session, mut cx| {
-                session.update(&mut cx, |session, cx| {
-                    let open_buffer_task = session
-                        .project
-                        .update(cx, |project, cx| project.open_buffer(project_path, cx));
-
-                    cx.spawn(async move |session, cx| {
-                        let buffer = open_buffer_task.await?;
-
-                        let diff = buffer.update(cx, |buffer, cx| buffer.diff(text, cx))?.await;
-
-                        let edit_ids = buffer.update(cx, |buffer, cx| {
-                            buffer.finalize_last_transaction();
-                            buffer.apply_diff(diff, cx);
-                            let transaction = buffer.finalize_last_transaction();
-                            transaction
-                                .map_or(Vec::new(), |transaction| transaction.edit_ids.clone())
-                        })?;
-
-                        session
-                            .update(cx, {
-                                let buffer = buffer.clone();
-
-                                |session, cx| {
-                                    session
-                                        .project
-                                        .update(cx, |project, cx| project.save_buffer(buffer, cx))
-                                }
-                            })?
-                            .await?;
-
-                        let snapshot = buffer.read_with(cx, |buffer, _| buffer.snapshot())?;
-
-                        // If we saved successfully, mark buffer as changed
-                        let buffer_without_changes =
-                            buffer.update(cx, |buffer, cx| buffer.branch(cx))?;
-                        session
-                            .update(cx, |session, cx| {
-                                let changed_buffer = session
-                                    .changes_by_buffer
-                                    .entry(buffer)
-                                    .or_insert_with(|| BufferChanges {
-                                        diff: cx.new(|cx| BufferDiff::new(&snapshot, cx)),
-                                        edit_ids: Vec::new(),
-                                    });
-                                changed_buffer.edit_ids.extend(edit_ids);
-                                let operations_to_undo = changed_buffer
-                                    .edit_ids
-                                    .iter()
-                                    .map(|edit_id| (*edit_id, u32::MAX))
-                                    .collect::<HashMap<_, _>>();
-                                buffer_without_changes.update(cx, |buffer, cx| {
-                                    buffer.undo_operations(operations_to_undo, cx);
-                                });
-                                changed_buffer.diff.update(cx, |diff, cx| {
-                                    diff.set_base_text(buffer_without_changes, snapshot.text, cx)
-                                })
-                            })?
-                            .await?;
-
-                        Ok(())
-                    })
-                })
-            }),
-        )
-        .await??
-        .await
-    }
-
-    async fn io_file_dir(
-        lua: &Lua,
-        fs: Arc<dyn Fs>,
-        file: Table,
-        path: &Path,
-    ) -> mlua::Result<(Option<Table>, String)> {
-        // Create a special directory handle
-        file.set("__is_directory", true)?;
-
-        // Store directory entries
-        let entries = match fs.read_dir(&path).await {
-            Ok(entries) => {
-                let mut entry_names = Vec::new();
-
-                // Process the stream of directory entries
-                pin_mut!(entries);
-                while let Some(Ok(entry_result)) = entries.next().await {
-                    if let Some(file_name) = entry_result.file_name() {
-                        entry_names.push(file_name.to_string_lossy().into_owned());
-                    }
-                }
-
-                entry_names
-            }
-            Err(e) => return Ok((None, format!("Error reading directory: {}", e))),
-        };
-
-        // Save the list of entries
-        file.set("__dir_entries", entries)?;
-        file.set("__dir_position", 0usize)?;
-
-        // Create a directory-specific read function
-        let read_fn = lua.create_function(|_lua, file_userdata: mlua::Table| {
-            let position = file_userdata.get::<usize>("__dir_position")?;
-            let entries = file_userdata.get::<Vec<String>>("__dir_entries")?;
-
-            if position >= entries.len() {
-                return Ok(None); // No more entries
-            }
-
-            let entry = entries[position].clone();
-            file_userdata.set("__dir_position", position + 1)?;
-
-            Ok(Some(entry))
-        })?;
-        file.set("read", read_fn)?;
-
-        // If we got this far, the directory was opened successfully
-        return Ok((Some(file), String::new()));
-    }
-
-    fn io_file_read(
-        lua: &Lua,
-        (file_userdata, format): (Table, Option<mlua::Value>),
-    ) -> mlua::Result<Option<mlua::String>> {
-        let read_perm = file_userdata.get::<bool>("__read_perm")?;
-        if !read_perm {
-            return Err(mlua::Error::runtime("File not open for reading"));
-        }
-
-        let content = file_userdata.get::<mlua::AnyUserData>("__content")?;
-        let position = file_userdata.get::<usize>("__position")?;
-        let content_ref = content.borrow::<FileContent>()?;
-        let content = content_ref.0.lock();
-
-        if position >= content.len() {
-            return Ok(None); // EOF
-        }
-
-        let (result, new_position) = match Self::io_file_read_format(format)? {
-            FileReadFormat::All => {
-                // Read entire file from current position
-                let result = content[position..].to_vec();
-                (Some(result), content.len())
-            }
-            FileReadFormat::Line => {
-                if let Some(next_newline_ix) = content[position..].iter().position(|c| *c == b'\n')
-                {
-                    let mut line = content[position..position + next_newline_ix].to_vec();
-                    if line.ends_with(b"\r") {
-                        line.pop();
-                    }
-                    (Some(line), position + next_newline_ix + 1)
-                } else if position < content.len() {
-                    let line = content[position..].to_vec();
-                    (Some(line), content.len())
-                } else {
-                    (None, position) // EOF
-                }
-            }
-            FileReadFormat::LineWithLineFeed => {
-                if position < content.len() {
-                    let next_line_ix = content[position..]
-                        .iter()
-                        .position(|c| *c == b'\n')
-                        .map_or(content.len(), |ix| position + ix + 1);
-                    let line = content[position..next_line_ix].to_vec();
-                    (Some(line), next_line_ix)
-                } else {
-                    (None, position) // EOF
-                }
-            }
-            FileReadFormat::Bytes(n) => {
-                let end = std::cmp::min(position + n, content.len());
-                let result = content[position..end].to_vec();
-                (Some(result), end)
-            }
-        };
-
-        // Update the position in the file userdata
-        if new_position != position {
-            file_userdata.set("__position", new_position)?;
-        }
-
-        // Convert the result to a Lua string
-        match result {
-            Some(bytes) => Ok(Some(lua.create_string(bytes)?)),
-            None => Ok(None),
-        }
-    }
-
-    fn io_file_lines(lua: &Lua, file_userdata: Table) -> mlua::Result<mlua::Function> {
-        let read_perm = file_userdata.get::<bool>("__read_perm")?;
-        if !read_perm {
-            return Err(mlua::Error::runtime("File not open for reading"));
-        }
-
-        lua.create_function::<_, _, mlua::Value>(move |lua, _: ()| {
-            file_userdata.call_method("read", lua.create_string("*l")?)
-        })
-    }
-
-    fn io_file_read_format(format: Option<mlua::Value>) -> mlua::Result<FileReadFormat> {
-        let format = match format {
-            Some(mlua::Value::String(s)) => {
-                let lossy_string = s.to_string_lossy();
-                let format_str: &str = lossy_string.as_ref();
-
-                // Only consider the first 2 bytes, since it's common to pass e.g. "*all"  instead of "*a"
-                match &format_str[0..2] {
-                    "*a" => FileReadFormat::All,
-                    "*l" => FileReadFormat::Line,
-                    "*L" => FileReadFormat::LineWithLineFeed,
-                    "*n" => {
-                        // Try to parse as a number (number of bytes to read)
-                        match format_str.parse::<usize>() {
-                            Ok(n) => FileReadFormat::Bytes(n),
-                            Err(_) => {
-                                return Err(mlua::Error::runtime(format!(
-                                    "Invalid format: {}",
-                                    format_str
-                                )))
-                            }
-                        }
-                    }
-                    _ => {
-                        return Err(mlua::Error::runtime(format!(
-                            "Unsupported format: {}",
-                            format_str
-                        )))
-                    }
-                }
-            }
-            Some(mlua::Value::Number(n)) => FileReadFormat::Bytes(n as usize),
-            Some(mlua::Value::Integer(n)) => FileReadFormat::Bytes(n as usize),
-            Some(value) => {
-                return Err(mlua::Error::runtime(format!(
-                    "Invalid file format {:?}",
-                    value
-                )))
-            }
-            None => FileReadFormat::Line, // Default is to read a line
-        };
-
-        Ok(format)
-    }
-
-    fn io_file_write(
-        _lua: &Lua,
-        (file_userdata, text): (Table, mlua::String),
-    ) -> mlua::Result<bool> {
-        let write_perm = file_userdata.get::<bool>("__write_perm")?;
-        if !write_perm {
-            return Err(mlua::Error::runtime("File not open for writing"));
-        }
-
-        let content = file_userdata.get::<mlua::AnyUserData>("__content")?;
-        let position = file_userdata.get::<usize>("__position")?;
-        let content_ref = content.borrow::<FileContent>()?;
-        let mut content_vec = content_ref.0.lock();
-
-        let bytes = text.as_bytes();
-
-        // Ensure the vector has enough capacity
-        if position + bytes.len() > content_vec.len() {
-            content_vec.resize(position + bytes.len(), 0);
-        }
-
-        // Write the bytes
-        for (i, &byte) in bytes.iter().enumerate() {
-            content_vec[position + i] = byte;
-        }
-
-        // Update position
-        let new_position = position + bytes.len();
-        file_userdata.set("__position", new_position)?;
-
-        Ok(true)
-    }
-
-    async fn search(
-        lua: &Lua,
-        foreground_tx: &mut mpsc::Sender<ForegroundFn>,
-        fs: Arc<dyn Fs>,
-        regex: String,
-    ) -> anyhow::Result<Table> {
-        // TODO: Allow specification of these options.
-        let search_query = SearchQuery::regex(
-            &regex,
-            false,
-            false,
-            false,
-            PathMatcher::default(),
-            PathMatcher::default(),
-            None,
-        );
-        let search_query = match search_query {
-            Ok(query) => query,
-            Err(e) => return Err(anyhow!("Invalid search query: {}", e)),
-        };
-
-        // TODO: Should use `search_query.regex`. The tool description should also be updated,
-        // as it specifies standard regex.
-        let search_regex = match Regex::new(&regex) {
-            Ok(re) => re,
-            Err(e) => return Err(anyhow!("Invalid regex: {}", e)),
-        };
-
-        let mut abs_paths_rx = Self::find_search_candidates(search_query, foreground_tx).await?;
-
-        let mut search_results: Vec<Table> = Vec::new();
-        while let Some(path) = abs_paths_rx.next().await {
-            // Skip files larger than 1MB
-            if let Ok(Some(metadata)) = fs.metadata(&path).await {
-                if metadata.len > 1_000_000 {
-                    continue;
-                }
-            }
-
-            // Attempt to read the file as text
-            if let Ok(content) = fs.load(&path).await {
-                let mut matches = Vec::new();
-
-                // Find all regex matches in the content
-                for capture in search_regex.find_iter(&content) {
-                    matches.push(capture.as_str().to_string());
-                }
-
-                // If we found matches, create a result entry
-                if !matches.is_empty() {
-                    let result_entry = lua.create_table()?;
-                    result_entry.set("path", path.to_string_lossy().to_string())?;
-
-                    let matches_table = lua.create_table()?;
-                    for (ix, m) in matches.iter().enumerate() {
-                        matches_table.set(ix + 1, m.clone())?;
-                    }
-                    result_entry.set("matches", matches_table)?;
-
-                    search_results.push(result_entry);
-                }
-            }
-        }
-
-        // Create a table to hold our results
-        let results_table = lua.create_table()?;
-        for (ix, entry) in search_results.into_iter().enumerate() {
-            results_table.set(ix + 1, entry)?;
-        }
-
-        Ok(results_table)
-    }
-
-    async fn find_search_candidates(
-        search_query: SearchQuery,
-        foreground_tx: &mut mpsc::Sender<ForegroundFn>,
-    ) -> anyhow::Result<mpsc::UnboundedReceiver<PathBuf>> {
-        Self::run_foreground_fn(
-            "finding search file candidates",
-            foreground_tx,
-            Box::new(move |session, mut cx| {
-                session.update(&mut cx, |session, cx| {
-                    session.project.update(cx, |project, cx| {
-                        project.worktree_store().update(cx, |worktree_store, cx| {
-                            // TODO: Better limit? For now this is the same as
-                            // MAX_SEARCH_RESULT_FILES.
-                            let limit = 5000;
-                            // TODO: Providing non-empty open_entries can make this a bit more
-                            // efficient as it can skip checking that these paths are textual.
-                            let open_entries = HashSet::default();
-                            let candidates = worktree_store.find_search_candidates(
-                                search_query,
-                                limit,
-                                open_entries,
-                                project.fs().clone(),
-                                cx,
-                            );
-                            let (abs_paths_tx, abs_paths_rx) = mpsc::unbounded();
-                            cx.spawn(async move |worktree_store, cx| {
-                                pin_mut!(candidates);
-
-                                while let Some(project_path) = candidates.next().await {
-                                    worktree_store.read_with(cx, |worktree_store, cx| {
-                                        if let Some(worktree) = worktree_store
-                                            .worktree_for_id(project_path.worktree_id, cx)
-                                        {
-                                            if let Some(abs_path) = worktree
-                                                .read(cx)
-                                                .absolutize(&project_path.path)
-                                                .log_err()
-                                            {
-                                                abs_paths_tx.unbounded_send(abs_path)?;
-                                            }
-                                        }
-                                        anyhow::Ok(())
-                                    })??;
-                                }
-                                anyhow::Ok(())
-                            })
-                            .detach();
-                            abs_paths_rx
-                        })
-                    })
-                })
-            }),
-        )
-        .await?
-    }
-
-    async fn outline(
-        root_dir: Option<Arc<Path>>,
-        foreground_tx: &mut mpsc::Sender<ForegroundFn>,
-        path_str: String,
-    ) -> anyhow::Result<String> {
-        let root_dir = root_dir
-            .ok_or_else(|| mlua::Error::runtime("cannot get outline without a root directory"))?;
-        let path = Self::parse_abs_path_in_root_dir(&root_dir, &path_str)?;
-        let outline = Self::run_foreground_fn(
-            "getting code outline",
-            foreground_tx,
-            Box::new(move |session, cx| {
-                cx.spawn(async move |cx| {
-                    // TODO: This will not use file content from `fs_changes`. It will also reflect
-                    // user changes that have not been saved.
-                    let buffer = session
-                        .update(cx, |session, cx| {
-                            session
-                                .project
-                                .update(cx, |project, cx| project.open_local_buffer(&path, cx))
-                        })?
-                        .await?;
-                    buffer.update(cx, |buffer, _cx| {
-                        if let Some(outline) = buffer.snapshot().outline(None) {
-                            Ok(outline)
-                        } else {
-                            Err(anyhow!("No outline for file {path_str}"))
-                        }
-                    })
-                })
-            }),
-        )
-        .await?
-        .await??;
-
-        Ok(outline
-            .items
-            .into_iter()
-            .map(|item| {
-                if item.text.contains('\n') {
-                    log::error!("Outline item unexpectedly contains newline");
-                }
-                format!("{}{}", "  ".repeat(item.depth), item.text)
-            })
-            .collect::<Vec<String>>()
-            .join("\n"))
-    }
-
-    async fn run_foreground_fn<R: Send + 'static>(
-        description: &str,
-        foreground_tx: &mut mpsc::Sender<ForegroundFn>,
-        function: Box<dyn FnOnce(WeakEntity<Self>, AsyncApp) -> R + Send>,
-    ) -> anyhow::Result<R> {
-        let (response_tx, response_rx) = oneshot::channel();
-        let send_result = foreground_tx
-            .send(ForegroundFn(Box::new(move |this, cx| {
-                response_tx.send(function(this, cx)).ok();
-            })))
-            .await;
-        match send_result {
-            Ok(()) => (),
-            Err(err) => {
-                return Err(anyhow::Error::new(err).context(format!(
-                    "Internal error while enqueuing work for {description}"
-                )));
-            }
-        }
-        match response_rx.await {
-            Ok(result) => Ok(result),
-            Err(oneshot::Canceled) => Err(anyhow!(
-                "Internal error: response oneshot was canceled while {description}."
-            )),
-        }
-    }
-
-    fn parse_abs_path_in_root_dir(root_dir: &Path, path_str: &str) -> anyhow::Result<PathBuf> {
-        let path = Path::new(&path_str);
-        if path.is_absolute() {
-            // Check if path starts with root_dir prefix without resolving symlinks
-            if path.starts_with(&root_dir) {
-                Ok(path.to_path_buf())
-            } else {
-                Err(anyhow!(
-                    "Error: Absolute path {} is outside the current working directory",
-                    path_str
-                ))
-            }
-        } else {
-            // TODO: Does use of `../` break sandbox - is path canonicalization needed?
-            Ok(root_dir.join(path))
-        }
-    }
-}
-
-enum FileReadFormat {
-    All,
-    Line,
-    LineWithLineFeed,
-    Bytes(usize),
-}
-
-struct FileContent(Arc<Mutex<Vec<u8>>>);
-
-impl UserData for FileContent {
-    fn add_methods<M: UserDataMethods<Self>>(_methods: &mut M) {
-        // FileContent doesn't have any methods so far.
-    }
-}
-
-#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
-pub struct ScriptId(u32);
-
-pub struct Script {
-    pub state: ScriptState,
-}
-
-#[derive(Debug)]
-pub enum ScriptState {
-    Running {
-        stdout: Arc<Mutex<String>>,
-    },
-    Succeeded {
-        stdout: String,
-    },
-    Failed {
-        stdout: String,
-        error: anyhow::Error,
-    },
-}
-
-impl Script {
-    /// If exited, returns a message with the output for the LLM
-    pub fn output_message_for_llm(&self) -> Option<String> {
-        match &self.state {
-            ScriptState::Running { .. } => None,
-            ScriptState::Succeeded { stdout } => {
-                format!("Here's the script output:\n{}", stdout).into()
-            }
-            ScriptState::Failed { stdout, error } => format!(
-                "The script failed with:\n{}\n\nHere's the output it managed to print:\n{}",
-                error, stdout
-            )
-            .into(),
-        }
-    }
-
-    /// Get a snapshot of the script's stdout
-    pub fn stdout_snapshot(&self) -> String {
-        match &self.state {
-            ScriptState::Running { stdout } => stdout.lock().clone(),
-            ScriptState::Succeeded { stdout } => stdout.clone(),
-            ScriptState::Failed { stdout, .. } => stdout.clone(),
-        }
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use gpui::TestAppContext;
-    use project::FakeFs;
-    use serde_json::json;
-    use settings::SettingsStore;
-    use util::path;
-
-    use super::*;
-
-    #[gpui::test]
-    async fn test_print(cx: &mut TestAppContext) {
-        let script = r#"
-            print("Hello", "world!")
-            print("Goodbye", "moon!")
-        "#;
-
-        let test_session = TestSession::init(cx).await;
-        let output = test_session.test_success(script, cx).await;
-        assert_eq!(output, "Hello\tworld!\nGoodbye\tmoon!\n");
-    }
-
-    // search
-
-    #[gpui::test]
-    async fn test_search(cx: &mut TestAppContext) {
-        let script = r#"
-            local results = search("world")
-            for i, result in ipairs(results) do
-                print("File: " .. result.path)
-                print("Matches:")
-                for j, match in ipairs(result.matches) do
-                    print("  " .. match)
-                end
-            end
-        "#;
-
-        let test_session = TestSession::init(cx).await;
-        let output = test_session.test_success(script, cx).await;
-        assert_eq!(
-            output,
-            concat!("File: ", path!("/file1.txt"), "\nMatches:\n  world\n")
-        );
-    }
-
-    // io.open
-
-    #[gpui::test]
-    async fn test_open_and_read_file(cx: &mut TestAppContext) {
-        let script = r#"
-            local file = io.open("file1.txt", "r")
-            local content = file:read()
-            print("Content:", content)
-            file:close()
-        "#;
-
-        let test_session = TestSession::init(cx).await;
-        let output = test_session.test_success(script, cx).await;
-        assert_eq!(output, "Content:\tHello world!\n");
-        assert_eq!(test_session.diff(cx), Vec::new());
-    }
-
-    #[gpui::test]
-    async fn test_lines_iterator(cx: &mut TestAppContext) {
-        let script = r#"
-            -- Create a test file with multiple lines
-            local file = io.open("lines_test.txt", "w")
-            file:write("Line 1\nLine 2\nLine 3\nLine 4\nLine 5")
-            file:close()
-
-            -- Read it back using the lines iterator
-            local read_file = io.open("lines_test.txt", "r")
-            local count = 0
-            for line in read_file:lines() do
-                count = count + 1
-                print(count .. ": " .. line)
-            end
-            read_file:close()
-
-            print("Total lines:", count)
-        "#;
-
-        let test_session = TestSession::init(cx).await;
-        let output = test_session.test_success(script, cx).await;
-        assert_eq!(
-            output,
-            "1: Line 1\n2: Line 2\n3: Line 3\n4: Line 4\n5: Line 5\nTotal lines:\t5\n"
-        );
-    }
-
-    #[gpui::test]
-    async fn test_read_write_roundtrip(cx: &mut TestAppContext) {
-        let script = r#"
-            local file = io.open("file1.txt", "w")
-            file:write("This is new content")
-            file:close()
-
-            -- Read back to verify
-            local read_file = io.open("file1.txt", "r")
-            local content = read_file:read("*a")
-            print("Written content:", content)
-            read_file:close()
-        "#;
-
-        let test_session = TestSession::init(cx).await;
-        let output = test_session.test_success(script, cx).await;
-        assert_eq!(output, "Written content:\tThis is new content\n");
-        assert_eq!(
-            test_session.diff(cx),
-            vec![(
-                PathBuf::from("file1.txt"),
-                vec![(
-                    "Hello world!\n".to_string(),
-                    "This is new content".to_string()
-                )]
-            )]
-        );
-    }
-
-    #[gpui::test]
-    async fn test_multiple_writes(cx: &mut TestAppContext) {
-        let script = r#"
-            -- Test writing to a file multiple times
-            local file = io.open("multiwrite.txt", "w")
-            file:write("First line\n")
-            file:write("Second line\n")
-            file:write("Third line")
-            file:close()
-
-            -- Read back to verify
-            local read_file = io.open("multiwrite.txt", "r")
-            if read_file then
-                local content = read_file:read("*a")
-                print("Full content:", content)
-                read_file:close()
-            end
-        "#;
-
-        let test_session = TestSession::init(cx).await;
-        let output = test_session.test_success(script, cx).await;
-        assert_eq!(
-            output,
-            "Full content:\tFirst line\nSecond line\nThird line\n"
-        );
-        assert_eq!(
-            test_session.diff(cx),
-            vec![(
-                PathBuf::from("multiwrite.txt"),
-                vec![(
-                    "".to_string(),
-                    "First line\nSecond line\nThird line".to_string()
-                )]
-            )]
-        );
-    }
-
-    #[gpui::test]
-    async fn test_multiple_writes_diff_handles(cx: &mut TestAppContext) {
-        let script = r#"
-            -- Write to a file
-            local file1 = io.open("multi_open.txt", "w")
-            file1:write("Content written by first handle\n")
-            file1:close()
-
-            -- Open it again and add more content
-            local file2 = io.open("multi_open.txt", "w")
-            file2:write("Content written by second handle\n")
-            file2:close()
-
-            -- Open it a third time and read
-            local file3 = io.open("multi_open.txt", "r")
-            local content = file3:read("*a")
-            print("Final content:", content)
-            file3:close()
-        "#;
-
-        let test_session = TestSession::init(cx).await;
-        let output = test_session.test_success(script, cx).await;
-        assert_eq!(
-            output,
-            "Final content:\tContent written by second handle\n\n"
-        );
-        assert_eq!(
-            test_session.diff(cx),
-            vec![(
-                PathBuf::from("multi_open.txt"),
-                vec![(
-                    "".to_string(),
-                    "Content written by second handle\n".to_string()
-                )]
-            )]
-        );
-    }
-
-    #[gpui::test]
-    async fn test_append_mode(cx: &mut TestAppContext) {
-        let script = r#"
-            -- Append more content
-            file = io.open("file1.txt", "a")
-            file:write("Appended content\n")
-            file:close()
-
-            -- Add even more
-            file = io.open("file1.txt", "a")
-            file:write("More appended content")
-            file:close()
-
-            -- Read back to verify
-            local read_file = io.open("file1.txt", "r")
-            local content = read_file:read("*a")
-            print("Content after appends:", content)
-            read_file:close()
-        "#;
-
-        let test_session = TestSession::init(cx).await;
-        let output = test_session.test_success(script, cx).await;
-        assert_eq!(
-            output,
-            "Content after appends:\tHello world!\nAppended content\nMore appended content\n"
-        );
-        assert_eq!(
-            test_session.diff(cx),
-            vec![(
-                PathBuf::from("file1.txt"),
-                vec![(
-                    "".to_string(),
-                    "Appended content\nMore appended content".to_string()
-                )]
-            )]
-        );
-    }
-
-    #[gpui::test]
-    async fn test_read_formats(cx: &mut TestAppContext) {
-        let script = r#"
-            local file = io.open("multiline.txt", "w")
-            file:write("Line 1\nLine 2\nLine 3")
-            file:close()
-
-            -- Test "*a" (all)
-            local f = io.open("multiline.txt", "r")
-            local all = f:read("*a")
-            print("All:", all)
-            f:close()
-
-            -- Test "*l" (line)
-            f = io.open("multiline.txt", "r")
-            local line1 = f:read("*l")
-            local line2 = f:read("*l")
-            local line3 = f:read("*l")
-            print("Line 1:", line1)
-            print("Line 2:", line2)
-            print("Line 3:", line3)
-            f:close()
-
-            -- Test "*L" (line with newline)
-            f = io.open("multiline.txt", "r")
-            local line_with_nl = f:read("*L")
-            print("Line with newline length:", #line_with_nl)
-            print("Last char:", string.byte(line_with_nl, #line_with_nl))
-            f:close()
-
-            -- Test number of bytes
-            f = io.open("multiline.txt", "r")
-            local bytes5 = f:read(5)
-            print("5 bytes:", bytes5)
-            f:close()
-        "#;
-
-        let test_session = TestSession::init(cx).await;
-        let output = test_session.test_success(script, cx).await;
-        println!("{}", &output);
-        assert!(output.contains("All:\tLine 1\nLine 2\nLine 3"));
-        assert!(output.contains("Line 1:\tLine 1"));
-        assert!(output.contains("Line 2:\tLine 2"));
-        assert!(output.contains("Line 3:\tLine 3"));
-        assert!(output.contains("Line with newline length:\t7"));
-        assert!(output.contains("Last char:\t10")); // LF
-        assert!(output.contains("5 bytes:\tLine "));
-        assert_eq!(
-            test_session.diff(cx),
-            vec![(
-                PathBuf::from("multiline.txt"),
-                vec![("".to_string(), "Line 1\nLine 2\nLine 3".to_string())]
-            )]
-        );
-    }
-
-    // helpers
-
-    struct TestSession {
-        session: Entity<ScriptingSession>,
-    }
-
-    impl TestSession {
-        async fn init(cx: &mut TestAppContext) -> Self {
-            let settings_store = cx.update(SettingsStore::test);
-            cx.set_global(settings_store);
-            cx.update(Project::init_settings);
-            cx.update(language::init);
-
-            let fs = FakeFs::new(cx.executor());
-            fs.insert_tree(
-                path!("/"),
-                json!({
-                    "file1.txt": "Hello world!\n",
-                    "file2.txt": "Goodbye moon!\n"
-                }),
-            )
-            .await;
-
-            let project = Project::test(fs.clone(), [Path::new(path!("/"))], cx).await;
-            let session = cx.new(|cx| ScriptingSession::new(project, cx));
-
-            TestSession { session }
-        }
-
-        async fn test_success(&self, source: &str, cx: &mut TestAppContext) -> String {
-            let script_id = self.run_script(source, cx).await;
-
-            self.session.read_with(cx, |session, _cx| {
-                let script = session.get(script_id);
-                let stdout = script.stdout_snapshot();
-
-                if let ScriptState::Failed { error, .. } = &script.state {
-                    panic!("Script failed:\n{}\n\n{}", error, stdout);
-                }
-
-                stdout
-            })
-        }
-
-        fn diff(&self, cx: &mut TestAppContext) -> Vec<(PathBuf, Vec<(String, String)>)> {
-            self.session.read_with(cx, |session, cx| {
-                session
-                    .changes_by_buffer
-                    .iter()
-                    .map(|(buffer, changes)| {
-                        let snapshot = buffer.read(cx).snapshot();
-                        let diff = changes.diff.read(cx);
-                        let hunks = diff.hunks(&snapshot, cx);
-                        let path = buffer.read(cx).file().unwrap().path().clone();
-                        let diffs = hunks
-                            .map(|hunk| {
-                                let old_text = diff
-                                    .base_text()
-                                    .text_for_range(hunk.diff_base_byte_range)
-                                    .collect::<String>();
-                                let new_text =
-                                    snapshot.text_for_range(hunk.range).collect::<String>();
-                                (old_text, new_text)
-                            })
-                            .collect();
-                        (path.to_path_buf(), diffs)
-                    })
-                    .collect()
-            })
-        }
-
-        async fn run_script(&self, source: &str, cx: &mut TestAppContext) -> ScriptId {
-            let (script_id, task) = self
-                .session
-                .update(cx, |session, cx| session.run_script(source.to_string(), cx));
-
-            task.await;
-
-            script_id
-        }
-    }
-}

crates/scripting_tool/src/scripting_tool.rs 🔗

@@ -1,30 +0,0 @@
-mod scripting_session;
-
-pub use scripting_session::*;
-
-use schemars::JsonSchema;
-use serde::Deserialize;
-
-#[derive(Debug, Deserialize, JsonSchema)]
-pub struct ScriptingToolInput {
-    pub lua_script: String,
-}
-
-pub struct ScriptingTool;
-
-impl ScriptingTool {
-    pub const NAME: &str = "lua-interpreter";
-
-    pub const DESCRIPTION: &str = include_str!("scripting_tool_description.md");
-
-    pub fn input_schema() -> serde_json::Value {
-        let schema = schemars::schema_for!(ScriptingToolInput);
-        serde_json::to_value(&schema).unwrap()
-    }
-
-    pub fn deserialize_input(
-        input: serde_json::Value,
-    ) -> Result<ScriptingToolInput, serde_json::Error> {
-        serde_json::from_value(input)
-    }
-}

crates/scripting_tool/src/scripting_tool_description.md 🔗

@@ -1,21 +0,0 @@
-Evaluates the given Lua script in an interpreter with access to the Lua standard library. The tool returns the scripts output to stdout and any error that may have occurred.
-
-Use this tool to explore the current project and edit the user's codebase or operating system as requested.
-
-Additional functions provided:
-
-```lua
---- Search for matches of a regular expression in files.
--- @param pattern The regex pattern to search for (uses Rust's regex syntax)
--- @return An array of tables with 'path' (file path) and 'matches' (array of matching strings)
--- @usage local results = search("function\\s+\\w+")
-function search(pattern)
-  -- Implementation provided by the tool
-end
-
---- Generates an outline for the given file path, extracting top-level symbols such as functions, classes, exports, and other significant declarations. This provides a structural overview of the file's contents.
--- @param path
-function outline(path)
-  -- Implementation provided by the tool
-end
-```