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