1use crate::context::load_context;
2use crate::context_store::ContextStore;
3use crate::inline_prompt_editor::{
4 CodegenStatus, PromptEditor, PromptEditorEvent, TerminalInlineAssistId,
5};
6use crate::terminal_codegen::{CLEAR_INPUT, CodegenEvent, TerminalCodegen};
7use crate::thread_store::{TextThreadStore, ThreadStore};
8use agent_settings::AgentSettings;
9use anyhow::{Context as _, Result};
10use client::telemetry::Telemetry;
11use collections::{HashMap, VecDeque};
12use editor::{MultiBuffer, actions::SelectAll};
13use fs::Fs;
14use gpui::{App, Entity, Focusable, Global, Subscription, Task, UpdateGlobal, WeakEntity};
15use language::Buffer;
16use language_model::{
17 ConfiguredModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
18 Role, report_assistant_event,
19};
20use project::Project;
21use prompt_store::{PromptBuilder, PromptStore};
22use std::sync::Arc;
23use telemetry_events::{AssistantEventData, AssistantKind, AssistantPhase};
24use terminal_view::TerminalView;
25use ui::prelude::*;
26use util::ResultExt;
27use workspace::{Toast, Workspace, notifications::NotificationId};
28use zed_llm_client::CompletionIntent;
29
30pub fn init(
31 fs: Arc<dyn Fs>,
32 prompt_builder: Arc<PromptBuilder>,
33 telemetry: Arc<Telemetry>,
34 cx: &mut App,
35) {
36 cx.set_global(TerminalInlineAssistant::new(fs, prompt_builder, telemetry));
37}
38
39const DEFAULT_CONTEXT_LINES: usize = 50;
40const PROMPT_HISTORY_MAX_LEN: usize = 20;
41
42pub struct TerminalInlineAssistant {
43 next_assist_id: TerminalInlineAssistId,
44 assists: HashMap<TerminalInlineAssistId, TerminalInlineAssist>,
45 prompt_history: VecDeque<String>,
46 telemetry: Option<Arc<Telemetry>>,
47 fs: Arc<dyn Fs>,
48 prompt_builder: Arc<PromptBuilder>,
49}
50
51impl Global for TerminalInlineAssistant {}
52
53impl TerminalInlineAssistant {
54 pub fn new(
55 fs: Arc<dyn Fs>,
56 prompt_builder: Arc<PromptBuilder>,
57 telemetry: Arc<Telemetry>,
58 ) -> Self {
59 Self {
60 next_assist_id: TerminalInlineAssistId::default(),
61 assists: HashMap::default(),
62 prompt_history: VecDeque::default(),
63 telemetry: Some(telemetry),
64 fs,
65 prompt_builder,
66 }
67 }
68
69 pub fn assist(
70 &mut self,
71 terminal_view: &Entity<TerminalView>,
72 workspace: WeakEntity<Workspace>,
73 project: WeakEntity<Project>,
74 prompt_store: Option<Entity<PromptStore>>,
75 thread_store: Option<WeakEntity<ThreadStore>>,
76 text_thread_store: Option<WeakEntity<TextThreadStore>>,
77 initial_prompt: Option<String>,
78 window: &mut Window,
79 cx: &mut App,
80 ) {
81 let terminal = terminal_view.read(cx).terminal().clone();
82 let assist_id = self.next_assist_id.post_inc();
83 let prompt_buffer = cx.new(|cx| {
84 MultiBuffer::singleton(
85 cx.new(|cx| Buffer::local(initial_prompt.unwrap_or_default(), cx)),
86 cx,
87 )
88 });
89 let context_store = cx.new(|_cx| ContextStore::new(project, thread_store.clone()));
90 let codegen = cx.new(|_| TerminalCodegen::new(terminal, self.telemetry.clone()));
91
92 let prompt_editor = cx.new(|cx| {
93 PromptEditor::new_terminal(
94 assist_id,
95 self.prompt_history.clone(),
96 prompt_buffer.clone(),
97 codegen,
98 self.fs.clone(),
99 context_store.clone(),
100 workspace.clone(),
101 thread_store.clone(),
102 text_thread_store.clone(),
103 window,
104 cx,
105 )
106 });
107 let prompt_editor_render = prompt_editor.clone();
108 let block = terminal_view::BlockProperties {
109 height: 4,
110 render: Box::new(move |_| prompt_editor_render.clone().into_any_element()),
111 };
112 terminal_view.update(cx, |terminal_view, cx| {
113 terminal_view.set_block_below_cursor(block, window, cx);
114 });
115
116 let terminal_assistant = TerminalInlineAssist::new(
117 assist_id,
118 terminal_view,
119 prompt_editor,
120 workspace.clone(),
121 context_store,
122 prompt_store,
123 window,
124 cx,
125 );
126
127 self.assists.insert(assist_id, terminal_assistant);
128
129 self.focus_assist(assist_id, window, cx);
130 }
131
132 fn focus_assist(
133 &mut self,
134 assist_id: TerminalInlineAssistId,
135 window: &mut Window,
136 cx: &mut App,
137 ) {
138 let assist = &self.assists[&assist_id];
139 if let Some(prompt_editor) = assist.prompt_editor.as_ref() {
140 prompt_editor.update(cx, |this, cx| {
141 this.editor.update(cx, |editor, cx| {
142 window.focus(&editor.focus_handle(cx));
143 editor.select_all(&SelectAll, window, cx);
144 });
145 });
146 }
147 }
148
149 fn handle_prompt_editor_event(
150 &mut self,
151 prompt_editor: Entity<PromptEditor<TerminalCodegen>>,
152 event: &PromptEditorEvent,
153 window: &mut Window,
154 cx: &mut App,
155 ) {
156 let assist_id = prompt_editor.read(cx).id();
157 match event {
158 PromptEditorEvent::StartRequested => {
159 self.start_assist(assist_id, cx);
160 }
161 PromptEditorEvent::StopRequested => {
162 self.stop_assist(assist_id, cx);
163 }
164 PromptEditorEvent::ConfirmRequested { execute } => {
165 self.finish_assist(assist_id, false, *execute, window, cx);
166 }
167 PromptEditorEvent::CancelRequested => {
168 self.finish_assist(assist_id, true, false, window, cx);
169 }
170 PromptEditorEvent::Resized { height_in_lines } => {
171 self.insert_prompt_editor_into_terminal(assist_id, *height_in_lines, window, cx);
172 }
173 }
174 }
175
176 fn start_assist(&mut self, assist_id: TerminalInlineAssistId, cx: &mut App) {
177 let assist = if let Some(assist) = self.assists.get_mut(&assist_id) {
178 assist
179 } else {
180 return;
181 };
182
183 let Some(user_prompt) = assist
184 .prompt_editor
185 .as_ref()
186 .map(|editor| editor.read(cx).prompt(cx))
187 else {
188 return;
189 };
190
191 self.prompt_history.retain(|prompt| *prompt != user_prompt);
192 self.prompt_history.push_back(user_prompt);
193 if self.prompt_history.len() > PROMPT_HISTORY_MAX_LEN {
194 self.prompt_history.pop_front();
195 }
196
197 assist
198 .terminal
199 .update(cx, |terminal, cx| {
200 terminal
201 .terminal()
202 .update(cx, |terminal, _| terminal.input(CLEAR_INPUT.as_bytes()));
203 })
204 .log_err();
205
206 let codegen = assist.codegen.clone();
207 let Some(request_task) = self.request_for_inline_assist(assist_id, cx).log_err() else {
208 return;
209 };
210
211 codegen.update(cx, |codegen, cx| codegen.start(request_task, cx));
212 }
213
214 fn stop_assist(&mut self, assist_id: TerminalInlineAssistId, cx: &mut App) {
215 let assist = if let Some(assist) = self.assists.get_mut(&assist_id) {
216 assist
217 } else {
218 return;
219 };
220
221 assist.codegen.update(cx, |codegen, cx| codegen.stop(cx));
222 }
223
224 fn request_for_inline_assist(
225 &self,
226 assist_id: TerminalInlineAssistId,
227 cx: &mut App,
228 ) -> Result<Task<LanguageModelRequest>> {
229 let assist = self.assists.get(&assist_id).context("invalid assist")?;
230
231 let shell = std::env::var("SHELL").ok();
232 let (latest_output, working_directory) = assist
233 .terminal
234 .update(cx, |terminal, cx| {
235 let terminal = terminal.entity().read(cx);
236 let latest_output = terminal.last_n_non_empty_lines(DEFAULT_CONTEXT_LINES);
237 let working_directory = terminal
238 .working_directory()
239 .map(|path| path.to_string_lossy().to_string());
240 (latest_output, working_directory)
241 })
242 .ok()
243 .unwrap_or_default();
244
245 let prompt = self.prompt_builder.generate_terminal_assistant_prompt(
246 &assist
247 .prompt_editor
248 .clone()
249 .context("invalid assist")?
250 .read(cx)
251 .prompt(cx),
252 shell.as_deref(),
253 working_directory.as_deref(),
254 &latest_output,
255 )?;
256
257 let contexts = assist
258 .context_store
259 .read(cx)
260 .context()
261 .cloned()
262 .collect::<Vec<_>>();
263 let context_load_task = assist.workspace.update(cx, |workspace, cx| {
264 let project = workspace.project();
265 load_context(contexts, project, &assist.prompt_store, cx)
266 })?;
267
268 let ConfiguredModel { model, .. } = LanguageModelRegistry::read_global(cx)
269 .inline_assistant_model()
270 .context("No inline assistant model")?;
271
272 let temperature = AgentSettings::temperature_for_model(&model, cx);
273
274 Ok(cx.background_spawn(async move {
275 let mut request_message = LanguageModelRequestMessage {
276 role: Role::User,
277 content: vec![],
278 cache: false,
279 };
280
281 context_load_task
282 .await
283 .loaded_context
284 .add_to_request_message(&mut request_message);
285
286 request_message.content.push(prompt.into());
287
288 LanguageModelRequest {
289 thread_id: None,
290 prompt_id: None,
291 mode: None,
292 intent: Some(CompletionIntent::TerminalInlineAssist),
293 messages: vec![request_message],
294 tools: Vec::new(),
295 tool_choice: None,
296 stop: Vec::new(),
297 temperature,
298 }
299 }))
300 }
301
302 fn finish_assist(
303 &mut self,
304 assist_id: TerminalInlineAssistId,
305 undo: bool,
306 execute: bool,
307 window: &mut Window,
308 cx: &mut App,
309 ) {
310 self.dismiss_assist(assist_id, window, cx);
311
312 if let Some(assist) = self.assists.remove(&assist_id) {
313 assist
314 .terminal
315 .update(cx, |this, cx| {
316 this.clear_block_below_cursor(cx);
317 this.focus_handle(cx).focus(window);
318 })
319 .log_err();
320
321 if let Some(ConfiguredModel { model, .. }) =
322 LanguageModelRegistry::read_global(cx).inline_assistant_model()
323 {
324 let codegen = assist.codegen.read(cx);
325 let executor = cx.background_executor().clone();
326 report_assistant_event(
327 AssistantEventData {
328 conversation_id: None,
329 kind: AssistantKind::InlineTerminal,
330 message_id: codegen.message_id.clone(),
331 phase: if undo {
332 AssistantPhase::Rejected
333 } else {
334 AssistantPhase::Accepted
335 },
336 model: model.telemetry_id(),
337 model_provider: model.provider_id().to_string(),
338 response_latency: None,
339 error_message: None,
340 language_name: None,
341 },
342 codegen.telemetry.clone(),
343 cx.http_client(),
344 model.api_key(cx),
345 &executor,
346 );
347 }
348
349 assist.codegen.update(cx, |codegen, cx| {
350 if undo {
351 codegen.undo(cx);
352 } else if execute {
353 codegen.complete(cx);
354 }
355 });
356 }
357 }
358
359 fn dismiss_assist(
360 &mut self,
361 assist_id: TerminalInlineAssistId,
362 window: &mut Window,
363 cx: &mut App,
364 ) -> bool {
365 let Some(assist) = self.assists.get_mut(&assist_id) else {
366 return false;
367 };
368 if assist.prompt_editor.is_none() {
369 return false;
370 }
371 assist.prompt_editor = None;
372 assist
373 .terminal
374 .update(cx, |this, cx| {
375 this.clear_block_below_cursor(cx);
376 this.focus_handle(cx).focus(window);
377 })
378 .is_ok()
379 }
380
381 fn insert_prompt_editor_into_terminal(
382 &mut self,
383 assist_id: TerminalInlineAssistId,
384 height: u8,
385 window: &mut Window,
386 cx: &mut App,
387 ) {
388 if let Some(assist) = self.assists.get_mut(&assist_id) {
389 if let Some(prompt_editor) = assist.prompt_editor.as_ref().cloned() {
390 assist
391 .terminal
392 .update(cx, |terminal, cx| {
393 terminal.clear_block_below_cursor(cx);
394 let block = terminal_view::BlockProperties {
395 height,
396 render: Box::new(move |_| prompt_editor.clone().into_any_element()),
397 };
398 terminal.set_block_below_cursor(block, window, cx);
399 })
400 .log_err();
401 }
402 }
403 }
404}
405
406struct TerminalInlineAssist {
407 terminal: WeakEntity<TerminalView>,
408 prompt_editor: Option<Entity<PromptEditor<TerminalCodegen>>>,
409 codegen: Entity<TerminalCodegen>,
410 workspace: WeakEntity<Workspace>,
411 context_store: Entity<ContextStore>,
412 prompt_store: Option<Entity<PromptStore>>,
413 _subscriptions: Vec<Subscription>,
414}
415
416impl TerminalInlineAssist {
417 pub fn new(
418 assist_id: TerminalInlineAssistId,
419 terminal: &Entity<TerminalView>,
420 prompt_editor: Entity<PromptEditor<TerminalCodegen>>,
421 workspace: WeakEntity<Workspace>,
422 context_store: Entity<ContextStore>,
423 prompt_store: Option<Entity<PromptStore>>,
424 window: &mut Window,
425 cx: &mut App,
426 ) -> Self {
427 let codegen = prompt_editor.read(cx).codegen().clone();
428 Self {
429 terminal: terminal.downgrade(),
430 prompt_editor: Some(prompt_editor.clone()),
431 codegen: codegen.clone(),
432 workspace: workspace.clone(),
433 context_store,
434 prompt_store,
435 _subscriptions: vec![
436 window.subscribe(&prompt_editor, cx, |prompt_editor, event, window, cx| {
437 TerminalInlineAssistant::update_global(cx, |this, cx| {
438 this.handle_prompt_editor_event(prompt_editor, event, window, cx)
439 })
440 }),
441 window.subscribe(&codegen, cx, move |codegen, event, window, cx| {
442 TerminalInlineAssistant::update_global(cx, |this, cx| match event {
443 CodegenEvent::Finished => {
444 let assist = if let Some(assist) = this.assists.get(&assist_id) {
445 assist
446 } else {
447 return;
448 };
449
450 if let CodegenStatus::Error(error) = &codegen.read(cx).status {
451 if assist.prompt_editor.is_none() {
452 if let Some(workspace) = assist.workspace.upgrade() {
453 let error =
454 format!("Terminal inline assistant error: {}", error);
455 workspace.update(cx, |workspace, cx| {
456 struct InlineAssistantError;
457
458 let id =
459 NotificationId::composite::<InlineAssistantError>(
460 assist_id.0,
461 );
462
463 workspace.show_toast(Toast::new(id, error), cx);
464 })
465 }
466 }
467 }
468
469 if assist.prompt_editor.is_none() {
470 this.finish_assist(assist_id, false, false, window, cx);
471 }
472 }
473 })
474 }),
475 ],
476 }
477 }
478}