Detailed changes
@@ -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"
@@ -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" }
@@ -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
@@ -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,
@@ -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,
@@ -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()]);
}
}
});
@@ -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)]
@@ -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;
}
}
@@ -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"] }
@@ -1 +0,0 @@
-../../LICENSE-GPL
@@ -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
@@ -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(
- ®ex,
- 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(®ex) {
- 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
- }
- }
-}
@@ -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)
- }
-}
@@ -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
-```