diff --git a/crates/assistant/src/slash_command/auto_command.rs b/crates/assistant/src/slash_command/auto_command.rs index 3ff169dcdd..0a02f12adb 100644 --- a/crates/assistant/src/slash_command/auto_command.rs +++ b/crates/assistant/src/slash_command/auto_command.rs @@ -1,11 +1,10 @@ +use super::create_label_for_command; use super::{SlashCommand, SlashCommandOutput}; use crate::{CompletionProvider, LanguageModelRequest, LanguageModelRequestMessage, Role}; -use anyhow::anyhow; -use anyhow::Result; -use futures::FutureExt; +use anyhow::{anyhow, Result}; use futures::StreamExt; use gpui::{AppContext, Task, WeakView}; -use language::LspAdapterDelegate; +use language::{CodeLabel, LspAdapterDelegate}; use std::sync::{atomic::AtomicBool, Arc}; use ui::WindowContext; use workspace::Workspace; @@ -25,32 +24,42 @@ impl SlashCommand for AutoCommand { "Automatically Infer Context".into() } - fn requires_argument(&self) -> bool { - false + fn label(&self, cx: &AppContext) -> CodeLabel { + create_label_for_command("auto", &["--prompt"], cx) } fn complete_argument( self: Arc, _query: String, - _cancel: Arc, + _cancellation_flag: Arc, _workspace: Option>, _cx: &mut AppContext, ) -> Task>> { - Task::ready(Err(anyhow!("this command does not require argument"))) + // There's no autocomplete for a prompt, since it's arbitrary text. + Task::ready(Ok(Vec::new())) + } + + fn requires_argument(&self) -> bool { + true } fn run( self: Arc, - _argument: Option<&str>, + argument: Option<&str>, _workspace: WeakView, _delegate: Arc, cx: &mut WindowContext, ) -> Task> { + let Some(argument) = argument else { + return Task::ready(Err(anyhow!("missing prompt"))); + }; + + let prompt = format!("{PROMPT_INSTRUCTIONS_BEFORE_SUMMARY}\n{SUMMARY}\n{PROMPT_INSTRUCTIONS_AFTER_SUMMARY}\n{argument}"); let request = LanguageModelRequest { model: CompletionProvider::global(cx).model(), messages: vec![LanguageModelRequestMessage { role: Role::User, - content: "please tell me a story".into(), + content: prompt, }], stop: vec![], temperature: 1.0, @@ -58,26 +67,117 @@ impl SlashCommand for AutoCommand { let stream = CompletionProvider::global(cx).complete(request); - cx.spawn(|_cx| async move { + println!("/auto {argument}"); + + let mut wip_action: String = String::new(); + let task = cx.spawn(|_cx| async move { let stream_completion = async { let mut messages = stream.await?; while let Some(message) = messages.next().await { let text = message?; - dbg!(&text); + chunked_line(&mut wip_action, &text, |line| { + println!("Running action: /{line}") + }); smol::future::yield_now().await; } + println!("End of actions."); + anyhow::Ok(()) }; - let result = stream_completion.await; - - dbg!(&result); + stream_completion.await }); - Task::ready(Ok(SlashCommandOutput::default())) + cx.background_executor().spawn(async move { + task.await?; + Ok(SlashCommandOutput::default()) + }) + } +} +const PROMPT_INSTRUCTIONS_BEFORE_SUMMARY: &str = r#" +I'm going to give you a prompt. I don't want you to respond +to the prompt itself. I want you to figure out which of the following +actions on my project, if any, would help you answer the prompt. + +Here are the actions: + +## file + +This action's parameter is a file path to one of the files +in the project. If you ask for this action, I will tell you +the full contents of the file, so you can learn all the +details of the file. + +## search + +This action's parameter is a string to search for across +the project. It will tell you which files this string +(or similar strings; it is a semantic search) appear in, +as well as some context of the lines surrounding each result. + +--- + +That was the end of the list of actions. + +Here is an XML summary of each of the files in my project: +"#; + +const PROMPT_INSTRUCTIONS_AFTER_SUMMARY: &str = r#" +Actions have a cost, so only include actions that you think +will be helpful to you in doing a great job answering the +prompt in the future. + +You must respond ONLY with a list of actions you would like to +perform. Each action should be on its own line, and followed by a space and then its parameter. + +Actions can be performed more than once with different parameters. +Here is an example valid response: + +``` +file path/to/my/file.txt +file path/to/another/file.txt +search something to search for +search something else to search for +``` + +Once again, do not forget: you must respond ONLY in the format of +one action per line, and the action name should be followed by +its parameter. Your response must not include anything other +than a list of actions, with one action per line, in this format. +It is extremely important that you do not deviate from this format even slightly! + +This is the end of my instructions for how to respond. The rest is the prompt: +"#; + +const SUMMARY: &str = ""; + +fn chunked_line(wip: &mut String, chunk: &str, mut on_line_end: impl FnMut(&str)) { + // The first iteration of the loop should just push to wip + // and nothing else. We only push what we encountered in + // previous iterations of the loop. + // + // This correctly handles both the scenario where no + // newlines are encountered (the loop will only run once, + // and so will only push to wip), as well as the scenario + // where the chunk contains at least one newline but + // does not end in a newline (the last iteration of the + // loop will update wip but will not run anything). + let mut is_first_iteration = true; + + for line in chunk.split('\n') { + if is_first_iteration { + is_first_iteration = false; + } else { + // Since this isn't the first iteration of the loop, we definitely hit a newline + // at the end of the previous iteration! Run the function on whatever wip we have. + on_line_end(wip); + wip.clear(); + } + + wip.push_str(line); } }