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