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