Compare commits

...

1 Commits

Author SHA1 Message Date
Michael Sloan
3ae1966432 Prototype of project creation flow in assistant
Co-authored-by: Anthony <anthony@zed.dev>
Co-authored-by: Thomas <thomas@zed.dev>
2025-02-27 15:30:46 -07:00
8 changed files with 502 additions and 36 deletions

View File

@@ -42,6 +42,7 @@ gpui.workspace = true
heed.workspace = true
html_to_markdown.workspace = true
http_client.workspace = true
indoc.workspace = true
itertools.workspace = true
language.workspace = true
language_model.workspace = true

View File

@@ -1,5 +1,7 @@
use std::path::PathBuf;
use std::sync::Arc;
use anyhow::anyhow;
use assistant_tool::ToolWorkingSet;
use collections::HashMap;
use gpui::{
@@ -9,11 +11,14 @@ use gpui::{
};
use language::LanguageRegistry;
use language_model::Role;
use markdown::parser::{CodeBlockKind, MarkdownEvent, MarkdownTag, MarkdownTagEnd};
use markdown::{Markdown, MarkdownStyle};
use settings::Settings as _;
use text::Rope;
use theme::ThemeSettings;
use ui::prelude::*;
use workspace::Workspace;
use util::ResultExt;
use workspace::{create_and_open_local_file, Workspace};
use crate::thread::{MessageId, Thread, ThreadError, ThreadEvent};
use crate::thread_store::ThreadStore;
@@ -103,6 +108,131 @@ impl ActiveThread {
self.last_error.take();
}
pub fn create_files(&self, window: &mut Window, cx: &mut App) {
let Some(workspace) = self.workspace.upgrade() else {
return;
};
let messages = self.thread.read(cx).messages();
let mut files_to_create = Vec::new();
for message in messages {
if message.role != Role::Assistant {
continue;
}
let Some(rendered_message) = self.rendered_messages_by_id.get(&message.id) else {
log::error!("Missing rendered message for ID: {:?}", message.id);
continue;
};
let mut in_file_path_block = false;
let mut code_block_to_create: Option<Rope> = None;
let mut prior_file_path: Option<String> = None;
for (_range, event) in rendered_message.read(cx).parsed_markdown().events().iter() {
match event {
MarkdownEvent::Start(MarkdownTag::CodeBlock(CodeBlockKind::Fenced(
language,
))) => {
if language == "file_path" {
in_file_path_block = true;
prior_file_path = None;
} else if prior_file_path.as_ref().is_some() {
code_block_to_create = Some(Rope::new());
}
}
MarkdownEvent::Text(text) => {
if in_file_path_block {
prior_file_path = Some(
(prior_file_path.unwrap_or("".to_string()) + &text).to_string(),
);
} else {
match code_block_to_create.as_mut() {
Some(code_block) => {
code_block.push(text);
}
None => {}
}
}
}
MarkdownEvent::End(MarkdownTagEnd::CodeBlock) => {
in_file_path_block = false;
match (&prior_file_path, &code_block_to_create) {
(Some(file_path), Some(code)) => {
let file_path = file_path.trim().to_string();
let file_path = PathBuf::from(
file_path
.strip_prefix("/")
.unwrap_or(&file_path)
.to_string(),
);
files_to_create.push((file_path, code.clone()));
}
_ => {}
}
}
_ => {}
}
}
}
let weak_workspace = workspace.downgrade();
window.defer(cx, move |window, cx| {
let resolved_files_to_create = weak_workspace
.update(cx, |workspace, cx| {
let Some(first_worktree) = workspace.worktrees(cx).next() else {
return Err(anyhow!("No worktree found"));
};
let first_worktree = first_worktree.read(cx).snapshot();
let mut resolved_files_to_create = Vec::new();
for (file_path, code) in files_to_create {
let abs_path = first_worktree.absolutize(file_path.as_path())?;
resolved_files_to_create.push((abs_path, code));
}
return Ok(resolved_files_to_create);
})
.log_err()
.and_then(|inner| inner.log_err());
let Some(resolved_files_to_create) = resolved_files_to_create else {
return;
};
let Some(fs) = weak_workspace
.update(cx, |workspace, _cx| workspace.app_state().fs.clone())
.log_err()
else {
return;
};
window
.spawn(cx, move |mut cx| async move {
for (abs_path, _) in resolved_files_to_create.iter() {
let parent = abs_path.parent().map(|parent| parent.to_path_buf());
if let Some(parent) = parent {
fs.create_dir(parent.as_path()).await?;
}
}
for (abs_path, code) in resolved_files_to_create {
weak_workspace
.update_in(&mut cx, |_, window, cx| {
create_and_open_local_file(abs_path, window, cx, move || {
code.clone()
})
.detach_and_log_err(cx);
})
.log_err();
}
anyhow::Ok(())
})
.detach_and_log_err(cx);
})
}
fn push_message(
&mut self,
id: &MessageId,

View File

@@ -45,6 +45,7 @@ actions!(
RemoveSelectedThread,
Chat,
ChatMode,
CreateFiles,
CycleNextInlineAssist,
CyclePreviousInlineAssist,
FocusUp,

View File

@@ -36,7 +36,9 @@ use crate::message_editor::MessageEditor;
use crate::thread::{Thread, ThreadError, ThreadId};
use crate::thread_history::{PastContext, PastThread, ThreadHistory};
use crate::thread_store::ThreadStore;
use crate::{InlineAssistant, NewPromptEditor, NewThread, OpenConfiguration, OpenHistory};
use crate::{
CreateFiles, InlineAssistant, NewPromptEditor, NewThread, OpenConfiguration, OpenHistory,
};
pub fn init(cx: &mut App) {
cx.observe_new(
@@ -65,6 +67,16 @@ pub fn init(cx: &mut App) {
workspace.focus_panel::<AssistantPanel>(window, cx);
panel.update(cx, |panel, cx| panel.open_configuration(window, cx));
}
})
.register_action(|workspace, _: &CreateFiles, window, cx| {
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
workspace.focus_panel::<AssistantPanel>(window, cx);
panel.update(cx, |panel, cx| {
panel
.thread
.update(cx, |thread, cx| thread.create_files(window, cx))
});
}
});
},
)

View File

@@ -2,11 +2,13 @@ use std::sync::Arc;
use editor::actions::MoveUp;
use editor::{Editor, EditorElement, EditorEvent, EditorStyle};
use feature_flags::FeatureFlagAppExt;
use fs::Fs;
use gpui::{
pulsating_between, Animation, AnimationExt, App, DismissEvent, Entity, Focusable, Subscription,
TextStyle, WeakEntity,
};
use indoc::indoc;
use language_model::{LanguageModelRegistry, LanguageModelRequestTool};
use language_model_selector::LanguageModelSelector;
use rope::Point;
@@ -25,7 +27,274 @@ use crate::context_store::{refresh_context_store_text, ContextStore};
use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
use crate::thread::{RequestKind, Thread};
use crate::thread_store::ThreadStore;
use crate::{Chat, ChatMode, RemoveAllContext, ToggleContextPicker, ToggleModelSelector};
use crate::{
Chat, ChatMode, CreateFiles, RemoveAllContext, ToggleContextPicker, ToggleModelSelector,
};
const CREATE_PROJECT_PROMPT: &str = indoc! {"
You are a **senior software engineer** with expertise in multiple programming languages, frameworks, libraries, and software architecture.
Your role is to **interpret user requests, plan a structured project layout, and return production-ready code** in a well-defined format.
Your solutions must be:
- **Scalable** Use modular, maintainable, and well-organized code.
- **Secure** Follow security best practices for the given language and context.
- **Efficient** Ensure optimized performance while keeping code readable.
- **Idiomatic** Follow conventions of the language, using proper formatting and structure.
---
## **Instructions for Code Generation**
### **1. Understand and Clarify the Request**
- Identify the **programming language(s), framework(s), and dependencies**.
- Recognize whether the request implies a **specific architecture or design pattern**.
- If the request is **ambiguous or lacks details**, ask clarifying questions before proceeding.
### **2. Request More Details if Needed**
If the user request is unclear, **DO NOT make assumptions**. Instead, ask targeted follow-up questions to clarify:
- \"Would you like this structured as a package, module, or script?\"
- \"Should this include authentication/security features?\"
- \"Are there any preferred frameworks or libraries for this?\"
- \"Should this support a specific database or storage mechanism?\"
- \"Is this for production, testing, or prototyping?\"
Only proceed with assumptions if no clarification is given.
### **3. Plan the Project Layout**
- Define necessary **directories and file structure** based on the request.
- Use **standard naming conventions** and organize files logically.
### **4. Generate Code**
- Write **clean, idiomatic, and well-documented** code.
- Include **proper error handling and logging**.
- Follow the **language's best practices and linting rules**.
### **5. Return Only Structured Output**
- Output files, paths, and code strictly within markdown code blocks with language block names.
- Use the standardized format **without additional commentary**.
- Ensure all **bash scripts and shell commands** are formatted properly.
### **6 Best Code Assistant**
- If the user does not specify a specific framework or library, use a popular and widely-used option.
- Keep the user in the loop around all details of the project, including any assumptions made.
- Provide clear and concise documentation for the generated code so the user can change the language or framework used if needed.
---
## **Output Format**
Each file should be structured as follows:
```file_path
/path/to/file.ext
```
```language
# Code content here
```
```text
# Explanation (if necessary)
```
```bash
# Commands to run the project
```
---
## **Rules and Constraints**
- Always return a structured response using the format above.
- **DO NOT** include unnecessary explanations unless required.
- **DO NOT** assume missing details—ask the user for clarification first.
- Ensure the project follows **best coding practices** for maintainability and performance.
- All **dependencies and installation steps** must be provided in the output.
---
## Follow-up Question Examples
- What programming language should I use for my project?
- What framework should I use for my project?
- What libraries should I use for my project?
- What are some best practices for optimizing performance in my project?
---
## **Example Scenarios**
### **Example 1: Python Fibonacci Package**
#### **User Request:** \"Create a Python package with a Fibonacci function.\"
#### **Output:**
```file_path
/README.md
```
```markdown
# Python Fibonacci Package
This package provides a function to generate Fibonacci sequences.
```
```file_path
/fibonacci.py
```
```python
def fibonacci(n):
\"\"\"Returns a list containing the Fibonacci sequence up to n terms.\"\"\"
if n <= 0:
return []
elif n == 1:
return [0]
sequence = [0, 1]
for i in range(2, n):
next_num = sequence[i - 1] + sequence[i - 2]
sequence.append(next_num)
return sequence
```
```file_path
/main.py
```
```python
from fibonacci import fibonacci
print(fibonacci(10))
```
```bash
python main.py
```
---
### **Example 2: Rust Project with Multiple Files**
#### **User Request:** \"Create a multi-file Rust project with modular structure.\"
#### **Output:**
```file_path
/Cargo.toml
```
```toml
[package]
name = \"rust_project\"
version = \"0.1.0\"
edition = \"2021\"
[dependencies]
```
```file_path
/README.md
```
```markdown
# Rust Project
A multi-file Rust project demonstrating modular design.
```
```file_path
/src/main.rs
```
```rust
mod lib;
mod utils;
fn main() {
println!(\"Starting Rust project...\");
let result = lib::fibonacci(10);
println!(\"Fibonacci sequence: {:?}\", result);
utils::print_message();
}
```
```file_path
/src/lib.rs
```
```rust
/// Returns a vector containing the Fibonacci sequence up to n terms.
pub fn fibonacci(n: usize) -> Vec<u64> {
if n == 0 {
return vec![];
} else if n == 1 {
return vec![0];
}
let mut sequence = vec![0, 1];
for i in 2..n {
let next = sequence[i - 1] + sequence[i - 2];
sequence.push(next);
}
sequence
}
```
```file_path
/src/utils.rs
```
```rust
/// Prints a simple message to the console.
pub fn print_message() {
println!(\"This is a utility message from the utils module.\");
}
```
```bash
cargo new rust_project
```
```bash
cd rust_project
```
```bash
cargo build
```
```bash
cargo run
```
---
### **Example 3: JavaScript Web Server with Express**
#### **User Request:** \"Create a simple Express.js web server with routes and a start script.\"
#### **Output:**
```file_path
/package.json
```
```json
{
\"name\": \"express-server\",
\"version\": \"1.0.0\",
\"description\": \"A simple Express.js server.\",
\"main\": \"index.js\",
\"scripts\": {
\"dev\": \"node index.js\"
},
\"dependencies\": {
\"express\": \"^4.18.2\"
}
}
```
```file_path
/index.js
```
```javascript
const express = require('express');
const app = express();
const port = 3000;
app.get('/', (req, res) => {
res.send('Hello, World!');
});
app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});
```
```bash
npm init -y
```
```bash
npm install express
```
```bash
npm run dev
```
"};
pub struct MessageEditor {
thread: Entity<Thread>,
@@ -38,6 +307,8 @@ pub struct MessageEditor {
model_selector: Entity<AssistantModelSelector>,
model_selector_menu_handle: PopoverMenuHandle<LanguageModelSelector>,
use_tools: bool,
create_project_mode: bool,
_subscriptions: Vec<Subscription>,
}
@@ -117,6 +388,7 @@ impl MessageEditor {
}),
model_selector_menu_handle,
use_tools: false,
create_project_mode: false,
_subscriptions: subscriptions,
}
}
@@ -199,11 +471,20 @@ impl MessageEditor {
let thread = self.thread.clone();
let context_store = self.context_store.clone();
let use_tools = self.use_tools;
let create_project_mode = self.create_project_mode;
cx.spawn(move |_, mut cx| async move {
refresh_task.await;
thread
.update(&mut cx, |thread, cx| {
let context = context_store.read(cx).snapshot(cx).collect::<Vec<_>>();
// Only first message gets the project mode system prompt.
if thread.is_empty() && create_project_mode {
thread.insert_message(
language_model::Role::System,
CREATE_PROJECT_PROMPT,
cx,
);
}
thread.insert_user_message(user_message, context, cx);
let mut request = thread.to_completion_request(request_kind, cx);
@@ -377,21 +658,61 @@ impl Render for MessageEditor {
h_flex()
.justify_between()
.child(
Switch::new("use-tools", self.use_tools.into())
.label("Tools")
.on_click(cx.listener(|this, selection, _window, _cx| {
this.use_tools = match selection {
ToggleState::Selected => true,
ToggleState::Unselected
| ToggleState::Indeterminate => false,
};
}))
.key_binding(KeyBinding::for_action_in(
&ChatMode,
&focus_handle,
window,
cx,
)),
h_flex()
.gap_2()
.child(
Switch::new("use-tools", self.use_tools.into())
.label("Tools")
.on_click(cx.listener(
|this, selection, _window, _cx| {
this.use_tools = match selection {
ToggleState::Selected => true,
ToggleState::Unselected
| ToggleState::Indeterminate => false,
};
},
))
.key_binding(KeyBinding::for_action_in(
&ChatMode,
&focus_handle,
window,
cx,
)),
)
.when(cx.is_staff(), |this| {
this.child(
Switch::new(
"create-project-mode",
self.create_project_mode.into(),
)
.label("Create project mode")
.on_click(
cx.listener(|this, selection, _window, _cx| {
this.create_project_mode = match selection {
ToggleState::Selected => true,
ToggleState::Unselected
| ToggleState::Indeterminate => false,
};
}),
),
)
})
.when(self.create_project_mode, |this| {
this.child(
ButtonLike::new("create-files")
.child(Label::new("Create Files"))
.on_click({
let focus_handle = focus_handle.clone();
move |_event, window, cx| {
focus_handle.dispatch_action(
&CreateFiles,
window,
cx,
);
}
}),
)
}),
)
.child(h_flex().gap_1().child(self.model_selector.clone()).child(
if is_streaming_completion {

View File

@@ -776,7 +776,7 @@ async fn open_disabled_globs_setting_in_editor(
) -> Result<()> {
let settings_editor = workspace
.update_in(&mut cx, |_, window, cx| {
create_and_open_local_file(paths::settings_file(), window, cx, || {
create_and_open_local_file(paths::settings_file().to_path_buf(), window, cx, || {
settings::initial_user_settings_content().as_ref().into()
})
})?

View File

@@ -6081,36 +6081,33 @@ pub fn open_new(
}
pub fn create_and_open_local_file(
path: &'static Path,
path: PathBuf,
window: &mut Window,
cx: &mut Context<Workspace>,
default_content: impl 'static + Send + FnOnce() -> Rope,
) -> Task<Result<Box<dyn ItemHandle>>> {
cx.spawn_in(window, |workspace, mut cx| async move {
let fs = workspace.update(&mut cx, |workspace, _| workspace.app_state().fs.clone())?;
if !fs.is_file(path).await {
fs.create_file(path, Default::default()).await?;
fs.save(path, &default_content(), Default::default())
if !fs.is_file(path.as_path()).await {
fs.create_file(path.as_path(), Default::default()).await?;
fs.save(path.as_path(), &default_content(), Default::default())
.await?;
}
let mut items = workspace
.update_in(&mut cx, |workspace, window, cx| {
workspace.with_local_workspace(window, cx, |workspace, window, cx| {
workspace.open_paths(
vec![path.to_path_buf()],
OpenVisible::None,
None,
window,
cx,
)
})
.update_in(&mut cx, {
let path = path.clone();
|workspace, window, cx| {
workspace.with_local_workspace(window, cx, |workspace, window, cx| {
workspace.open_paths(vec![path], OpenVisible::None, None, window, cx)
})
}
})?
.await?
.await;
let item = items.pop().flatten();
item.ok_or_else(|| anyhow!("path {path:?} is not a file"))?
item.ok_or_else(|| anyhow!("path {:?} is not a file", path))?
})
}

View File

@@ -1704,8 +1704,12 @@ fn open_settings_file(
// restarts.
project.find_or_create_worktree(paths::config_dir().as_path(), false, cx)
});
let settings_open_task =
create_and_open_local_file(abs_path, window, cx, default_content);
let settings_open_task = create_and_open_local_file(
abs_path.to_path_buf(),
window,
cx,
default_content,
);
(worktree_creation_task, settings_open_task)
})
})?