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