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 cache: false,
281 });
282
283 Ok(LanguageModelRequest {
284 messages,
285 tools: Vec::new(),
286 stop: Vec::new(),
287 temperature: 1.0,
288 })
289 }
290
291 fn finish_assist(
292 &mut self,
293 assist_id: TerminalInlineAssistId,
294 undo: bool,
295 cx: &mut WindowContext,
296 ) {
297 self.dismiss_assist(assist_id, 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(cx);
305 })
306 .log_err();
307 assist.codegen.update(cx, |codegen, cx| {
308 if undo {
309 codegen.undo(cx);
310 } else {
311 codegen.complete(cx);
312 }
313 });
314 }
315 }
316
317 fn dismiss_assist(
318 &mut self,
319 assist_id: TerminalInlineAssistId,
320 cx: &mut WindowContext,
321 ) -> bool {
322 let Some(assist) = self.assists.get_mut(&assist_id) else {
323 return false;
324 };
325 if assist.prompt_editor.is_none() {
326 return false;
327 }
328 assist.prompt_editor = None;
329 assist
330 .terminal
331 .update(cx, |this, cx| {
332 this.clear_block_below_cursor(cx);
333 this.focus_handle(cx).focus(cx);
334 })
335 .is_ok()
336 }
337
338 fn insert_prompt_editor_into_terminal(
339 &mut self,
340 assist_id: TerminalInlineAssistId,
341 height: u8,
342 cx: &mut WindowContext,
343 ) {
344 if let Some(assist) = self.assists.get_mut(&assist_id) {
345 if let Some(prompt_editor) = assist.prompt_editor.as_ref().cloned() {
346 assist
347 .terminal
348 .update(cx, |terminal, cx| {
349 terminal.clear_block_below_cursor(cx);
350 let block = terminal_view::BlockProperties {
351 height,
352 render: Box::new(move |_| prompt_editor.clone().into_any_element()),
353 };
354 terminal.set_block_below_cursor(block, cx);
355 })
356 .log_err();
357 }
358 }
359 }
360}
361
362struct TerminalInlineAssist {
363 terminal: WeakView<TerminalView>,
364 prompt_editor: Option<View<PromptEditor>>,
365 codegen: Model<Codegen>,
366 workspace: Option<WeakView<Workspace>>,
367 include_context: bool,
368 _subscriptions: Vec<Subscription>,
369}
370
371impl TerminalInlineAssist {
372 pub fn new(
373 assist_id: TerminalInlineAssistId,
374 terminal: &View<TerminalView>,
375 include_context: bool,
376 prompt_editor: View<PromptEditor>,
377 workspace: Option<WeakView<Workspace>>,
378 cx: &mut WindowContext,
379 ) -> Self {
380 let codegen = prompt_editor.read(cx).codegen.clone();
381 Self {
382 terminal: terminal.downgrade(),
383 prompt_editor: Some(prompt_editor.clone()),
384 codegen: codegen.clone(),
385 workspace: workspace.clone(),
386 include_context,
387 _subscriptions: vec![
388 cx.subscribe(&prompt_editor, |prompt_editor, event, cx| {
389 TerminalInlineAssistant::update_global(cx, |this, cx| {
390 this.handle_prompt_editor_event(prompt_editor, event, cx)
391 })
392 }),
393 cx.subscribe(&codegen, move |codegen, event, cx| {
394 TerminalInlineAssistant::update_global(cx, |this, cx| match event {
395 CodegenEvent::Finished => {
396 let assist = if let Some(assist) = this.assists.get(&assist_id) {
397 assist
398 } else {
399 return;
400 };
401
402 if let CodegenStatus::Error(error) = &codegen.read(cx).status {
403 if assist.prompt_editor.is_none() {
404 if let Some(workspace) = assist
405 .workspace
406 .as_ref()
407 .and_then(|workspace| workspace.upgrade())
408 {
409 let error =
410 format!("Terminal inline assistant error: {}", error);
411 workspace.update(cx, |workspace, cx| {
412 struct InlineAssistantError;
413
414 let id =
415 NotificationId::identified::<InlineAssistantError>(
416 assist_id.0,
417 );
418
419 workspace.show_toast(Toast::new(id, error), cx);
420 })
421 }
422 }
423 }
424
425 if assist.prompt_editor.is_none() {
426 this.finish_assist(assist_id, false, cx);
427 }
428 }
429 })
430 }),
431 ],
432 }
433 }
434}
435
436enum PromptEditorEvent {
437 StartRequested,
438 StopRequested,
439 ConfirmRequested,
440 CancelRequested,
441 DismissRequested,
442 Resized { height_in_lines: u8 },
443}
444
445struct PromptEditor {
446 id: TerminalInlineAssistId,
447 fs: Arc<dyn Fs>,
448 height_in_lines: u8,
449 editor: View<Editor>,
450 edited_since_done: bool,
451 prompt_history: VecDeque<String>,
452 prompt_history_ix: Option<usize>,
453 pending_prompt: String,
454 codegen: Model<Codegen>,
455 _codegen_subscription: Subscription,
456 editor_subscriptions: Vec<Subscription>,
457 pending_token_count: Task<Result<()>>,
458 token_count: Option<usize>,
459 _token_count_subscriptions: Vec<Subscription>,
460 workspace: Option<WeakView<Workspace>>,
461}
462
463impl EventEmitter<PromptEditorEvent> for PromptEditor {}
464
465impl Render for PromptEditor {
466 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
467 let buttons = match &self.codegen.read(cx).status {
468 CodegenStatus::Idle => {
469 vec![
470 IconButton::new("cancel", IconName::Close)
471 .icon_color(Color::Muted)
472 .shape(IconButtonShape::Square)
473 .tooltip(|cx| Tooltip::for_action("Cancel Assist", &menu::Cancel, cx))
474 .on_click(
475 cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested)),
476 ),
477 IconButton::new("start", IconName::SparkleAlt)
478 .icon_color(Color::Muted)
479 .shape(IconButtonShape::Square)
480 .tooltip(|cx| Tooltip::for_action("Generate", &menu::Confirm, cx))
481 .on_click(
482 cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::StartRequested)),
483 ),
484 ]
485 }
486 CodegenStatus::Pending => {
487 vec![
488 IconButton::new("cancel", IconName::Close)
489 .icon_color(Color::Muted)
490 .shape(IconButtonShape::Square)
491 .tooltip(|cx| Tooltip::text("Cancel Assist", cx))
492 .on_click(
493 cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested)),
494 ),
495 IconButton::new("stop", IconName::Stop)
496 .icon_color(Color::Error)
497 .shape(IconButtonShape::Square)
498 .tooltip(|cx| {
499 Tooltip::with_meta(
500 "Interrupt Generation",
501 Some(&menu::Cancel),
502 "Changes won't be discarded",
503 cx,
504 )
505 })
506 .on_click(
507 cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::StopRequested)),
508 ),
509 ]
510 }
511 CodegenStatus::Error(_) | CodegenStatus::Done => {
512 vec![
513 IconButton::new("cancel", IconName::Close)
514 .icon_color(Color::Muted)
515 .shape(IconButtonShape::Square)
516 .tooltip(|cx| Tooltip::for_action("Cancel Assist", &menu::Cancel, cx))
517 .on_click(
518 cx.listener(|_, _, cx| cx.emit(PromptEditorEvent::CancelRequested)),
519 ),
520 if self.edited_since_done {
521 IconButton::new("restart", IconName::RotateCw)
522 .icon_color(Color::Info)
523 .shape(IconButtonShape::Square)
524 .tooltip(|cx| {
525 Tooltip::with_meta(
526 "Restart Generation",
527 Some(&menu::Confirm),
528 "Changes will be discarded",
529 cx,
530 )
531 })
532 .on_click(cx.listener(|_, _, cx| {
533 cx.emit(PromptEditorEvent::StartRequested);
534 }))
535 } else {
536 IconButton::new("confirm", IconName::Play)
537 .icon_color(Color::Info)
538 .shape(IconButtonShape::Square)
539 .tooltip(|cx| {
540 Tooltip::for_action("Execute generated command", &menu::Confirm, cx)
541 })
542 .on_click(cx.listener(|_, _, cx| {
543 cx.emit(PromptEditorEvent::ConfirmRequested);
544 }))
545 },
546 ]
547 }
548 };
549
550 h_flex()
551 .bg(cx.theme().colors().editor_background)
552 .border_y_1()
553 .border_color(cx.theme().status().info_border)
554 .py_1p5()
555 .h_full()
556 .w_full()
557 .on_action(cx.listener(Self::confirm))
558 .on_action(cx.listener(Self::cancel))
559 .on_action(cx.listener(Self::move_up))
560 .on_action(cx.listener(Self::move_down))
561 .child(
562 h_flex()
563 .w_12()
564 .justify_center()
565 .gap_2()
566 .child(ModelSelector::new(
567 self.fs.clone(),
568 IconButton::new("context", IconName::SlidersAlt)
569 .shape(IconButtonShape::Square)
570 .icon_size(IconSize::Small)
571 .icon_color(Color::Muted)
572 .tooltip(move |cx| {
573 Tooltip::with_meta(
574 format!(
575 "Using {}",
576 LanguageModelRegistry::read_global(cx)
577 .active_model()
578 .map(|model| model.name().0)
579 .unwrap_or_else(|| "No model selected".into()),
580 ),
581 None,
582 "Change Model",
583 cx,
584 )
585 }),
586 ))
587 .children(
588 if let CodegenStatus::Error(error) = &self.codegen.read(cx).status {
589 let error_message = SharedString::from(error.to_string());
590 Some(
591 div()
592 .id("error")
593 .tooltip(move |cx| Tooltip::text(error_message.clone(), cx))
594 .child(
595 Icon::new(IconName::XCircle)
596 .size(IconSize::Small)
597 .color(Color::Error),
598 ),
599 )
600 } else {
601 None
602 },
603 ),
604 )
605 .child(div().flex_1().child(self.render_prompt_editor(cx)))
606 .child(
607 h_flex()
608 .gap_2()
609 .pr_4()
610 .children(self.render_token_count(cx))
611 .children(buttons),
612 )
613 }
614}
615
616impl FocusableView for PromptEditor {
617 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
618 self.editor.focus_handle(cx)
619 }
620}
621
622impl PromptEditor {
623 const MAX_LINES: u8 = 8;
624
625 #[allow(clippy::too_many_arguments)]
626 fn new(
627 id: TerminalInlineAssistId,
628 prompt_history: VecDeque<String>,
629 prompt_buffer: Model<MultiBuffer>,
630 codegen: Model<Codegen>,
631 assistant_panel: Option<&View<AssistantPanel>>,
632 workspace: Option<WeakView<Workspace>>,
633 fs: Arc<dyn Fs>,
634 cx: &mut ViewContext<Self>,
635 ) -> Self {
636 let prompt_editor = cx.new_view(|cx| {
637 let mut editor = Editor::new(
638 EditorMode::AutoHeight {
639 max_lines: Self::MAX_LINES as usize,
640 },
641 prompt_buffer,
642 None,
643 false,
644 cx,
645 );
646 editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
647 editor.set_placeholder_text("Add a prompt…", cx);
648 editor
649 });
650
651 let mut token_count_subscriptions = Vec::new();
652 if let Some(assistant_panel) = assistant_panel {
653 token_count_subscriptions
654 .push(cx.subscribe(assistant_panel, Self::handle_assistant_panel_event));
655 }
656
657 let mut this = Self {
658 id,
659 height_in_lines: 1,
660 editor: prompt_editor,
661 edited_since_done: false,
662 prompt_history,
663 prompt_history_ix: None,
664 pending_prompt: String::new(),
665 _codegen_subscription: cx.observe(&codegen, Self::handle_codegen_changed),
666 editor_subscriptions: Vec::new(),
667 codegen,
668 fs,
669 pending_token_count: Task::ready(Ok(())),
670 token_count: None,
671 _token_count_subscriptions: token_count_subscriptions,
672 workspace,
673 };
674 this.count_lines(cx);
675 this.count_tokens(cx);
676 this.subscribe_to_editor(cx);
677 this
678 }
679
680 fn subscribe_to_editor(&mut self, cx: &mut ViewContext<Self>) {
681 self.editor_subscriptions.clear();
682 self.editor_subscriptions
683 .push(cx.observe(&self.editor, Self::handle_prompt_editor_changed));
684 self.editor_subscriptions
685 .push(cx.subscribe(&self.editor, Self::handle_prompt_editor_events));
686 }
687
688 fn prompt(&self, cx: &AppContext) -> String {
689 self.editor.read(cx).text(cx)
690 }
691
692 fn count_lines(&mut self, cx: &mut ViewContext<Self>) {
693 let height_in_lines = cmp::max(
694 2, // Make the editor at least two lines tall, to account for padding and buttons.
695 cmp::min(
696 self.editor
697 .update(cx, |editor, cx| editor.max_point(cx).row().0 + 1),
698 Self::MAX_LINES as u32,
699 ),
700 ) as u8;
701
702 if height_in_lines != self.height_in_lines {
703 self.height_in_lines = height_in_lines;
704 cx.emit(PromptEditorEvent::Resized { height_in_lines });
705 }
706 }
707
708 fn handle_assistant_panel_event(
709 &mut self,
710 _: View<AssistantPanel>,
711 event: &AssistantPanelEvent,
712 cx: &mut ViewContext<Self>,
713 ) {
714 let AssistantPanelEvent::ContextEdited { .. } = event;
715 self.count_tokens(cx);
716 }
717
718 fn count_tokens(&mut self, cx: &mut ViewContext<Self>) {
719 let assist_id = self.id;
720 let Some(model) = LanguageModelRegistry::read_global(cx).active_model() else {
721 return;
722 };
723 self.pending_token_count = cx.spawn(|this, mut cx| async move {
724 cx.background_executor().timer(Duration::from_secs(1)).await;
725 let request =
726 cx.update_global(|inline_assistant: &mut TerminalInlineAssistant, cx| {
727 inline_assistant.request_for_inline_assist(assist_id, cx)
728 })??;
729
730 let token_count = cx.update(|cx| model.count_tokens(request, cx))?.await?;
731 this.update(&mut cx, |this, cx| {
732 this.token_count = Some(token_count);
733 cx.notify();
734 })
735 })
736 }
737
738 fn handle_prompt_editor_changed(&mut self, _: View<Editor>, cx: &mut ViewContext<Self>) {
739 self.count_lines(cx);
740 }
741
742 fn handle_prompt_editor_events(
743 &mut self,
744 _: View<Editor>,
745 event: &EditorEvent,
746 cx: &mut ViewContext<Self>,
747 ) {
748 match event {
749 EditorEvent::Edited { .. } => {
750 let prompt = self.editor.read(cx).text(cx);
751 if self
752 .prompt_history_ix
753 .map_or(true, |ix| self.prompt_history[ix] != prompt)
754 {
755 self.prompt_history_ix.take();
756 self.pending_prompt = prompt;
757 }
758
759 self.edited_since_done = true;
760 cx.notify();
761 }
762 EditorEvent::BufferEdited => {
763 self.count_tokens(cx);
764 }
765 _ => {}
766 }
767 }
768
769 fn handle_codegen_changed(&mut self, _: Model<Codegen>, cx: &mut ViewContext<Self>) {
770 match &self.codegen.read(cx).status {
771 CodegenStatus::Idle => {
772 self.editor
773 .update(cx, |editor, _| editor.set_read_only(false));
774 }
775 CodegenStatus::Pending => {
776 self.editor
777 .update(cx, |editor, _| editor.set_read_only(true));
778 }
779 CodegenStatus::Done | CodegenStatus::Error(_) => {
780 self.edited_since_done = false;
781 self.editor
782 .update(cx, |editor, _| editor.set_read_only(false));
783 }
784 }
785 }
786
787 fn cancel(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
788 match &self.codegen.read(cx).status {
789 CodegenStatus::Idle | CodegenStatus::Done | CodegenStatus::Error(_) => {
790 cx.emit(PromptEditorEvent::CancelRequested);
791 }
792 CodegenStatus::Pending => {
793 cx.emit(PromptEditorEvent::StopRequested);
794 }
795 }
796 }
797
798 fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
799 match &self.codegen.read(cx).status {
800 CodegenStatus::Idle => {
801 if !self.editor.read(cx).text(cx).trim().is_empty() {
802 cx.emit(PromptEditorEvent::StartRequested);
803 }
804 }
805 CodegenStatus::Pending => {
806 cx.emit(PromptEditorEvent::DismissRequested);
807 }
808 CodegenStatus::Done => {
809 if self.edited_since_done {
810 cx.emit(PromptEditorEvent::StartRequested);
811 } else {
812 cx.emit(PromptEditorEvent::ConfirmRequested);
813 }
814 }
815 CodegenStatus::Error(_) => {
816 cx.emit(PromptEditorEvent::StartRequested);
817 }
818 }
819 }
820
821 fn move_up(&mut self, _: &MoveUp, cx: &mut ViewContext<Self>) {
822 if let Some(ix) = self.prompt_history_ix {
823 if ix > 0 {
824 self.prompt_history_ix = Some(ix - 1);
825 let prompt = self.prompt_history[ix - 1].as_str();
826 self.editor.update(cx, |editor, cx| {
827 editor.set_text(prompt, cx);
828 editor.move_to_beginning(&Default::default(), cx);
829 });
830 }
831 } else if !self.prompt_history.is_empty() {
832 self.prompt_history_ix = Some(self.prompt_history.len() - 1);
833 let prompt = self.prompt_history[self.prompt_history.len() - 1].as_str();
834 self.editor.update(cx, |editor, cx| {
835 editor.set_text(prompt, cx);
836 editor.move_to_beginning(&Default::default(), cx);
837 });
838 }
839 }
840
841 fn move_down(&mut self, _: &MoveDown, cx: &mut ViewContext<Self>) {
842 if let Some(ix) = self.prompt_history_ix {
843 if ix < self.prompt_history.len() - 1 {
844 self.prompt_history_ix = Some(ix + 1);
845 let prompt = self.prompt_history[ix + 1].as_str();
846 self.editor.update(cx, |editor, cx| {
847 editor.set_text(prompt, cx);
848 editor.move_to_end(&Default::default(), cx)
849 });
850 } else {
851 self.prompt_history_ix = None;
852 let prompt = self.pending_prompt.as_str();
853 self.editor.update(cx, |editor, cx| {
854 editor.set_text(prompt, cx);
855 editor.move_to_end(&Default::default(), cx)
856 });
857 }
858 }
859 }
860
861 fn render_token_count(&self, cx: &mut ViewContext<Self>) -> Option<impl IntoElement> {
862 let model = LanguageModelRegistry::read_global(cx).active_model()?;
863 let token_count = self.token_count?;
864 let max_token_count = model.max_token_count();
865
866 let remaining_tokens = max_token_count as isize - token_count as isize;
867 let token_count_color = if remaining_tokens <= 0 {
868 Color::Error
869 } else if token_count as f32 / max_token_count as f32 >= 0.8 {
870 Color::Warning
871 } else {
872 Color::Muted
873 };
874
875 let mut token_count = h_flex()
876 .id("token_count")
877 .gap_0p5()
878 .child(
879 Label::new(humanize_token_count(token_count))
880 .size(LabelSize::Small)
881 .color(token_count_color),
882 )
883 .child(Label::new("/").size(LabelSize::Small).color(Color::Muted))
884 .child(
885 Label::new(humanize_token_count(max_token_count))
886 .size(LabelSize::Small)
887 .color(Color::Muted),
888 );
889 if let Some(workspace) = self.workspace.clone() {
890 token_count = token_count
891 .tooltip(|cx| {
892 Tooltip::with_meta(
893 "Tokens Used by Inline Assistant",
894 None,
895 "Click to Open Assistant Panel",
896 cx,
897 )
898 })
899 .cursor_pointer()
900 .on_mouse_down(gpui::MouseButton::Left, |_, cx| cx.stop_propagation())
901 .on_click(move |_, cx| {
902 cx.stop_propagation();
903 workspace
904 .update(cx, |workspace, cx| {
905 workspace.focus_panel::<AssistantPanel>(cx)
906 })
907 .ok();
908 });
909 } else {
910 token_count = token_count
911 .cursor_default()
912 .tooltip(|cx| Tooltip::text("Tokens Used by Inline Assistant", cx));
913 }
914
915 Some(token_count)
916 }
917
918 fn render_prompt_editor(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
919 let settings = ThemeSettings::get_global(cx);
920 let text_style = TextStyle {
921 color: if self.editor.read(cx).read_only(cx) {
922 cx.theme().colors().text_disabled
923 } else {
924 cx.theme().colors().text
925 },
926 font_family: settings.ui_font.family.clone(),
927 font_features: settings.ui_font.features.clone(),
928 font_fallbacks: settings.ui_font.fallbacks.clone(),
929 font_size: rems(0.875).into(),
930 font_weight: settings.ui_font.weight,
931 line_height: relative(1.3),
932 ..Default::default()
933 };
934 EditorElement::new(
935 &self.editor,
936 EditorStyle {
937 background: cx.theme().colors().editor_background,
938 local_player: cx.theme().players().local(),
939 text: text_style,
940 ..Default::default()
941 },
942 )
943 }
944}
945
946#[derive(Debug)]
947pub enum CodegenEvent {
948 Finished,
949}
950
951impl EventEmitter<CodegenEvent> for Codegen {}
952
953const CLEAR_INPUT: &str = "\x15";
954const CARRIAGE_RETURN: &str = "\x0d";
955
956struct TerminalTransaction {
957 terminal: Model<Terminal>,
958}
959
960impl TerminalTransaction {
961 pub fn start(terminal: Model<Terminal>) -> Self {
962 Self { terminal }
963 }
964
965 pub fn push(&mut self, hunk: String, cx: &mut AppContext) {
966 // Ensure that the assistant cannot accidentally execute commands that are streamed into the terminal
967 let input = hunk.replace(CARRIAGE_RETURN, " ");
968 self.terminal
969 .update(cx, |terminal, _| terminal.input(input));
970 }
971
972 pub fn undo(&self, cx: &mut AppContext) {
973 self.terminal
974 .update(cx, |terminal, _| terminal.input(CLEAR_INPUT.to_string()));
975 }
976
977 pub fn complete(&self, cx: &mut AppContext) {
978 self.terminal.update(cx, |terminal, _| {
979 terminal.input(CARRIAGE_RETURN.to_string())
980 });
981 }
982}
983
984pub struct Codegen {
985 status: CodegenStatus,
986 telemetry: Option<Arc<Telemetry>>,
987 terminal: Model<Terminal>,
988 generation: Task<()>,
989 transaction: Option<TerminalTransaction>,
990}
991
992impl Codegen {
993 pub fn new(terminal: Model<Terminal>, telemetry: Option<Arc<Telemetry>>) -> Self {
994 Self {
995 terminal,
996 telemetry,
997 status: CodegenStatus::Idle,
998 generation: Task::ready(()),
999 transaction: None,
1000 }
1001 }
1002
1003 pub fn start(&mut self, prompt: LanguageModelRequest, cx: &mut ModelContext<Self>) {
1004 let Some(model) = LanguageModelRegistry::read_global(cx).active_model() else {
1005 return;
1006 };
1007
1008 let telemetry = self.telemetry.clone();
1009 self.status = CodegenStatus::Pending;
1010 self.transaction = Some(TerminalTransaction::start(self.terminal.clone()));
1011 self.generation = cx.spawn(|this, mut cx| async move {
1012 let model_telemetry_id = model.telemetry_id();
1013 let response = model.stream_completion_text(prompt, &cx).await;
1014 let generate = async {
1015 let (mut hunks_tx, mut hunks_rx) = mpsc::channel(1);
1016
1017 let task = cx.background_executor().spawn(async move {
1018 let mut response_latency = None;
1019 let request_start = Instant::now();
1020 let task = async {
1021 let mut chunks = response?;
1022 while let Some(chunk) = chunks.next().await {
1023 if response_latency.is_none() {
1024 response_latency = Some(request_start.elapsed());
1025 }
1026 let chunk = chunk?;
1027 hunks_tx.send(chunk).await?;
1028 }
1029
1030 anyhow::Ok(())
1031 };
1032
1033 let result = task.await;
1034
1035 let error_message = result.as_ref().err().map(|error| error.to_string());
1036 if let Some(telemetry) = telemetry {
1037 telemetry.report_assistant_event(
1038 None,
1039 telemetry_events::AssistantKind::Inline,
1040 model_telemetry_id,
1041 response_latency,
1042 error_message,
1043 );
1044 }
1045
1046 result?;
1047 anyhow::Ok(())
1048 });
1049
1050 while let Some(hunk) = hunks_rx.next().await {
1051 this.update(&mut cx, |this, cx| {
1052 if let Some(transaction) = &mut this.transaction {
1053 transaction.push(hunk, cx);
1054 cx.notify();
1055 }
1056 })?;
1057 }
1058
1059 task.await?;
1060 anyhow::Ok(())
1061 };
1062
1063 let result = generate.await;
1064
1065 this.update(&mut cx, |this, cx| {
1066 if let Err(error) = result {
1067 this.status = CodegenStatus::Error(error);
1068 } else {
1069 this.status = CodegenStatus::Done;
1070 }
1071 cx.emit(CodegenEvent::Finished);
1072 cx.notify();
1073 })
1074 .ok();
1075 });
1076 cx.notify();
1077 }
1078
1079 pub fn stop(&mut self, cx: &mut ModelContext<Self>) {
1080 self.status = CodegenStatus::Done;
1081 self.generation = Task::ready(());
1082 cx.emit(CodegenEvent::Finished);
1083 cx.notify();
1084 }
1085
1086 pub fn complete(&mut self, cx: &mut ModelContext<Self>) {
1087 if let Some(transaction) = self.transaction.take() {
1088 transaction.complete(cx);
1089 }
1090 }
1091
1092 pub fn undo(&mut self, cx: &mut ModelContext<Self>) {
1093 if let Some(transaction) = self.transaction.take() {
1094 transaction.undo(cx);
1095 }
1096 }
1097}
1098
1099enum CodegenStatus {
1100 Idle,
1101 Pending,
1102 Done,
1103 Error(anyhow::Error),
1104}