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