1use crate::assistant_settings::AssistantSettings;
2use crate::{
3 humanize_token_count, prompts::PromptBuilder, AssistantPanel, AssistantPanelEvent, RequestType,
4 DEFAULT_CONTEXT_LINES,
5};
6use anyhow::{Context as _, Result};
7use client::telemetry::Telemetry;
8use collections::{HashMap, VecDeque};
9use editor::{
10 actions::{MoveDown, MoveUp, SelectAll},
11 Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, MultiBuffer,
12};
13use fs::Fs;
14use futures::{channel::mpsc, SinkExt, StreamExt};
15use gpui::{
16 AppContext, Context, EventEmitter, FocusHandle, FocusableView, Global, Model, ModelContext,
17 Subscription, Task, TextStyle, UpdateGlobal, View, WeakView,
18};
19use language::Buffer;
20use language_model::{
21 LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, Role,
22};
23use language_model_selector::LanguageModelSelector;
24use language_models::report_assistant_event;
25use settings::{update_settings_file, Settings};
26use std::{
27 cmp,
28 sync::Arc,
29 time::{Duration, Instant},
30};
31use telemetry_events::{AssistantEvent, AssistantKind, AssistantPhase};
32use terminal::Terminal;
33use terminal_view::TerminalView;
34use theme::ThemeSettings;
35use ui::{prelude::*, text_for_action, IconButtonShape, Tooltip};
36use util::ResultExt;
37use workspace::{notifications::NotificationId, Toast, Workspace};
38
39pub fn init(
40 fs: Arc<dyn Fs>,
41 prompt_builder: Arc<PromptBuilder>,
42 telemetry: Arc<Telemetry>,
43 cx: &mut AppContext,
44) {
45 cx.set_global(TerminalInlineAssistant::new(fs, prompt_builder, telemetry));
46}
47
48const PROMPT_HISTORY_MAX_LEN: usize = 20;
49
50#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash)]
51struct TerminalInlineAssistId(usize);
52
53impl TerminalInlineAssistId {
54 fn post_inc(&mut self) -> TerminalInlineAssistId {
55 let id = *self;
56 self.0 += 1;
57 id
58 }
59}
60
61pub struct TerminalInlineAssistant {
62 next_assist_id: TerminalInlineAssistId,
63 assists: HashMap<TerminalInlineAssistId, TerminalInlineAssist>,
64 prompt_history: VecDeque<String>,
65 telemetry: Option<Arc<Telemetry>>,
66 fs: Arc<dyn Fs>,
67 prompt_builder: Arc<PromptBuilder>,
68}
69
70impl Global for TerminalInlineAssistant {}
71
72impl TerminalInlineAssistant {
73 pub fn new(
74 fs: Arc<dyn Fs>,
75 prompt_builder: Arc<PromptBuilder>,
76 telemetry: Arc<Telemetry>,
77 ) -> Self {
78 Self {
79 next_assist_id: TerminalInlineAssistId::default(),
80 assists: HashMap::default(),
81 prompt_history: VecDeque::default(),
82 telemetry: Some(telemetry),
83 fs,
84 prompt_builder,
85 }
86 }
87
88 pub fn assist(
89 &mut self,
90 terminal_view: &View<TerminalView>,
91 workspace: Option<WeakView<Workspace>>,
92 assistant_panel: Option<&View<AssistantPanel>>,
93 initial_prompt: Option<String>,
94 cx: &mut WindowContext,
95 ) {
96 let terminal = terminal_view.read(cx).terminal().clone();
97 let assist_id = self.next_assist_id.post_inc();
98 let prompt_buffer =
99 cx.new_model(|cx| Buffer::local(initial_prompt.unwrap_or_default(), cx));
100 let prompt_buffer = cx.new_model(|cx| MultiBuffer::singleton(prompt_buffer, cx));
101 let codegen = cx.new_model(|_| Codegen::new(terminal, self.telemetry.clone()));
102
103 let prompt_editor = cx.new_view(|cx| {
104 PromptEditor::new(
105 assist_id,
106 self.prompt_history.clone(),
107 prompt_buffer.clone(),
108 codegen,
109 assistant_panel,
110 workspace.clone(),
111 self.fs.clone(),
112 cx,
113 )
114 });
115 let prompt_editor_render = prompt_editor.clone();
116 let block = terminal_view::BlockProperties {
117 height: 2,
118 render: Box::new(move |_| prompt_editor_render.clone().into_any_element()),
119 };
120 terminal_view.update(cx, |terminal_view, cx| {
121 terminal_view.set_block_below_cursor(block, cx);
122 });
123
124 let terminal_assistant = TerminalInlineAssist::new(
125 assist_id,
126 terminal_view,
127 assistant_panel.is_some(),
128 prompt_editor,
129 workspace.clone(),
130 cx,
131 );
132
133 self.assists.insert(assist_id, terminal_assistant);
134
135 self.focus_assist(assist_id, cx);
136 }
137
138 fn focus_assist(&mut self, assist_id: TerminalInlineAssistId, cx: &mut WindowContext) {
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 editor.focus(cx);
144 editor.select_all(&SelectAll, cx);
145 });
146 });
147 }
148 }
149
150 fn handle_prompt_editor_event(
151 &mut self,
152 prompt_editor: View<PromptEditor>,
153 event: &PromptEditorEvent,
154 cx: &mut WindowContext,
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, cx);
166 }
167 PromptEditorEvent::CancelRequested => {
168 self.finish_assist(assist_id, true, false, cx);
169 }
170 PromptEditorEvent::DismissRequested => {
171 self.dismiss_assist(assist_id, cx);
172 }
173 PromptEditorEvent::Resized { height_in_lines } => {
174 self.insert_prompt_editor_into_terminal(assist_id, *height_in_lines, cx);
175 }
176 }
177 }
178
179 fn start_assist(&mut self, assist_id: TerminalInlineAssistId, cx: &mut WindowContext) {
180 let assist = if let Some(assist) = self.assists.get_mut(&assist_id) {
181 assist
182 } else {
183 return;
184 };
185
186 let Some(user_prompt) = assist
187 .prompt_editor
188 .as_ref()
189 .map(|editor| editor.read(cx).prompt(cx))
190 else {
191 return;
192 };
193
194 self.prompt_history.retain(|prompt| *prompt != user_prompt);
195 self.prompt_history.push_back(user_prompt.clone());
196 if self.prompt_history.len() > PROMPT_HISTORY_MAX_LEN {
197 self.prompt_history.pop_front();
198 }
199
200 assist
201 .terminal
202 .update(cx, |terminal, cx| {
203 terminal
204 .terminal()
205 .update(cx, |terminal, _| terminal.input(CLEAR_INPUT.to_string()));
206 })
207 .log_err();
208
209 let codegen = assist.codegen.clone();
210 let Some(request) = self.request_for_inline_assist(assist_id, cx).log_err() else {
211 return;
212 };
213
214 codegen.update(cx, |codegen, cx| codegen.start(request, cx));
215 }
216
217 fn stop_assist(&mut self, assist_id: TerminalInlineAssistId, cx: &mut WindowContext) {
218 let assist = if let Some(assist) = self.assists.get_mut(&assist_id) {
219 assist
220 } else {
221 return;
222 };
223
224 assist.codegen.update(cx, |codegen, cx| codegen.stop(cx));
225 }
226
227 fn request_for_inline_assist(
228 &self,
229 assist_id: TerminalInlineAssistId,
230 cx: &mut WindowContext,
231 ) -> Result<LanguageModelRequest> {
232 let assist = self.assists.get(&assist_id).context("invalid assist")?;
233
234 let shell = std::env::var("SHELL").ok();
235 let (latest_output, working_directory) = assist
236 .terminal
237 .update(cx, |terminal, cx| {
238 let terminal = terminal.model().read(cx);
239 let latest_output = terminal.last_n_non_empty_lines(DEFAULT_CONTEXT_LINES);
240 let working_directory = terminal
241 .working_directory()
242 .map(|path| path.to_string_lossy().to_string());
243 (latest_output, working_directory)
244 })
245 .ok()
246 .unwrap_or_default();
247
248 let context_request = if assist.include_context {
249 assist.workspace.as_ref().and_then(|workspace| {
250 let workspace = workspace.upgrade()?.read(cx);
251 let assistant_panel = workspace.panel::<AssistantPanel>(cx)?;
252 Some(
253 assistant_panel
254 .read(cx)
255 .active_context(cx)?
256 .read(cx)
257 .to_completion_request(RequestType::Chat, cx),
258 )
259 })
260 } else {
261 None
262 };
263
264 let prompt = self.prompt_builder.generate_terminal_assistant_prompt(
265 &assist
266 .prompt_editor
267 .clone()
268 .context("invalid assist")?
269 .read(cx)
270 .prompt(cx),
271 shell.as_deref(),
272 working_directory.as_deref(),
273 &latest_output,
274 )?;
275
276 let mut messages = Vec::new();
277 if let Some(context_request) = context_request {
278 messages = context_request.messages;
279 }
280
281 messages.push(LanguageModelRequestMessage {
282 role: Role::User,
283 content: vec![prompt.into()],
284 cache: false,
285 });
286
287 Ok(LanguageModelRequest {
288 messages,
289 tools: Vec::new(),
290 stop: Vec::new(),
291 temperature: None,
292 })
293 }
294
295 fn finish_assist(
296 &mut self,
297 assist_id: TerminalInlineAssistId,
298 undo: bool,
299 execute: bool,
300 cx: &mut WindowContext,
301 ) {
302 self.dismiss_assist(assist_id, cx);
303
304 if let Some(assist) = self.assists.remove(&assist_id) {
305 assist
306 .terminal
307 .update(cx, |this, cx| {
308 this.clear_block_below_cursor(cx);
309 this.focus_handle(cx).focus(cx);
310 })
311 .log_err();
312
313 if let Some(model) = LanguageModelRegistry::read_global(cx).active_model() {
314 let codegen = assist.codegen.read(cx);
315 let executor = cx.background_executor().clone();
316 report_assistant_event(
317 AssistantEvent {
318 conversation_id: None,
319 kind: AssistantKind::InlineTerminal,
320 message_id: codegen.message_id.clone(),
321 phase: if undo {
322 AssistantPhase::Rejected
323 } else {
324 AssistantPhase::Accepted
325 },
326 model: model.telemetry_id(),
327 model_provider: model.provider_id().to_string(),
328 response_latency: None,
329 error_message: None,
330 language_name: None,
331 },
332 codegen.telemetry.clone(),
333 cx.http_client(),
334 model.api_key(cx),
335 &executor,
336 );
337 }
338
339 assist.codegen.update(cx, |codegen, cx| {
340 if undo {
341 codegen.undo(cx);
342 } else if execute {
343 codegen.complete(cx);
344 }
345 });
346 }
347 }
348
349 fn dismiss_assist(
350 &mut self,
351 assist_id: TerminalInlineAssistId,
352 cx: &mut WindowContext,
353 ) -> bool {
354 let Some(assist) = self.assists.get_mut(&assist_id) else {
355 return false;
356 };
357 if assist.prompt_editor.is_none() {
358 return false;
359 }
360 assist.prompt_editor = None;
361 assist
362 .terminal
363 .update(cx, |this, cx| {
364 this.clear_block_below_cursor(cx);
365 this.focus_handle(cx).focus(cx);
366 })
367 .is_ok()
368 }
369
370 fn insert_prompt_editor_into_terminal(
371 &mut self,
372 assist_id: TerminalInlineAssistId,
373 height: u8,
374 cx: &mut WindowContext,
375 ) {
376 if let Some(assist) = self.assists.get_mut(&assist_id) {
377 if let Some(prompt_editor) = assist.prompt_editor.as_ref().cloned() {
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, cx);
387 })
388 .log_err();
389 }
390 }
391 }
392}
393
394struct TerminalInlineAssist {
395 terminal: WeakView<TerminalView>,
396 prompt_editor: Option<View<PromptEditor>>,
397 codegen: Model<Codegen>,
398 workspace: Option<WeakView<Workspace>>,
399 include_context: bool,
400 _subscriptions: Vec<Subscription>,
401}
402
403impl TerminalInlineAssist {
404 pub fn new(
405 assist_id: TerminalInlineAssistId,
406 terminal: &View<TerminalView>,
407 include_context: bool,
408 prompt_editor: View<PromptEditor>,
409 workspace: Option<WeakView<Workspace>>,
410 cx: &mut WindowContext,
411 ) -> Self {
412 let codegen = prompt_editor.read(cx).codegen.clone();
413 Self {
414 terminal: terminal.downgrade(),
415 prompt_editor: Some(prompt_editor.clone()),
416 codegen: codegen.clone(),
417 workspace: workspace.clone(),
418 include_context,
419 _subscriptions: vec![
420 cx.subscribe(&prompt_editor, |prompt_editor, event, cx| {
421 TerminalInlineAssistant::update_global(cx, |this, cx| {
422 this.handle_prompt_editor_event(prompt_editor, event, cx)
423 })
424 }),
425 cx.subscribe(&codegen, move |codegen, event, cx| {
426 TerminalInlineAssistant::update_global(cx, |this, cx| match event {
427 CodegenEvent::Finished => {
428 let assist = if let Some(assist) = this.assists.get(&assist_id) {
429 assist
430 } else {
431 return;
432 };
433
434 if let CodegenStatus::Error(error) = &codegen.read(cx).status {
435 if assist.prompt_editor.is_none() {
436 if let Some(workspace) = assist
437 .workspace
438 .as_ref()
439 .and_then(|workspace| workspace.upgrade())
440 {
441 let error =
442 format!("Terminal inline assistant error: {}", error);
443 workspace.update(cx, |workspace, cx| {
444 struct InlineAssistantError;
445
446 let id =
447 NotificationId::composite::<InlineAssistantError>(
448 assist_id.0,
449 );
450
451 workspace.show_toast(Toast::new(id, error), cx);
452 })
453 }
454 }
455 }
456
457 if assist.prompt_editor.is_none() {
458 this.finish_assist(assist_id, false, false, cx);
459 }
460 }
461 })
462 }),
463 ],
464 }
465 }
466}
467
468enum PromptEditorEvent {
469 StartRequested,
470 StopRequested,
471 ConfirmRequested { execute: bool },
472 CancelRequested,
473 DismissRequested,
474 Resized { height_in_lines: u8 },
475}
476
477struct PromptEditor {
478 id: TerminalInlineAssistId,
479 fs: Arc<dyn Fs>,
480 height_in_lines: u8,
481 editor: View<Editor>,
482 edited_since_done: bool,
483 prompt_history: VecDeque<String>,
484 prompt_history_ix: Option<usize>,
485 pending_prompt: String,
486 codegen: Model<Codegen>,
487 _codegen_subscription: Subscription,
488 editor_subscriptions: Vec<Subscription>,
489 pending_token_count: Task<Result<()>>,
490 token_count: Option<usize>,
491 _token_count_subscriptions: Vec<Subscription>,
492 workspace: Option<WeakView<Workspace>>,
493}
494
495impl EventEmitter<PromptEditorEvent> for PromptEditor {}
496
497impl Render for PromptEditor {
498 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
499 let status = &self.codegen.read(cx).status;
500 let buttons = match status {
501 CodegenStatus::Idle => {
502 vec![
503 IconButton::new("cancel", IconName::Close)
504 .icon_color(Color::Muted)
505 .shape(IconButtonShape::Square)
506 .tooltip(|cx| Tooltip::for_action("Cancel Assist", &menu::Cancel, cx))
507 .on_click(
508 cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested)),
509 ),
510 IconButton::new("start", IconName::SparkleAlt)
511 .icon_color(Color::Muted)
512 .shape(IconButtonShape::Square)
513 .tooltip(|cx| Tooltip::for_action("Generate", &menu::Confirm, cx))
514 .on_click(
515 cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::StartRequested)),
516 ),
517 ]
518 }
519 CodegenStatus::Pending => {
520 vec![
521 IconButton::new("cancel", IconName::Close)
522 .icon_color(Color::Muted)
523 .shape(IconButtonShape::Square)
524 .tooltip(|cx| Tooltip::text("Cancel Assist", cx))
525 .on_click(
526 cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested)),
527 ),
528 IconButton::new("stop", IconName::Stop)
529 .icon_color(Color::Error)
530 .shape(IconButtonShape::Square)
531 .tooltip(|cx| {
532 Tooltip::with_meta(
533 "Interrupt Generation",
534 Some(&menu::Cancel),
535 "Changes won't be discarded",
536 cx,
537 )
538 })
539 .on_click(
540 cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::StopRequested)),
541 ),
542 ]
543 }
544 CodegenStatus::Error(_) | CodegenStatus::Done => {
545 let cancel = IconButton::new("cancel", IconName::Close)
546 .icon_color(Color::Muted)
547 .shape(IconButtonShape::Square)
548 .tooltip(|cx| Tooltip::for_action("Cancel Assist", &menu::Cancel, cx))
549 .on_click(cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested)));
550
551 let has_error = matches!(status, CodegenStatus::Error(_));
552 if has_error || self.edited_since_done {
553 vec![
554 cancel,
555 IconButton::new("restart", IconName::RotateCw)
556 .icon_color(Color::Info)
557 .shape(IconButtonShape::Square)
558 .tooltip(|cx| {
559 Tooltip::with_meta(
560 "Restart Generation",
561 Some(&menu::Confirm),
562 "Changes will be discarded",
563 cx,
564 )
565 })
566 .on_click(cx.listener(|_, _, cx| {
567 cx.emit(PromptEditorEvent::StartRequested);
568 })),
569 ]
570 } else {
571 vec![
572 cancel,
573 IconButton::new("accept", IconName::Check)
574 .icon_color(Color::Info)
575 .shape(IconButtonShape::Square)
576 .tooltip(|cx| {
577 Tooltip::for_action("Accept Generated Command", &menu::Confirm, cx)
578 })
579 .on_click(cx.listener(|_, _, cx| {
580 cx.emit(PromptEditorEvent::ConfirmRequested { execute: false });
581 })),
582 IconButton::new("confirm", IconName::Play)
583 .icon_color(Color::Info)
584 .shape(IconButtonShape::Square)
585 .tooltip(|cx| {
586 Tooltip::for_action(
587 "Execute Generated Command",
588 &menu::SecondaryConfirm,
589 cx,
590 )
591 })
592 .on_click(cx.listener(|_, _, cx| {
593 cx.emit(PromptEditorEvent::ConfirmRequested { execute: true });
594 })),
595 ]
596 }
597 }
598 };
599
600 h_flex()
601 .bg(cx.theme().colors().editor_background)
602 .border_y_1()
603 .border_color(cx.theme().status().info_border)
604 .py_2()
605 .h_full()
606 .w_full()
607 .on_action(cx.listener(Self::confirm))
608 .on_action(cx.listener(Self::secondary_confirm))
609 .on_action(cx.listener(Self::cancel))
610 .on_action(cx.listener(Self::move_up))
611 .on_action(cx.listener(Self::move_down))
612 .child(
613 h_flex()
614 .w_12()
615 .justify_center()
616 .gap_2()
617 .child(LanguageModelSelector::new(
618 {
619 let fs = self.fs.clone();
620 move |model, cx| {
621 update_settings_file::<AssistantSettings>(
622 fs.clone(),
623 cx,
624 move |settings, _| settings.set_model(model.clone()),
625 );
626 }
627 },
628 IconButton::new("context", IconName::SettingsAlt)
629 .shape(IconButtonShape::Square)
630 .icon_size(IconSize::Small)
631 .icon_color(Color::Muted)
632 .tooltip(move |cx| {
633 Tooltip::with_meta(
634 format!(
635 "Using {}",
636 LanguageModelRegistry::read_global(cx)
637 .active_model()
638 .map(|model| model.name().0)
639 .unwrap_or_else(|| "No model selected".into()),
640 ),
641 None,
642 "Change Model",
643 cx,
644 )
645 }),
646 ))
647 .children(
648 if let CodegenStatus::Error(error) = &self.codegen.read(cx).status {
649 let error_message = SharedString::from(error.to_string());
650 Some(
651 div()
652 .id("error")
653 .tooltip(move |cx| Tooltip::text(error_message.clone(), cx))
654 .child(
655 Icon::new(IconName::XCircle)
656 .size(IconSize::Small)
657 .color(Color::Error),
658 ),
659 )
660 } else {
661 None
662 },
663 ),
664 )
665 .child(div().flex_1().child(self.render_prompt_editor(cx)))
666 .child(
667 h_flex()
668 .gap_1()
669 .pr_4()
670 .children(self.render_token_count(cx))
671 .children(buttons),
672 )
673 }
674}
675
676impl FocusableView for PromptEditor {
677 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
678 self.editor.focus_handle(cx)
679 }
680}
681
682impl PromptEditor {
683 const MAX_LINES: u8 = 8;
684
685 #[allow(clippy::too_many_arguments)]
686 fn new(
687 id: TerminalInlineAssistId,
688 prompt_history: VecDeque<String>,
689 prompt_buffer: Model<MultiBuffer>,
690 codegen: Model<Codegen>,
691 assistant_panel: Option<&View<AssistantPanel>>,
692 workspace: Option<WeakView<Workspace>>,
693 fs: Arc<dyn Fs>,
694 cx: &mut ViewContext<Self>,
695 ) -> Self {
696 let prompt_editor = cx.new_view(|cx| {
697 let mut editor = Editor::new(
698 EditorMode::AutoHeight {
699 max_lines: Self::MAX_LINES as usize,
700 },
701 prompt_buffer,
702 None,
703 false,
704 cx,
705 );
706 editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
707 editor.set_placeholder_text(Self::placeholder_text(cx), cx);
708 editor
709 });
710
711 let mut token_count_subscriptions = Vec::new();
712 if let Some(assistant_panel) = assistant_panel {
713 token_count_subscriptions
714 .push(cx.subscribe(assistant_panel, Self::handle_assistant_panel_event));
715 }
716
717 let mut this = Self {
718 id,
719 height_in_lines: 1,
720 editor: prompt_editor,
721 edited_since_done: false,
722 prompt_history,
723 prompt_history_ix: None,
724 pending_prompt: String::new(),
725 _codegen_subscription: cx.observe(&codegen, Self::handle_codegen_changed),
726 editor_subscriptions: Vec::new(),
727 codegen,
728 fs,
729 pending_token_count: Task::ready(Ok(())),
730 token_count: None,
731 _token_count_subscriptions: token_count_subscriptions,
732 workspace,
733 };
734 this.count_lines(cx);
735 this.count_tokens(cx);
736 this.subscribe_to_editor(cx);
737 this
738 }
739
740 fn placeholder_text(cx: &WindowContext) -> String {
741 let context_keybinding = text_for_action(&crate::ToggleFocus, cx)
742 .map(|keybinding| format!(" • {keybinding} for context"))
743 .unwrap_or_default();
744
745 format!("Generate…{context_keybinding} • ↓↑ for history")
746 }
747
748 fn subscribe_to_editor(&mut self, cx: &mut ViewContext<Self>) {
749 self.editor_subscriptions.clear();
750 self.editor_subscriptions
751 .push(cx.observe(&self.editor, Self::handle_prompt_editor_changed));
752 self.editor_subscriptions
753 .push(cx.subscribe(&self.editor, Self::handle_prompt_editor_events));
754 }
755
756 fn prompt(&self, cx: &AppContext) -> String {
757 self.editor.read(cx).text(cx)
758 }
759
760 fn count_lines(&mut self, cx: &mut ViewContext<Self>) {
761 let height_in_lines = cmp::max(
762 2, // Make the editor at least two lines tall, to account for padding and buttons.
763 cmp::min(
764 self.editor
765 .update(cx, |editor, cx| editor.max_point(cx).row().0 + 1),
766 Self::MAX_LINES as u32,
767 ),
768 ) as u8;
769
770 if height_in_lines != self.height_in_lines {
771 self.height_in_lines = height_in_lines;
772 cx.emit(PromptEditorEvent::Resized { height_in_lines });
773 }
774 }
775
776 fn handle_assistant_panel_event(
777 &mut self,
778 _: View<AssistantPanel>,
779 event: &AssistantPanelEvent,
780 cx: &mut ViewContext<Self>,
781 ) {
782 let AssistantPanelEvent::ContextEdited { .. } = event;
783 self.count_tokens(cx);
784 }
785
786 fn count_tokens(&mut self, cx: &mut ViewContext<Self>) {
787 let assist_id = self.id;
788 let Some(model) = LanguageModelRegistry::read_global(cx).active_model() else {
789 return;
790 };
791 self.pending_token_count = cx.spawn(|this, mut cx| async move {
792 cx.background_executor().timer(Duration::from_secs(1)).await;
793 let request =
794 cx.update_global(|inline_assistant: &mut TerminalInlineAssistant, cx| {
795 inline_assistant.request_for_inline_assist(assist_id, cx)
796 })??;
797
798 let token_count = cx.update(|cx| model.count_tokens(request, cx))?.await?;
799 this.update(&mut cx, |this, cx| {
800 this.token_count = Some(token_count);
801 cx.notify();
802 })
803 })
804 }
805
806 fn handle_prompt_editor_changed(&mut self, _: View<Editor>, cx: &mut ViewContext<Self>) {
807 self.count_lines(cx);
808 }
809
810 fn handle_prompt_editor_events(
811 &mut self,
812 _: View<Editor>,
813 event: &EditorEvent,
814 cx: &mut ViewContext<Self>,
815 ) {
816 match event {
817 EditorEvent::Edited { .. } => {
818 let prompt = self.editor.read(cx).text(cx);
819 if self
820 .prompt_history_ix
821 .map_or(true, |ix| self.prompt_history[ix] != prompt)
822 {
823 self.prompt_history_ix.take();
824 self.pending_prompt = prompt;
825 }
826
827 self.edited_since_done = true;
828 cx.notify();
829 }
830 EditorEvent::BufferEdited => {
831 self.count_tokens(cx);
832 }
833 _ => {}
834 }
835 }
836
837 fn handle_codegen_changed(&mut self, _: Model<Codegen>, cx: &mut ViewContext<Self>) {
838 match &self.codegen.read(cx).status {
839 CodegenStatus::Idle => {
840 self.editor
841 .update(cx, |editor, _| editor.set_read_only(false));
842 }
843 CodegenStatus::Pending => {
844 self.editor
845 .update(cx, |editor, _| editor.set_read_only(true));
846 }
847 CodegenStatus::Done | CodegenStatus::Error(_) => {
848 self.edited_since_done = false;
849 self.editor
850 .update(cx, |editor, _| editor.set_read_only(false));
851 }
852 }
853 }
854
855 fn cancel(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
856 match &self.codegen.read(cx).status {
857 CodegenStatus::Idle | CodegenStatus::Done | CodegenStatus::Error(_) => {
858 cx.emit(PromptEditorEvent::CancelRequested);
859 }
860 CodegenStatus::Pending => {
861 cx.emit(PromptEditorEvent::StopRequested);
862 }
863 }
864 }
865
866 fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
867 match &self.codegen.read(cx).status {
868 CodegenStatus::Idle => {
869 if !self.editor.read(cx).text(cx).trim().is_empty() {
870 cx.emit(PromptEditorEvent::StartRequested);
871 }
872 }
873 CodegenStatus::Pending => {
874 cx.emit(PromptEditorEvent::DismissRequested);
875 }
876 CodegenStatus::Done => {
877 if self.edited_since_done {
878 cx.emit(PromptEditorEvent::StartRequested);
879 } else {
880 cx.emit(PromptEditorEvent::ConfirmRequested { execute: false });
881 }
882 }
883 CodegenStatus::Error(_) => {
884 cx.emit(PromptEditorEvent::StartRequested);
885 }
886 }
887 }
888
889 fn secondary_confirm(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext<Self>) {
890 if matches!(self.codegen.read(cx).status, CodegenStatus::Done) {
891 cx.emit(PromptEditorEvent::ConfirmRequested { execute: true });
892 }
893 }
894
895 fn move_up(&mut self, _: &MoveUp, cx: &mut ViewContext<Self>) {
896 if let Some(ix) = self.prompt_history_ix {
897 if ix > 0 {
898 self.prompt_history_ix = Some(ix - 1);
899 let prompt = self.prompt_history[ix - 1].as_str();
900 self.editor.update(cx, |editor, cx| {
901 editor.set_text(prompt, cx);
902 editor.move_to_beginning(&Default::default(), cx);
903 });
904 }
905 } else if !self.prompt_history.is_empty() {
906 self.prompt_history_ix = Some(self.prompt_history.len() - 1);
907 let prompt = self.prompt_history[self.prompt_history.len() - 1].as_str();
908 self.editor.update(cx, |editor, cx| {
909 editor.set_text(prompt, cx);
910 editor.move_to_beginning(&Default::default(), cx);
911 });
912 }
913 }
914
915 fn move_down(&mut self, _: &MoveDown, cx: &mut ViewContext<Self>) {
916 if let Some(ix) = self.prompt_history_ix {
917 if ix < self.prompt_history.len() - 1 {
918 self.prompt_history_ix = Some(ix + 1);
919 let prompt = self.prompt_history[ix + 1].as_str();
920 self.editor.update(cx, |editor, cx| {
921 editor.set_text(prompt, cx);
922 editor.move_to_end(&Default::default(), cx)
923 });
924 } else {
925 self.prompt_history_ix = None;
926 let prompt = self.pending_prompt.as_str();
927 self.editor.update(cx, |editor, cx| {
928 editor.set_text(prompt, cx);
929 editor.move_to_end(&Default::default(), cx)
930 });
931 }
932 }
933 }
934
935 fn render_token_count(&self, cx: &mut ViewContext<Self>) -> Option<impl IntoElement> {
936 let model = LanguageModelRegistry::read_global(cx).active_model()?;
937 let token_count = self.token_count?;
938 let max_token_count = model.max_token_count();
939
940 let remaining_tokens = max_token_count as isize - token_count as isize;
941 let token_count_color = if remaining_tokens <= 0 {
942 Color::Error
943 } else if token_count as f32 / max_token_count as f32 >= 0.8 {
944 Color::Warning
945 } else {
946 Color::Muted
947 };
948
949 let mut token_count = h_flex()
950 .id("token_count")
951 .gap_0p5()
952 .child(
953 Label::new(humanize_token_count(token_count))
954 .size(LabelSize::Small)
955 .color(token_count_color),
956 )
957 .child(Label::new("/").size(LabelSize::Small).color(Color::Muted))
958 .child(
959 Label::new(humanize_token_count(max_token_count))
960 .size(LabelSize::Small)
961 .color(Color::Muted),
962 );
963 if let Some(workspace) = self.workspace.clone() {
964 token_count = token_count
965 .tooltip(|cx| {
966 Tooltip::with_meta(
967 "Tokens Used by Inline Assistant",
968 None,
969 "Click to Open Assistant Panel",
970 cx,
971 )
972 })
973 .cursor_pointer()
974 .on_mouse_down(gpui::MouseButton::Left, |_, cx| cx.stop_propagation())
975 .on_click(move |_, cx| {
976 cx.stop_propagation();
977 workspace
978 .update(cx, |workspace, cx| {
979 workspace.focus_panel::<AssistantPanel>(cx)
980 })
981 .ok();
982 });
983 } else {
984 token_count = token_count
985 .cursor_default()
986 .tooltip(|cx| Tooltip::text("Tokens Used by Inline Assistant", cx));
987 }
988
989 Some(token_count)
990 }
991
992 fn render_prompt_editor(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
993 let settings = ThemeSettings::get_global(cx);
994 let text_style = TextStyle {
995 color: if self.editor.read(cx).read_only(cx) {
996 cx.theme().colors().text_disabled
997 } else {
998 cx.theme().colors().text
999 },
1000 font_family: settings.buffer_font.family.clone(),
1001 font_fallbacks: settings.buffer_font.fallbacks.clone(),
1002 font_size: settings.buffer_font_size.into(),
1003 font_weight: settings.buffer_font.weight,
1004 line_height: relative(settings.buffer_line_height.value()),
1005 ..Default::default()
1006 };
1007 EditorElement::new(
1008 &self.editor,
1009 EditorStyle {
1010 background: cx.theme().colors().editor_background,
1011 local_player: cx.theme().players().local(),
1012 text: text_style,
1013 ..Default::default()
1014 },
1015 )
1016 }
1017}
1018
1019#[derive(Debug)]
1020pub enum CodegenEvent {
1021 Finished,
1022}
1023
1024impl EventEmitter<CodegenEvent> for Codegen {}
1025
1026const CLEAR_INPUT: &str = "\x15";
1027const CARRIAGE_RETURN: &str = "\x0d";
1028
1029struct TerminalTransaction {
1030 terminal: Model<Terminal>,
1031}
1032
1033impl TerminalTransaction {
1034 pub fn start(terminal: Model<Terminal>) -> Self {
1035 Self { terminal }
1036 }
1037
1038 pub fn push(&mut self, hunk: String, cx: &mut AppContext) {
1039 // Ensure that the assistant cannot accidentally execute commands that are streamed into the terminal
1040 let input = Self::sanitize_input(hunk);
1041 self.terminal
1042 .update(cx, |terminal, _| terminal.input(input));
1043 }
1044
1045 pub fn undo(&self, cx: &mut AppContext) {
1046 self.terminal
1047 .update(cx, |terminal, _| terminal.input(CLEAR_INPUT.to_string()));
1048 }
1049
1050 pub fn complete(&self, cx: &mut AppContext) {
1051 self.terminal.update(cx, |terminal, _| {
1052 terminal.input(CARRIAGE_RETURN.to_string())
1053 });
1054 }
1055
1056 fn sanitize_input(input: String) -> String {
1057 input.replace(['\r', '\n'], "")
1058 }
1059}
1060
1061pub struct Codegen {
1062 status: CodegenStatus,
1063 telemetry: Option<Arc<Telemetry>>,
1064 terminal: Model<Terminal>,
1065 generation: Task<()>,
1066 message_id: Option<String>,
1067 transaction: Option<TerminalTransaction>,
1068}
1069
1070impl Codegen {
1071 pub fn new(terminal: Model<Terminal>, telemetry: Option<Arc<Telemetry>>) -> Self {
1072 Self {
1073 terminal,
1074 telemetry,
1075 status: CodegenStatus::Idle,
1076 generation: Task::ready(()),
1077 message_id: None,
1078 transaction: None,
1079 }
1080 }
1081
1082 pub fn start(&mut self, prompt: LanguageModelRequest, cx: &mut ModelContext<Self>) {
1083 let Some(model) = LanguageModelRegistry::read_global(cx).active_model() else {
1084 return;
1085 };
1086
1087 let model_api_key = model.api_key(cx);
1088 let http_client = cx.http_client();
1089 let telemetry = self.telemetry.clone();
1090 self.status = CodegenStatus::Pending;
1091 self.transaction = Some(TerminalTransaction::start(self.terminal.clone()));
1092 self.generation = cx.spawn(|this, mut cx| async move {
1093 let model_telemetry_id = model.telemetry_id();
1094 let model_provider_id = model.provider_id();
1095 let response = model.stream_completion_text(prompt, &cx).await;
1096 let generate = async {
1097 let message_id = response
1098 .as_ref()
1099 .ok()
1100 .and_then(|response| response.message_id.clone());
1101
1102 let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1);
1103
1104 let task = cx.background_executor().spawn({
1105 let message_id = message_id.clone();
1106 let executor = cx.background_executor().clone();
1107 async move {
1108 let mut response_latency = None;
1109 let request_start = Instant::now();
1110 let task = async {
1111 let mut chunks = response?.stream;
1112 while let Some(chunk) = chunks.next().await {
1113 if response_latency.is_none() {
1114 response_latency = Some(request_start.elapsed());
1115 }
1116 let chunk = chunk?;
1117 hunks_tx.send(chunk).await?;
1118 }
1119
1120 anyhow::Ok(())
1121 };
1122
1123 let result = task.await;
1124
1125 let error_message = result.as_ref().err().map(|error| error.to_string());
1126 report_assistant_event(
1127 AssistantEvent {
1128 conversation_id: None,
1129 kind: AssistantKind::InlineTerminal,
1130 message_id,
1131 phase: AssistantPhase::Response,
1132 model: model_telemetry_id,
1133 model_provider: model_provider_id.to_string(),
1134 response_latency,
1135 error_message,
1136 language_name: None,
1137 },
1138 telemetry,
1139 http_client,
1140 model_api_key,
1141 &executor,
1142 );
1143
1144 result?;
1145 anyhow::Ok(())
1146 }
1147 });
1148
1149 this.update(&mut cx, |this, _| {
1150 this.message_id = message_id;
1151 })?;
1152
1153 while let Some(hunk) = hunks_rx.next().await {
1154 this.update(&mut cx, |this, cx| {
1155 if let Some(transaction) = &mut this.transaction {
1156 transaction.push(hunk, cx);
1157 cx.notify();
1158 }
1159 })?;
1160 }
1161
1162 task.await?;
1163 anyhow::Ok(())
1164 };
1165
1166 let result = generate.await;
1167
1168 this.update(&mut cx, |this, cx| {
1169 if let Err(error) = result {
1170 this.status = CodegenStatus::Error(error);
1171 } else {
1172 this.status = CodegenStatus::Done;
1173 }
1174 cx.emit(CodegenEvent::Finished);
1175 cx.notify();
1176 })
1177 .ok();
1178 });
1179 cx.notify();
1180 }
1181
1182 pub fn stop(&mut self, cx: &mut ModelContext<Self>) {
1183 self.status = CodegenStatus::Done;
1184 self.generation = Task::ready(());
1185 cx.emit(CodegenEvent::Finished);
1186 cx.notify();
1187 }
1188
1189 pub fn complete(&mut self, cx: &mut ModelContext<Self>) {
1190 if let Some(transaction) = self.transaction.take() {
1191 transaction.complete(cx);
1192 }
1193 }
1194
1195 pub fn undo(&mut self, cx: &mut ModelContext<Self>) {
1196 if let Some(transaction) = self.transaction.take() {
1197 transaction.undo(cx);
1198 }
1199 }
1200}
1201
1202enum CodegenStatus {
1203 Idle,
1204 Pending,
1205 Done,
1206 Error(anyhow::Error),
1207}