1use crate::{
2 assistant_settings::{AssistantDockPosition, AssistantSettings, OpenAIModel},
3 codegen::{self, Codegen, CodegenKind},
4 prompts::generate_content_prompt,
5 MessageId, MessageMetadata, MessageStatus, Role, SavedConversation, SavedConversationMetadata,
6 SavedMessage,
7};
8
9use ai::{
10 auth::ProviderCredential,
11 completion::{CompletionProvider, CompletionRequest},
12 providers::open_ai::{OpenAICompletionProvider, OpenAIRequest, RequestMessage},
13};
14
15use ai::prompts::repository_context::PromptCodeSnippet;
16use anyhow::{anyhow, Result};
17use chrono::{DateTime, Local};
18use client::{telemetry::AssistantKind, TelemetrySettings};
19use collections::{hash_map, HashMap, HashSet, VecDeque};
20use editor::{
21 display_map::{
22 BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, ToDisplayPoint,
23 },
24 scroll::autoscroll::{Autoscroll, AutoscrollStrategy},
25 Anchor, Editor, EditorEvent, MoveDown, MoveUp, MultiBufferSnapshot, ToOffset, ToPoint,
26};
27use fs::Fs;
28use futures::StreamExt;
29use gpui::{
30 actions, div, point, uniform_list, Action, AnyElement, AppContext, AsyncAppContext,
31 ClipboardItem, Div, Element, Entity, EventEmitter, FocusHandle, Focusable, FocusableView,
32 HighlightStyle, InteractiveElement, IntoElement, Model, ModelContext, ParentElement, Pixels,
33 PromptLevel, Render, StatefulInteractiveElement, Styled, Subscription, Task,
34 UniformListScrollHandle, View, ViewContext, VisualContext, WeakModel, WeakView, WindowContext,
35};
36use language::{language_settings::SoftWrap, Buffer, LanguageRegistry, ToOffset as _};
37use project::Project;
38use search::BufferSearchBar;
39use semantic_index::{SemanticIndex, SemanticIndexStatus};
40use settings::{Settings, SettingsStore};
41use std::{
42 cell::Cell,
43 cmp,
44 fmt::Write,
45 iter,
46 ops::Range,
47 path::{Path, PathBuf},
48 rc::Rc,
49 sync::Arc,
50 time::{Duration, Instant},
51};
52use ui::{
53 h_stack, v_stack, Button, ButtonCommon, ButtonLike, Clickable, Color, Icon, IconButton,
54 IconElement, Label, Selectable, StyledExt, Tooltip,
55};
56use util::{paths::CONVERSATIONS_DIR, post_inc, ResultExt, TryFutureExt};
57use uuid::Uuid;
58use workspace::{
59 dock::{DockPosition, Panel, PanelEvent},
60 searchable::Direction,
61 Save, Toast, ToggleZoom, Toolbar, Workspace,
62};
63
64actions!(
65 NewConversation,
66 Assist,
67 Split,
68 CycleMessageRole,
69 QuoteSelection,
70 ToggleFocus,
71 ResetKey,
72 InlineAssist,
73 ToggleIncludeConversation,
74 ToggleRetrieveContext,
75);
76
77pub fn init(cx: &mut AppContext) {
78 AssistantSettings::register(cx);
79 cx.add_action(
80 |this: &mut AssistantPanel,
81 _: &workspace::NewFile,
82 cx: &mut ViewContext<AssistantPanel>| {
83 this.new_conversation(cx);
84 },
85 );
86 cx.add_action(ConversationEditor::assist);
87 cx.capture_action(ConversationEditor::cancel_last_assist);
88 cx.capture_action(ConversationEditor::save);
89 cx.add_action(ConversationEditor::quote_selection);
90 cx.capture_action(ConversationEditor::copy);
91 cx.add_action(ConversationEditor::split);
92 cx.capture_action(ConversationEditor::cycle_message_role);
93 cx.add_action(AssistantPanel::save_credentials);
94 cx.add_action(AssistantPanel::reset_credentials);
95 cx.add_action(AssistantPanel::toggle_zoom);
96 cx.add_action(AssistantPanel::deploy);
97 cx.add_action(AssistantPanel::select_next_match);
98 cx.add_action(AssistantPanel::select_prev_match);
99 cx.add_action(AssistantPanel::handle_editor_cancel);
100 cx.add_action(
101 |workspace: &mut Workspace, _: &ToggleFocus, cx: &mut ViewContext<Workspace>| {
102 workspace.toggle_panel_focus::<AssistantPanel>(cx);
103 },
104 );
105 cx.add_action(AssistantPanel::inline_assist);
106 cx.add_action(AssistantPanel::cancel_last_inline_assist);
107 cx.add_action(InlineAssistant::confirm);
108 cx.add_action(InlineAssistant::cancel);
109 cx.add_action(InlineAssistant::toggle_include_conversation);
110 cx.add_action(InlineAssistant::toggle_retrieve_context);
111 cx.add_action(InlineAssistant::move_up);
112 cx.add_action(InlineAssistant::move_down);
113}
114
115#[derive(Debug)]
116pub enum AssistantPanelEvent {
117 ZoomIn,
118 ZoomOut,
119 Focus,
120 Close,
121 DockPositionChanged,
122}
123
124pub struct AssistantPanel {
125 workspace: WeakView<Workspace>,
126 width: Option<f32>,
127 height: Option<f32>,
128 active_editor_index: Option<usize>,
129 prev_active_editor_index: Option<usize>,
130 editors: Vec<View<ConversationEditor>>,
131 saved_conversations: Vec<SavedConversationMetadata>,
132 saved_conversations_scroll_handle: UniformListScrollHandle,
133 zoomed: bool,
134 // todo!("remove has_focus field")
135 focus_handle: FocusHandle,
136 toolbar: View<Toolbar>,
137 completion_provider: Arc<dyn CompletionProvider>,
138 api_key_editor: Option<View<Editor>>,
139 languages: Arc<LanguageRegistry>,
140 fs: Arc<dyn Fs>,
141 subscriptions: Vec<Subscription>,
142 next_inline_assist_id: usize,
143 pending_inline_assists: HashMap<usize, PendingInlineAssist>,
144 pending_inline_assist_ids_by_editor: HashMap<WeakView<Editor>, Vec<usize>>,
145 include_conversation_in_next_inline_assist: bool,
146 inline_prompt_history: VecDeque<String>,
147 _watch_saved_conversations: Task<Result<()>>,
148 semantic_index: Option<Model<SemanticIndex>>,
149 retrieve_context_in_next_inline_assist: bool,
150}
151
152impl AssistantPanel {
153 const INLINE_PROMPT_HISTORY_MAX_LEN: usize = 20;
154
155 pub fn load(workspace: WeakView<Workspace>, cx: AsyncAppContext) -> Task<Result<View<Self>>> {
156 cx.spawn(|mut cx| async move {
157 let fs = workspace.read_with(&cx, |workspace, _| workspace.app_state().fs.clone())?;
158 let saved_conversations = SavedConversationMetadata::list(fs.clone())
159 .await
160 .log_err()
161 .unwrap_or_default();
162
163 // TODO: deserialize state.
164 let workspace_handle = workspace.clone();
165 workspace.update(&mut cx, |workspace, cx| {
166 cx.add_view::<Self, _>(|cx| {
167 const CONVERSATION_WATCH_DURATION: Duration = Duration::from_millis(100);
168 let _watch_saved_conversations = cx.spawn(move |this, mut cx| async move {
169 let mut events = fs
170 .watch(&CONVERSATIONS_DIR, CONVERSATION_WATCH_DURATION)
171 .await;
172 while events.next().await.is_some() {
173 let saved_conversations = SavedConversationMetadata::list(fs.clone())
174 .await
175 .log_err()
176 .unwrap_or_default();
177 this.update(&mut cx, |this, cx| {
178 this.saved_conversations = saved_conversations;
179 cx.notify();
180 })
181 .ok();
182 }
183
184 anyhow::Ok(())
185 });
186
187 let toolbar = cx.add_view(|cx| {
188 let mut toolbar = Toolbar::new();
189 toolbar.set_can_navigate(false, cx);
190 toolbar.add_item(cx.add_view(|cx| BufferSearchBar::new(cx)), cx);
191 toolbar
192 });
193
194 let semantic_index = SemanticIndex::global(cx);
195 // Defaulting currently to GPT4, allow for this to be set via config.
196 let completion_provider = Arc::new(OpenAICompletionProvider::new(
197 "gpt-4",
198 cx.background_executor().clone(),
199 ));
200
201 let focus_handle = cx.focus_handle();
202 cx.on_focus_in(Self::focus_in).detach();
203 cx.on_focus_out(Self::focus_out).detach();
204
205 let mut this = Self {
206 workspace: workspace_handle,
207 active_editor_index: Default::default(),
208 prev_active_editor_index: Default::default(),
209 editors: Default::default(),
210 saved_conversations,
211 saved_conversations_scroll_handle: Default::default(),
212 zoomed: false,
213 focus_handle,
214 toolbar,
215 completion_provider,
216 api_key_editor: None,
217 languages: workspace.app_state().languages.clone(),
218 fs: workspace.app_state().fs.clone(),
219 width: None,
220 height: None,
221 subscriptions: Default::default(),
222 next_inline_assist_id: 0,
223 pending_inline_assists: Default::default(),
224 pending_inline_assist_ids_by_editor: Default::default(),
225 include_conversation_in_next_inline_assist: false,
226 inline_prompt_history: Default::default(),
227 _watch_saved_conversations,
228 semantic_index,
229 retrieve_context_in_next_inline_assist: false,
230 };
231
232 let mut old_dock_position = this.position(cx);
233 this.subscriptions =
234 vec![cx.observe_global::<SettingsStore, _>(move |this, cx| {
235 let new_dock_position = this.position(cx);
236 if new_dock_position != old_dock_position {
237 old_dock_position = new_dock_position;
238 cx.emit(AssistantPanelEvent::DockPositionChanged);
239 }
240 cx.notify();
241 })];
242
243 this
244 })
245 })
246 })
247 }
248
249 fn focus_in(&mut self, cx: &mut ViewContext<Self>) {
250 self.toolbar
251 .update(cx, |toolbar, cx| toolbar.focus_changed(true, cx));
252 cx.notify();
253 if self.focus_handle.is_focused(cx) {
254 if let Some(editor) = self.active_editor() {
255 cx.focus_view(editor);
256 } else if let Some(api_key_editor) = self.api_key_editor.as_ref() {
257 cx.focus_view(api_key_editor);
258 }
259 }
260 }
261
262 fn focus_out(&mut self, cx: &mut ViewContext<Self>) {
263 self.toolbar
264 .update(cx, |toolbar, cx| toolbar.focus_changed(false, cx));
265 cx.notify();
266 }
267
268 pub fn inline_assist(
269 workspace: &mut Workspace,
270 _: &InlineAssist,
271 cx: &mut ViewContext<Workspace>,
272 ) {
273 let this = if let Some(this) = workspace.panel::<AssistantPanel>(cx) {
274 if this.update(cx, |assistant, cx| {
275 if !assistant.has_credentials() {
276 assistant.load_credentials(cx);
277 };
278
279 assistant.has_credentials()
280 }) {
281 this
282 } else {
283 workspace.focus_panel::<AssistantPanel>(cx);
284 return;
285 }
286 } else {
287 return;
288 };
289
290 let active_editor = if let Some(active_editor) = workspace
291 .active_item(cx)
292 .and_then(|item| item.act_as::<Editor>(cx))
293 {
294 active_editor
295 } else {
296 return;
297 };
298
299 let project = workspace.project();
300
301 this.update(cx, |assistant, cx| {
302 assistant.new_inline_assist(&active_editor, cx, project)
303 });
304 }
305
306 fn new_inline_assist(
307 &mut self,
308 editor: &View<Editor>,
309 cx: &mut ViewContext<Self>,
310 project: &Model<Project>,
311 ) {
312 let selection = editor.read(cx).selections.newest_anchor().clone();
313 if selection.start.excerpt_id != selection.end.excerpt_id {
314 return;
315 }
316 let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
317
318 // Extend the selection to the start and the end of the line.
319 let mut point_selection = selection.map(|selection| selection.to_point(&snapshot));
320 if point_selection.end > point_selection.start {
321 point_selection.start.column = 0;
322 // If the selection ends at the start of the line, we don't want to include it.
323 if point_selection.end.column == 0 {
324 point_selection.end.row -= 1;
325 }
326 point_selection.end.column = snapshot.line_len(point_selection.end.row);
327 }
328
329 let codegen_kind = if point_selection.start == point_selection.end {
330 CodegenKind::Generate {
331 position: snapshot.anchor_after(point_selection.start),
332 }
333 } else {
334 CodegenKind::Transform {
335 range: snapshot.anchor_before(point_selection.start)
336 ..snapshot.anchor_after(point_selection.end),
337 }
338 };
339
340 let inline_assist_id = post_inc(&mut self.next_inline_assist_id);
341 let provider = self.completion_provider.clone();
342
343 // Retrieve Credentials Authenticates the Provider
344 provider.retrieve_credentials(cx);
345
346 let codegen = cx.add_model(|cx| {
347 Codegen::new(editor.read(cx).buffer().clone(), codegen_kind, provider, cx)
348 });
349
350 if let Some(semantic_index) = self.semantic_index.clone() {
351 let project = project.clone();
352 cx.spawn(|_, mut cx| async move {
353 let previously_indexed = semantic_index
354 .update(&mut cx, |index, cx| {
355 index.project_previously_indexed(&project, cx)
356 })
357 .await
358 .unwrap_or(false);
359 if previously_indexed {
360 let _ = semantic_index
361 .update(&mut cx, |index, cx| {
362 index.index_project(project.clone(), cx)
363 })
364 .await;
365 }
366 anyhow::Ok(())
367 })
368 .detach_and_log_err(cx);
369 }
370
371 let measurements = Rc::new(Cell::new(BlockMeasurements::default()));
372 let inline_assistant = cx.add_view(|cx| {
373 let assistant = InlineAssistant::new(
374 inline_assist_id,
375 measurements.clone(),
376 self.include_conversation_in_next_inline_assist,
377 self.inline_prompt_history.clone(),
378 codegen.clone(),
379 self.workspace.clone(),
380 cx,
381 self.retrieve_context_in_next_inline_assist,
382 self.semantic_index.clone(),
383 project.clone(),
384 );
385 cx.focus_self();
386 assistant
387 });
388 let block_id = editor.update(cx, |editor, cx| {
389 editor.change_selections(None, cx, |selections| {
390 selections.select_anchor_ranges([selection.head()..selection.head()])
391 });
392 editor.insert_blocks(
393 [BlockProperties {
394 style: BlockStyle::Flex,
395 position: snapshot.anchor_before(point_selection.head()),
396 height: 2,
397 render: Arc::new({
398 let inline_assistant = inline_assistant.clone();
399 move |cx: &mut BlockContext| {
400 measurements.set(BlockMeasurements {
401 anchor_x: cx.anchor_x,
402 gutter_width: cx.gutter_width,
403 });
404 inline_assistant.clone().into_any_element()
405 }
406 }),
407 disposition: if selection.reversed {
408 BlockDisposition::Above
409 } else {
410 BlockDisposition::Below
411 },
412 }],
413 Some(Autoscroll::Strategy(AutoscrollStrategy::Newest)),
414 cx,
415 )[0]
416 });
417
418 self.pending_inline_assists.insert(
419 inline_assist_id,
420 PendingInlineAssist {
421 editor: editor.downgrade(),
422 inline_assistant: Some((block_id, inline_assistant.clone())),
423 codegen: codegen.clone(),
424 project: project.downgrade(),
425 _subscriptions: vec![
426 cx.subscribe(&inline_assistant, Self::handle_inline_assistant_event),
427 cx.subscribe(editor, {
428 let inline_assistant = inline_assistant.downgrade();
429 move |_, editor, event, cx| {
430 if let Some(inline_assistant) = inline_assistant.upgrade() {
431 if let EditorEvent::SelectionsChanged { local } = event {
432 if *local && inline_assistant.read(cx).has_focus {
433 cx.focus(&editor);
434 }
435 }
436 }
437 }
438 }),
439 cx.observe(&codegen, {
440 let editor = editor.downgrade();
441 move |this, _, cx| {
442 if let Some(editor) = editor.upgrade() {
443 this.update_highlights_for_editor(&editor, cx);
444 }
445 }
446 }),
447 cx.subscribe(&codegen, move |this, codegen, event, cx| match event {
448 codegen::Event::Undone => {
449 this.finish_inline_assist(inline_assist_id, false, cx)
450 }
451 codegen::Event::Finished => {
452 let pending_assist = if let Some(pending_assist) =
453 this.pending_inline_assists.get(&inline_assist_id)
454 {
455 pending_assist
456 } else {
457 return;
458 };
459
460 let error = codegen
461 .read(cx)
462 .error()
463 .map(|error| format!("Inline assistant error: {}", error));
464 if let Some(error) = error {
465 if pending_assist.inline_assistant.is_none() {
466 if let Some(workspace) = this.workspace.upgrade() {
467 workspace.update(cx, |workspace, cx| {
468 workspace.show_toast(
469 Toast::new(inline_assist_id, error),
470 cx,
471 );
472 })
473 }
474
475 this.finish_inline_assist(inline_assist_id, false, cx);
476 }
477 } else {
478 this.finish_inline_assist(inline_assist_id, false, cx);
479 }
480 }
481 }),
482 ],
483 },
484 );
485 self.pending_inline_assist_ids_by_editor
486 .entry(editor.downgrade())
487 .or_default()
488 .push(inline_assist_id);
489 self.update_highlights_for_editor(&editor, cx);
490 }
491
492 fn handle_inline_assistant_event(
493 &mut self,
494 inline_assistant: View<InlineAssistant>,
495 event: &InlineAssistantEvent,
496 cx: &mut ViewContext<Self>,
497 ) {
498 let assist_id = inline_assistant.read(cx).id;
499 match event {
500 InlineAssistantEvent::Confirmed {
501 prompt,
502 include_conversation,
503 retrieve_context,
504 } => {
505 self.confirm_inline_assist(
506 assist_id,
507 prompt,
508 *include_conversation,
509 cx,
510 *retrieve_context,
511 );
512 }
513 InlineAssistantEvent::Canceled => {
514 self.finish_inline_assist(assist_id, true, cx);
515 }
516 InlineAssistantEvent::Dismissed => {
517 self.hide_inline_assist(assist_id, cx);
518 }
519 InlineAssistantEvent::IncludeConversationToggled {
520 include_conversation,
521 } => {
522 self.include_conversation_in_next_inline_assist = *include_conversation;
523 }
524 InlineAssistantEvent::RetrieveContextToggled { retrieve_context } => {
525 self.retrieve_context_in_next_inline_assist = *retrieve_context
526 }
527 }
528 }
529
530 fn cancel_last_inline_assist(
531 workspace: &mut Workspace,
532 _: &editor::Cancel,
533 cx: &mut ViewContext<Workspace>,
534 ) {
535 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
536 if let Some(editor) = workspace
537 .active_item(cx)
538 .and_then(|item| item.downcast::<Editor>())
539 {
540 let handled = panel.update(cx, |panel, cx| {
541 if let Some(assist_id) = panel
542 .pending_inline_assist_ids_by_editor
543 .get(&editor.downgrade())
544 .and_then(|assist_ids| assist_ids.last().copied())
545 {
546 panel.finish_inline_assist(assist_id, true, cx);
547 true
548 } else {
549 false
550 }
551 });
552 if handled {
553 return;
554 }
555 }
556 }
557
558 cx.propagate_action();
559 }
560
561 fn finish_inline_assist(&mut self, assist_id: usize, undo: bool, cx: &mut ViewContext<Self>) {
562 self.hide_inline_assist(assist_id, cx);
563
564 if let Some(pending_assist) = self.pending_inline_assists.remove(&assist_id) {
565 if let hash_map::Entry::Occupied(mut entry) = self
566 .pending_inline_assist_ids_by_editor
567 .entry(pending_assist.editor)
568 {
569 entry.get_mut().retain(|id| *id != assist_id);
570 if entry.get().is_empty() {
571 entry.remove();
572 }
573 }
574
575 if let Some(editor) = pending_assist.editor.upgrade() {
576 self.update_highlights_for_editor(&editor, cx);
577
578 if undo {
579 pending_assist
580 .codegen
581 .update(cx, |codegen, cx| codegen.undo(cx));
582 }
583 }
584 }
585 }
586
587 fn hide_inline_assist(&mut self, assist_id: usize, cx: &mut ViewContext<Self>) {
588 if let Some(pending_assist) = self.pending_inline_assists.get_mut(&assist_id) {
589 if let Some(editor) = pending_assist.editor.upgrade() {
590 if let Some((block_id, _)) = pending_assist.inline_assistant.take() {
591 editor.update(cx, |editor, cx| {
592 editor.remove_blocks(HashSet::from_iter([block_id]), None, cx);
593 });
594 }
595 }
596 }
597 }
598
599 fn confirm_inline_assist(
600 &mut self,
601 inline_assist_id: usize,
602 user_prompt: &str,
603 include_conversation: bool,
604 cx: &mut ViewContext<Self>,
605 retrieve_context: bool,
606 ) {
607 let conversation = if include_conversation {
608 self.active_editor()
609 .map(|editor| editor.read(cx).conversation.clone())
610 } else {
611 None
612 };
613
614 let pending_assist =
615 if let Some(pending_assist) = self.pending_inline_assists.get_mut(&inline_assist_id) {
616 pending_assist
617 } else {
618 return;
619 };
620
621 let editor = if let Some(editor) = pending_assist.editor.upgrade() {
622 editor
623 } else {
624 return;
625 };
626
627 let project = pending_assist.project.clone();
628
629 let project_name = if let Some(project) = project.upgrade() {
630 Some(
631 project
632 .read(cx)
633 .worktree_root_names(cx)
634 .collect::<Vec<&str>>()
635 .join("/"),
636 )
637 } else {
638 None
639 };
640
641 self.inline_prompt_history
642 .retain(|prompt| prompt != user_prompt);
643 self.inline_prompt_history.push_back(user_prompt.into());
644 if self.inline_prompt_history.len() > Self::INLINE_PROMPT_HISTORY_MAX_LEN {
645 self.inline_prompt_history.pop_front();
646 }
647
648 let codegen = pending_assist.codegen.clone();
649 let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
650 let range = codegen.read(cx).range();
651 let start = snapshot.point_to_buffer_offset(range.start);
652 let end = snapshot.point_to_buffer_offset(range.end);
653 let (buffer, range) = if let Some((start, end)) = start.zip(end) {
654 let (start_buffer, start_buffer_offset) = start;
655 let (end_buffer, end_buffer_offset) = end;
656 if start_buffer.remote_id() == end_buffer.remote_id() {
657 (start_buffer.clone(), start_buffer_offset..end_buffer_offset)
658 } else {
659 self.finish_inline_assist(inline_assist_id, false, cx);
660 return;
661 }
662 } else {
663 self.finish_inline_assist(inline_assist_id, false, cx);
664 return;
665 };
666
667 let language = buffer.language_at(range.start);
668 let language_name = if let Some(language) = language.as_ref() {
669 if Arc::ptr_eq(language, &language::PLAIN_TEXT) {
670 None
671 } else {
672 Some(language.name())
673 }
674 } else {
675 None
676 };
677
678 // Higher Temperature increases the randomness of model outputs.
679 // If Markdown or No Language is Known, increase the randomness for more creative output
680 // If Code, decrease temperature to get more deterministic outputs
681 let temperature = if let Some(language) = language_name.clone() {
682 if language.to_string() != "Markdown".to_string() {
683 0.5
684 } else {
685 1.0
686 }
687 } else {
688 1.0
689 };
690
691 let user_prompt = user_prompt.to_string();
692
693 let snippets = if retrieve_context {
694 let Some(project) = project.upgrade() else {
695 return;
696 };
697
698 let search_results = if let Some(semantic_index) = self.semantic_index.clone() {
699 let search_results = semantic_index.update(cx, |this, cx| {
700 this.search_project(project, user_prompt.to_string(), 10, vec![], vec![], cx)
701 });
702
703 cx.background_executor()
704 .spawn(async move { search_results.await.unwrap_or_default() })
705 } else {
706 Task::ready(Vec::new())
707 };
708
709 let snippets = cx.spawn(|_, mut cx| async move {
710 let mut snippets = Vec::new();
711 for result in search_results.await {
712 snippets.push(PromptCodeSnippet::new(result.buffer, result.range, &mut cx));
713 }
714 snippets
715 });
716 snippets
717 } else {
718 Task::ready(Vec::new())
719 };
720
721 let mut model = AssistantSettings::get_global(cx)
722 .default_open_ai_model
723 .clone();
724 let model_name = model.full_name();
725
726 let prompt = cx.background_executor().spawn(async move {
727 let snippets = snippets.await;
728
729 let language_name = language_name.as_deref();
730 generate_content_prompt(
731 user_prompt,
732 language_name,
733 buffer,
734 range,
735 snippets,
736 model_name,
737 project_name,
738 )
739 });
740
741 let mut messages = Vec::new();
742 if let Some(conversation) = conversation {
743 let conversation = conversation.read(cx);
744 let buffer = conversation.buffer.read(cx);
745 messages.extend(
746 conversation
747 .messages(cx)
748 .map(|message| message.to_open_ai_message(buffer)),
749 );
750 model = conversation.model.clone();
751 }
752
753 cx.spawn(|_, mut cx| async move {
754 // I Don't know if we want to return a ? here.
755 let prompt = prompt.await?;
756
757 messages.push(RequestMessage {
758 role: Role::User,
759 content: prompt,
760 });
761
762 let request = Box::new(OpenAIRequest {
763 model: model.full_name().into(),
764 messages,
765 stream: true,
766 stop: vec!["|END|>".to_string()],
767 temperature,
768 });
769
770 codegen.update(&mut cx, |codegen, cx| codegen.start(request, cx));
771 anyhow::Ok(())
772 })
773 .detach();
774 }
775
776 fn update_highlights_for_editor(&self, editor: &View<Editor>, cx: &mut ViewContext<Self>) {
777 let mut background_ranges = Vec::new();
778 let mut foreground_ranges = Vec::new();
779 let empty_inline_assist_ids = Vec::new();
780 let inline_assist_ids = self
781 .pending_inline_assist_ids_by_editor
782 .get(&editor.downgrade())
783 .unwrap_or(&empty_inline_assist_ids);
784
785 for inline_assist_id in inline_assist_ids {
786 if let Some(pending_assist) = self.pending_inline_assists.get(inline_assist_id) {
787 let codegen = pending_assist.codegen.read(cx);
788 background_ranges.push(codegen.range());
789 foreground_ranges.extend(codegen.last_equal_ranges().iter().cloned());
790 }
791 }
792
793 let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
794 merge_ranges(&mut background_ranges, &snapshot);
795 merge_ranges(&mut foreground_ranges, &snapshot);
796 editor.update(cx, |editor, cx| {
797 if background_ranges.is_empty() {
798 editor.clear_background_highlights::<PendingInlineAssist>(cx);
799 } else {
800 editor.highlight_background::<PendingInlineAssist>(
801 background_ranges,
802 |theme| theme.assistant.inline.pending_edit_background,
803 cx,
804 );
805 }
806
807 if foreground_ranges.is_empty() {
808 editor.clear_highlights::<PendingInlineAssist>(cx);
809 } else {
810 editor.highlight_text::<PendingInlineAssist>(
811 foreground_ranges,
812 HighlightStyle {
813 fade_out: Some(0.6),
814 ..Default::default()
815 },
816 cx,
817 );
818 }
819 });
820 }
821
822 fn new_conversation(&mut self, cx: &mut ViewContext<Self>) -> View<ConversationEditor> {
823 let editor = cx.add_view(|cx| {
824 ConversationEditor::new(
825 self.completion_provider.clone(),
826 self.languages.clone(),
827 self.fs.clone(),
828 self.workspace.clone(),
829 cx,
830 )
831 });
832 self.add_conversation(editor.clone(), cx);
833 editor
834 }
835
836 fn add_conversation(&mut self, editor: View<ConversationEditor>, cx: &mut ViewContext<Self>) {
837 self.subscriptions
838 .push(cx.subscribe(&editor, Self::handle_conversation_editor_event));
839
840 let conversation = editor.read(cx).conversation.clone();
841 self.subscriptions
842 .push(cx.observe(&conversation, |_, _, cx| cx.notify()));
843
844 let index = self.editors.len();
845 self.editors.push(editor);
846 self.set_active_editor_index(Some(index), cx);
847 }
848
849 fn set_active_editor_index(&mut self, index: Option<usize>, cx: &mut ViewContext<Self>) {
850 self.prev_active_editor_index = self.active_editor_index;
851 self.active_editor_index = index;
852 if let Some(editor) = self.active_editor() {
853 let editor = editor.read(cx).editor.clone();
854 self.toolbar.update(cx, |toolbar, cx| {
855 toolbar.set_active_item(Some(&editor), cx);
856 });
857 if self.has_focus(cx) {
858 cx.focus(&editor);
859 }
860 } else {
861 self.toolbar.update(cx, |toolbar, cx| {
862 toolbar.set_active_item(None, cx);
863 });
864 }
865
866 cx.notify();
867 }
868
869 fn handle_conversation_editor_event(
870 &mut self,
871 _: View<ConversationEditor>,
872 event: &ConversationEditorEvent,
873 cx: &mut ViewContext<Self>,
874 ) {
875 match event {
876 ConversationEditorEvent::TabContentChanged => cx.notify(),
877 }
878 }
879
880 fn save_credentials(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
881 if let Some(api_key) = self
882 .api_key_editor
883 .as_ref()
884 .map(|editor| editor.read(cx).text(cx))
885 {
886 if !api_key.is_empty() {
887 let credential = ProviderCredential::Credentials {
888 api_key: api_key.clone(),
889 };
890
891 self.completion_provider.save_credentials(cx, credential);
892
893 self.api_key_editor.take();
894 cx.focus_self();
895 cx.notify();
896 }
897 } else {
898 cx.propagate_action();
899 }
900 }
901
902 fn reset_credentials(&mut self, _: &ResetKey, cx: &mut ViewContext<Self>) {
903 self.completion_provider.delete_credentials(cx);
904 self.api_key_editor = Some(build_api_key_editor(cx));
905 cx.focus_self();
906 cx.notify();
907 }
908
909 fn toggle_zoom(&mut self, _: &workspace::ToggleZoom, cx: &mut ViewContext<Self>) {
910 if self.zoomed {
911 cx.emit(AssistantPanelEvent::ZoomOut)
912 } else {
913 cx.emit(AssistantPanelEvent::ZoomIn)
914 }
915 }
916
917 fn deploy(&mut self, action: &search::buffer_search::Deploy, cx: &mut ViewContext<Self>) {
918 let mut propagate_action = true;
919 if let Some(search_bar) = self.toolbar.read(cx).item_of_type::<BufferSearchBar>() {
920 search_bar.update(cx, |search_bar, cx| {
921 if search_bar.show(cx) {
922 search_bar.search_suggested(cx);
923 if action.focus {
924 search_bar.select_query(cx);
925 cx.focus_self();
926 }
927 propagate_action = false
928 }
929 });
930 }
931 if propagate_action {
932 cx.propagate_action();
933 }
934 }
935
936 fn handle_editor_cancel(&mut self, _: &editor::Cancel, cx: &mut ViewContext<Self>) {
937 if let Some(search_bar) = self.toolbar.read(cx).item_of_type::<BufferSearchBar>() {
938 if !search_bar.read(cx).is_dismissed() {
939 search_bar.update(cx, |search_bar, cx| {
940 search_bar.dismiss(&Default::default(), cx)
941 });
942 return;
943 }
944 }
945 cx.propagate_action();
946 }
947
948 fn select_next_match(&mut self, _: &search::SelectNextMatch, cx: &mut ViewContext<Self>) {
949 if let Some(search_bar) = self.toolbar.read(cx).item_of_type::<BufferSearchBar>() {
950 search_bar.update(cx, |bar, cx| bar.select_match(Direction::Next, 1, cx));
951 }
952 }
953
954 fn select_prev_match(&mut self, _: &search::SelectPrevMatch, cx: &mut ViewContext<Self>) {
955 if let Some(search_bar) = self.toolbar.read(cx).item_of_type::<BufferSearchBar>() {
956 search_bar.update(cx, |bar, cx| bar.select_match(Direction::Prev, 1, cx));
957 }
958 }
959
960 fn active_editor(&self) -> Option<&View<ConversationEditor>> {
961 self.editors.get(self.active_editor_index?)
962 }
963
964 fn render_hamburger_button(cx: &mut ViewContext<Self>) -> impl IntoElement {
965 IconButton::new("hamburger_button", Icon::Menu)
966 .on_click(cx.listener(|this, _event, cx| {
967 if this.active_editor().is_some() {
968 this.set_active_editor_index(None, cx);
969 } else {
970 this.set_active_editor_index(this.prev_active_editor_index, cx);
971 }
972 }))
973 .tooltip(|cx| Tooltip::text("History", cx))
974 }
975
976 fn render_editor_tools(&self, cx: &mut ViewContext<Self>) -> Vec<AnyElement> {
977 if self.active_editor().is_some() {
978 vec![
979 Self::render_split_button(cx).into_any(),
980 Self::render_quote_button(cx).into_any(),
981 Self::render_assist_button(cx).into_any(),
982 ]
983 } else {
984 Default::default()
985 }
986 }
987
988 fn render_split_button(cx: &mut ViewContext<Self>) -> impl IntoElement {
989 IconButton::new("split_button", Icon::Menu)
990 .on_click(cx.listener(|this, _event, cx| {
991 if let Some(active_editor) = this.active_editor() {
992 active_editor.update(cx, |editor, cx| editor.split(&Default::default(), cx));
993 }
994 }))
995 .tooltip(|cx| Tooltip::for_action("Split Message", &Split, cx))
996 }
997
998 fn render_assist_button(cx: &mut ViewContext<Self>) -> impl IntoElement {
999 IconButton::new("assist_button", Icon::Menu)
1000 .on_click(cx.listener(|this, _event, cx| {
1001 if let Some(active_editor) = this.active_editor() {
1002 active_editor.update(cx, |editor, cx| editor.assist(&Default::default(), cx));
1003 }
1004 }))
1005 .tooltip(|cx| Tooltip::for_action("Assist", &Assist, cx))
1006 }
1007
1008 fn render_quote_button(cx: &mut ViewContext<Self>) -> impl IntoElement {
1009 IconButton::new("quote_button", Icon::Menu)
1010 .on_click(cx.listener(|this, _event, cx| {
1011 if let Some(workspace) = this.workspace.upgrade() {
1012 cx.window_context().defer(move |cx| {
1013 workspace.update(cx, |workspace, cx| {
1014 ConversationEditor::quote_selection(workspace, &Default::default(), cx)
1015 });
1016 });
1017 }
1018 }))
1019 .tooltip(|cx| Tooltip::for_action("Quote Seleciton", &QuoteSelection, cx))
1020 }
1021
1022 fn render_plus_button(cx: &mut ViewContext<Self>) -> impl IntoElement {
1023 IconButton::new("plus_button", Icon::Menu)
1024 .on_click(cx.listener(|this, _event, cx| {
1025 this.new_conversation(cx);
1026 }))
1027 .tooltip(|cx| Tooltip::for_action("New Conversation", &NewConversation, cx))
1028 }
1029
1030 fn render_zoom_button(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
1031 IconButton::new("zoom_button", Icon::Menu)
1032 .on_click(cx.listener(|this, _event, cx| {
1033 this.toggle_zoom(&ToggleZoom, cx);
1034 }))
1035 .tooltip(|cx| {
1036 Tooltip::for_action(
1037 if self.zoomed { "Zoom Out" } else { "Zoom In" },
1038 &ToggleZoom,
1039 cx,
1040 )
1041 })
1042 }
1043
1044 fn render_saved_conversation(
1045 &mut self,
1046 index: usize,
1047 cx: &mut ViewContext<Self>,
1048 ) -> impl IntoElement {
1049 let conversation = &self.saved_conversations[index];
1050 let path = conversation.path.clone();
1051
1052 ButtonLike::new(index)
1053 .on_click(cx.listener(move |this, _, cx| {
1054 this.open_conversation(path.clone(), cx)
1055 .detach_and_log_err(cx)
1056 }))
1057 .child(Label::new(
1058 conversation.mtime.format("%F %I:%M%p").to_string(),
1059 ))
1060 .child(Label::new(conversation.title.clone()))
1061 }
1062
1063 fn open_conversation(&mut self, path: PathBuf, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
1064 if let Some(ix) = self.editor_index_for_path(&path, cx) {
1065 self.set_active_editor_index(Some(ix), cx);
1066 return Task::ready(Ok(()));
1067 }
1068
1069 let fs = self.fs.clone();
1070 let workspace = self.workspace.clone();
1071 let languages = self.languages.clone();
1072 cx.spawn(|this, mut cx| async move {
1073 let saved_conversation = fs.load(&path).await?;
1074 let saved_conversation = serde_json::from_str(&saved_conversation)?;
1075 let conversation = cx.add_model(|cx| {
1076 Conversation::deserialize(saved_conversation, path.clone(), languages, cx)
1077 });
1078 this.update(&mut cx, |this, cx| {
1079 // If, by the time we've loaded the conversation, the user has already opened
1080 // the same conversation, we don't want to open it again.
1081 if let Some(ix) = this.editor_index_for_path(&path, cx) {
1082 this.set_active_editor_index(Some(ix), cx);
1083 } else {
1084 let editor = cx.add_view(|cx| {
1085 ConversationEditor::for_conversation(conversation, fs, workspace, cx)
1086 });
1087 this.add_conversation(editor, cx);
1088 }
1089 })?;
1090 Ok(())
1091 })
1092 }
1093
1094 fn editor_index_for_path(&self, path: &Path, cx: &AppContext) -> Option<usize> {
1095 self.editors
1096 .iter()
1097 .position(|editor| editor.read(cx).conversation.read(cx).path.as_deref() == Some(path))
1098 }
1099
1100 fn has_credentials(&mut self) -> bool {
1101 self.completion_provider.has_credentials()
1102 }
1103
1104 fn load_credentials(&mut self, cx: &mut ViewContext<Self>) {
1105 self.completion_provider.retrieve_credentials(cx);
1106 }
1107}
1108
1109fn build_api_key_editor(cx: &mut ViewContext<AssistantPanel>) -> View<Editor> {
1110 cx.build_view(|cx| {
1111 let mut editor = Editor::single_line(cx);
1112 editor.set_placeholder_text("sk-000000000000000000000000000000000000000000000000", cx);
1113 editor
1114 })
1115}
1116
1117impl Render for AssistantPanel {
1118 type Element = Focusable<Div>;
1119
1120 fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
1121 if let Some(api_key_editor) = self.api_key_editor.clone() {
1122 v_stack()
1123 .track_focus(&self.focus_handle)
1124 .child(Label::new(
1125 "To use the assistant panel or inline assistant, you need to add your OpenAI api key.",
1126 ))
1127 .child(Label::new(
1128 " - Having a subscription for another service like GitHub Copilot won't work."
1129 ))
1130 .child(Label::new(
1131 " - You can create a api key at: platform.openai.com/api-keys"
1132 ))
1133 .child(Label::new(
1134 " "
1135 ))
1136 .child(Label::new(
1137 "Paste your OpenAI API key and press Enter to use the assistant"
1138 ))
1139 .child(api_key_editor)
1140 .child(Label::new(
1141 "Click on the Z button in the status bar to close this panel."
1142 ))
1143 .border()
1144 .border_color(gpui::red())
1145 } else {
1146 let title = self
1147 .active_editor()
1148 .map(|editor| Label::new(editor.read(cx).title(cx)));
1149
1150 let mut header = h_stack()
1151 .child(Self::render_hamburger_button(cx))
1152 .children(title);
1153
1154 if self.focus_handle.contains_focused(cx) {
1155 header = header
1156 .children(self.render_editor_tools(cx))
1157 .child(Self::render_plus_button(cx))
1158 .child(self.render_zoom_button(cx));
1159 }
1160
1161 v_stack()
1162 .track_focus(&self.focus_handle)
1163 .child(header)
1164 .children(if self.toolbar.read(cx).hidden() {
1165 None
1166 } else {
1167 Some(self.toolbar.clone())
1168 })
1169 .child(if let Some(editor) = self.active_editor() {
1170 editor.clone().into_any_element()
1171 } else {
1172 uniform_list(
1173 cx.view().clone(),
1174 "saved_conversations",
1175 self.saved_conversations.len(),
1176 |this, range, cx| {
1177 range
1178 .map(|ix| this.render_saved_conversation(ix, cx).into_any())
1179 .collect()
1180 },
1181 )
1182 .track_scroll(self.saved_conversations_scroll_handle.clone())
1183 .into_any_element()
1184 })
1185 .border()
1186 .border_color(gpui::red())
1187 }
1188 }
1189}
1190
1191impl Panel for AssistantPanel {
1192 fn persistent_name() -> &'static str {
1193 "AssistantPanel"
1194 }
1195
1196 fn position(&self, cx: &WindowContext) -> DockPosition {
1197 match AssistantSettings::get_global(cx).dock {
1198 AssistantDockPosition::Left => DockPosition::Left,
1199 AssistantDockPosition::Bottom => DockPosition::Bottom,
1200 AssistantDockPosition::Right => DockPosition::Right,
1201 }
1202 }
1203
1204 fn position_is_valid(&self, _: DockPosition) -> bool {
1205 true
1206 }
1207
1208 fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
1209 settings::update_settings_file::<AssistantSettings>(self.fs.clone(), cx, move |settings| {
1210 let dock = match position {
1211 DockPosition::Left => AssistantDockPosition::Left,
1212 DockPosition::Bottom => AssistantDockPosition::Bottom,
1213 DockPosition::Right => AssistantDockPosition::Right,
1214 };
1215 settings.dock = Some(dock);
1216 });
1217 }
1218
1219 fn size(&self, cx: &WindowContext) -> f32 {
1220 let settings = AssistantSettings::get_global(cx);
1221 match self.position(cx) {
1222 DockPosition::Left | DockPosition::Right => {
1223 self.width.unwrap_or_else(|| settings.default_width)
1224 }
1225 DockPosition::Bottom => self.height.unwrap_or_else(|| settings.default_height),
1226 }
1227 }
1228
1229 fn set_size(&mut self, size: Option<f32>, cx: &mut ViewContext<Self>) {
1230 match self.position(cx) {
1231 DockPosition::Left | DockPosition::Right => self.width = size,
1232 DockPosition::Bottom => self.height = size,
1233 }
1234 cx.notify();
1235 }
1236
1237 fn is_zoomed(&self, _: &WindowContext) -> bool {
1238 self.zoomed
1239 }
1240
1241 fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
1242 self.zoomed = zoomed;
1243 cx.notify();
1244 }
1245
1246 fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
1247 if active {
1248 self.load_credentials(cx);
1249
1250 if self.editors.is_empty() {
1251 self.new_conversation(cx);
1252 }
1253 }
1254 }
1255
1256 fn icon(&self, cx: &WindowContext) -> Option<Icon> {
1257 Some(Icon::Ai)
1258 }
1259
1260 fn toggle_action(&self) -> Box<dyn Action> {
1261 Box::new(ToggleFocus)
1262 }
1263}
1264
1265impl EventEmitter<PanelEvent> for AssistantPanel {}
1266
1267impl FocusableView for AssistantPanel {
1268 fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
1269 self.focus_handle.clone()
1270 }
1271}
1272
1273enum ConversationEvent {
1274 MessagesEdited,
1275 SummaryChanged,
1276 StreamedCompletion,
1277}
1278
1279#[derive(Default)]
1280struct Summary {
1281 text: String,
1282 done: bool,
1283}
1284
1285struct Conversation {
1286 id: Option<String>,
1287 buffer: Model<Buffer>,
1288 message_anchors: Vec<MessageAnchor>,
1289 messages_metadata: HashMap<MessageId, MessageMetadata>,
1290 next_message_id: MessageId,
1291 summary: Option<Summary>,
1292 pending_summary: Task<Option<()>>,
1293 completion_count: usize,
1294 pending_completions: Vec<PendingCompletion>,
1295 model: OpenAIModel,
1296 token_count: Option<usize>,
1297 max_token_count: usize,
1298 pending_token_count: Task<Option<()>>,
1299 pending_save: Task<Result<()>>,
1300 path: Option<PathBuf>,
1301 _subscriptions: Vec<Subscription>,
1302 completion_provider: Arc<dyn CompletionProvider>,
1303}
1304
1305impl EventEmitter<ConversationEvent> for Conversation {}
1306
1307impl Conversation {
1308 fn new(
1309 language_registry: Arc<LanguageRegistry>,
1310 cx: &mut ModelContext<Self>,
1311 completion_provider: Arc<dyn CompletionProvider>,
1312 ) -> Self {
1313 let markdown = language_registry.language_for_name("Markdown");
1314 let buffer = cx.add_model(|cx| {
1315 let mut buffer = Buffer::new(0, cx.model_id() as u64, "");
1316 buffer.set_language_registry(language_registry);
1317 cx.spawn_weak(|buffer, mut cx| async move {
1318 let markdown = markdown.await?;
1319 let buffer = buffer
1320 .upgrade(&cx)
1321 .ok_or_else(|| anyhow!("buffer was dropped"))?;
1322 buffer.update(&mut cx, |buffer: &mut Buffer, cx| {
1323 buffer.set_language(Some(markdown), cx)
1324 });
1325 anyhow::Ok(())
1326 })
1327 .detach_and_log_err(cx);
1328 buffer
1329 });
1330
1331 let settings = AssistantSettings::get_global(cx);
1332 let model = settings.default_open_ai_model.clone();
1333
1334 let mut this = Self {
1335 id: Some(Uuid::new_v4().to_string()),
1336 message_anchors: Default::default(),
1337 messages_metadata: Default::default(),
1338 next_message_id: Default::default(),
1339 summary: None,
1340 pending_summary: Task::ready(None),
1341 completion_count: Default::default(),
1342 pending_completions: Default::default(),
1343 token_count: None,
1344 max_token_count: tiktoken_rs::model::get_context_size(&model.full_name()),
1345 pending_token_count: Task::ready(None),
1346 model: model.clone(),
1347 _subscriptions: vec![cx.subscribe(&buffer, Self::handle_buffer_event)],
1348 pending_save: Task::ready(Ok(())),
1349 path: None,
1350 buffer,
1351 completion_provider,
1352 };
1353 let message = MessageAnchor {
1354 id: MessageId(post_inc(&mut this.next_message_id.0)),
1355 start: language::Anchor::MIN,
1356 };
1357 this.message_anchors.push(message.clone());
1358 this.messages_metadata.insert(
1359 message.id,
1360 MessageMetadata {
1361 role: Role::User,
1362 sent_at: Local::now(),
1363 status: MessageStatus::Done,
1364 },
1365 );
1366
1367 this.count_remaining_tokens(cx);
1368 this
1369 }
1370
1371 fn serialize(&self, cx: &AppContext) -> SavedConversation {
1372 SavedConversation {
1373 id: self.id.clone(),
1374 zed: "conversation".into(),
1375 version: SavedConversation::VERSION.into(),
1376 text: self.buffer.read(cx).text(),
1377 message_metadata: self.messages_metadata.clone(),
1378 messages: self
1379 .messages(cx)
1380 .map(|message| SavedMessage {
1381 id: message.id,
1382 start: message.offset_range.start,
1383 })
1384 .collect(),
1385 summary: self
1386 .summary
1387 .as_ref()
1388 .map(|summary| summary.text.clone())
1389 .unwrap_or_default(),
1390 model: self.model.clone(),
1391 }
1392 }
1393
1394 fn deserialize(
1395 saved_conversation: SavedConversation,
1396 path: PathBuf,
1397 language_registry: Arc<LanguageRegistry>,
1398 cx: &mut ModelContext<Self>,
1399 ) -> Self {
1400 let id = match saved_conversation.id {
1401 Some(id) => Some(id),
1402 None => Some(Uuid::new_v4().to_string()),
1403 };
1404 let model = saved_conversation.model;
1405 let completion_provider: Arc<dyn CompletionProvider> = Arc::new(
1406 OpenAICompletionProvider::new(model.full_name(), cx.background_executor().clone()),
1407 );
1408 completion_provider.retrieve_credentials(cx);
1409 let markdown = language_registry.language_for_name("Markdown");
1410 let mut message_anchors = Vec::new();
1411 let mut next_message_id = MessageId(0);
1412 let buffer = cx.add_model(|cx| {
1413 let mut buffer = Buffer::new(0, cx.model_id() as u64, saved_conversation.text);
1414 for message in saved_conversation.messages {
1415 message_anchors.push(MessageAnchor {
1416 id: message.id,
1417 start: buffer.anchor_before(message.start),
1418 });
1419 next_message_id = cmp::max(next_message_id, MessageId(message.id.0 + 1));
1420 }
1421 buffer.set_language_registry(language_registry);
1422 cx.spawn_weak(|buffer, mut cx| async move {
1423 let markdown = markdown.await?;
1424 let buffer = buffer
1425 .upgrade(&cx)
1426 .ok_or_else(|| anyhow!("buffer was dropped"))?;
1427 buffer.update(&mut cx, |buffer: &mut Buffer, cx| {
1428 buffer.set_language(Some(markdown), cx)
1429 });
1430 anyhow::Ok(())
1431 })
1432 .detach_and_log_err(cx);
1433 buffer
1434 });
1435
1436 let mut this = Self {
1437 id,
1438 message_anchors,
1439 messages_metadata: saved_conversation.message_metadata,
1440 next_message_id,
1441 summary: Some(Summary {
1442 text: saved_conversation.summary,
1443 done: true,
1444 }),
1445 pending_summary: Task::ready(None),
1446 completion_count: Default::default(),
1447 pending_completions: Default::default(),
1448 token_count: None,
1449 max_token_count: tiktoken_rs::model::get_context_size(&model.full_name()),
1450 pending_token_count: Task::ready(None),
1451 model,
1452 _subscriptions: vec![cx.subscribe(&buffer, Self::handle_buffer_event)],
1453 pending_save: Task::ready(Ok(())),
1454 path: Some(path),
1455 buffer,
1456 completion_provider,
1457 };
1458 this.count_remaining_tokens(cx);
1459 this
1460 }
1461
1462 fn handle_buffer_event(
1463 &mut self,
1464 _: Model<Buffer>,
1465 event: &language::Event,
1466 cx: &mut ModelContext<Self>,
1467 ) {
1468 match event {
1469 language::Event::Edited => {
1470 self.count_remaining_tokens(cx);
1471 cx.emit(ConversationEvent::MessagesEdited);
1472 }
1473 _ => {}
1474 }
1475 }
1476
1477 fn count_remaining_tokens(&mut self, cx: &mut ModelContext<Self>) {
1478 let messages = self
1479 .messages(cx)
1480 .into_iter()
1481 .filter_map(|message| {
1482 Some(tiktoken_rs::ChatCompletionRequestMessage {
1483 role: match message.role {
1484 Role::User => "user".into(),
1485 Role::Assistant => "assistant".into(),
1486 Role::System => "system".into(),
1487 },
1488 content: Some(
1489 self.buffer
1490 .read(cx)
1491 .text_for_range(message.offset_range)
1492 .collect(),
1493 ),
1494 name: None,
1495 function_call: None,
1496 })
1497 })
1498 .collect::<Vec<_>>();
1499 let model = self.model.clone();
1500 self.pending_token_count = cx.spawn_weak(|this, mut cx| {
1501 async move {
1502 cx.background_executor()
1503 .timer(Duration::from_millis(200))
1504 .await;
1505 let token_count = cx
1506 .background()
1507 .spawn(async move {
1508 tiktoken_rs::num_tokens_from_messages(&model.full_name(), &messages)
1509 })
1510 .await?;
1511
1512 this.upgrade(&cx)
1513 .ok_or_else(|| anyhow!("conversation was dropped"))?
1514 .update(&mut cx, |this, cx| {
1515 this.max_token_count =
1516 tiktoken_rs::model::get_context_size(&this.model.full_name());
1517 this.token_count = Some(token_count);
1518 cx.notify()
1519 });
1520 anyhow::Ok(())
1521 }
1522 .log_err()
1523 });
1524 }
1525
1526 fn remaining_tokens(&self) -> Option<isize> {
1527 Some(self.max_token_count as isize - self.token_count? as isize)
1528 }
1529
1530 fn set_model(&mut self, model: OpenAIModel, cx: &mut ModelContext<Self>) {
1531 self.model = model;
1532 self.count_remaining_tokens(cx);
1533 cx.notify();
1534 }
1535
1536 fn assist(
1537 &mut self,
1538 selected_messages: HashSet<MessageId>,
1539 cx: &mut ModelContext<Self>,
1540 ) -> Vec<MessageAnchor> {
1541 let mut user_messages = Vec::new();
1542
1543 let last_message_id = if let Some(last_message_id) =
1544 self.message_anchors.iter().rev().find_map(|message| {
1545 message
1546 .start
1547 .is_valid(self.buffer.read(cx))
1548 .then_some(message.id)
1549 }) {
1550 last_message_id
1551 } else {
1552 return Default::default();
1553 };
1554
1555 let mut should_assist = false;
1556 for selected_message_id in selected_messages {
1557 let selected_message_role =
1558 if let Some(metadata) = self.messages_metadata.get(&selected_message_id) {
1559 metadata.role
1560 } else {
1561 continue;
1562 };
1563
1564 if selected_message_role == Role::Assistant {
1565 if let Some(user_message) = self.insert_message_after(
1566 selected_message_id,
1567 Role::User,
1568 MessageStatus::Done,
1569 cx,
1570 ) {
1571 user_messages.push(user_message);
1572 }
1573 } else {
1574 should_assist = true;
1575 }
1576 }
1577
1578 if should_assist {
1579 if !self.completion_provider.has_credentials() {
1580 return Default::default();
1581 }
1582
1583 let request: Box<dyn CompletionRequest> = Box::new(OpenAIRequest {
1584 model: self.model.full_name().to_string(),
1585 messages: self
1586 .messages(cx)
1587 .filter(|message| matches!(message.status, MessageStatus::Done))
1588 .map(|message| message.to_open_ai_message(self.buffer.read(cx)))
1589 .collect(),
1590 stream: true,
1591 stop: vec![],
1592 temperature: 1.0,
1593 });
1594
1595 let stream = self.completion_provider.complete(request);
1596 let assistant_message = self
1597 .insert_message_after(last_message_id, Role::Assistant, MessageStatus::Pending, cx)
1598 .unwrap();
1599
1600 // Queue up the user's next reply.
1601 let user_message = self
1602 .insert_message_after(assistant_message.id, Role::User, MessageStatus::Done, cx)
1603 .unwrap();
1604 user_messages.push(user_message);
1605
1606 let task = cx.spawn_weak({
1607 |this, mut cx| async move {
1608 let assistant_message_id = assistant_message.id;
1609 let stream_completion = async {
1610 let mut messages = stream.await?;
1611
1612 while let Some(message) = messages.next().await {
1613 let text = message?;
1614
1615 this.upgrade(&cx)
1616 .ok_or_else(|| anyhow!("conversation was dropped"))?
1617 .update(&mut cx, |this, cx| {
1618 let message_ix = this
1619 .message_anchors
1620 .iter()
1621 .position(|message| message.id == assistant_message_id)?;
1622 this.buffer.update(cx, |buffer, cx| {
1623 let offset = this.message_anchors[message_ix + 1..]
1624 .iter()
1625 .find(|message| message.start.is_valid(buffer))
1626 .map_or(buffer.len(), |message| {
1627 message.start.to_offset(buffer).saturating_sub(1)
1628 });
1629 buffer.edit([(offset..offset, text)], None, cx);
1630 });
1631 cx.emit(ConversationEvent::StreamedCompletion);
1632
1633 Some(())
1634 });
1635 smol::future::yield_now().await;
1636 }
1637
1638 this.upgrade(&cx)
1639 .ok_or_else(|| anyhow!("conversation was dropped"))?
1640 .update(&mut cx, |this, cx| {
1641 this.pending_completions
1642 .retain(|completion| completion.id != this.completion_count);
1643 this.summarize(cx);
1644 });
1645
1646 anyhow::Ok(())
1647 };
1648
1649 let result = stream_completion.await;
1650 if let Some(this) = this.upgrade(&cx) {
1651 this.update(&mut cx, |this, cx| {
1652 if let Some(metadata) =
1653 this.messages_metadata.get_mut(&assistant_message.id)
1654 {
1655 match result {
1656 Ok(_) => {
1657 metadata.status = MessageStatus::Done;
1658 }
1659 Err(error) => {
1660 metadata.status =
1661 MessageStatus::Error(error.to_string().trim().into());
1662 }
1663 }
1664 cx.notify();
1665 }
1666 });
1667 }
1668 }
1669 });
1670
1671 self.pending_completions.push(PendingCompletion {
1672 id: post_inc(&mut self.completion_count),
1673 _task: task,
1674 });
1675 }
1676
1677 user_messages
1678 }
1679
1680 fn cancel_last_assist(&mut self) -> bool {
1681 self.pending_completions.pop().is_some()
1682 }
1683
1684 fn cycle_message_roles(&mut self, ids: HashSet<MessageId>, cx: &mut ModelContext<Self>) {
1685 for id in ids {
1686 if let Some(metadata) = self.messages_metadata.get_mut(&id) {
1687 metadata.role.cycle();
1688 cx.emit(ConversationEvent::MessagesEdited);
1689 cx.notify();
1690 }
1691 }
1692 }
1693
1694 fn insert_message_after(
1695 &mut self,
1696 message_id: MessageId,
1697 role: Role,
1698 status: MessageStatus,
1699 cx: &mut ModelContext<Self>,
1700 ) -> Option<MessageAnchor> {
1701 if let Some(prev_message_ix) = self
1702 .message_anchors
1703 .iter()
1704 .position(|message| message.id == message_id)
1705 {
1706 // Find the next valid message after the one we were given.
1707 let mut next_message_ix = prev_message_ix + 1;
1708 while let Some(next_message) = self.message_anchors.get(next_message_ix) {
1709 if next_message.start.is_valid(self.buffer.read(cx)) {
1710 break;
1711 }
1712 next_message_ix += 1;
1713 }
1714
1715 let start = self.buffer.update(cx, |buffer, cx| {
1716 let offset = self
1717 .message_anchors
1718 .get(next_message_ix)
1719 .map_or(buffer.len(), |message| message.start.to_offset(buffer) - 1);
1720 buffer.edit([(offset..offset, "\n")], None, cx);
1721 buffer.anchor_before(offset + 1)
1722 });
1723 let message = MessageAnchor {
1724 id: MessageId(post_inc(&mut self.next_message_id.0)),
1725 start,
1726 };
1727 self.message_anchors
1728 .insert(next_message_ix, message.clone());
1729 self.messages_metadata.insert(
1730 message.id,
1731 MessageMetadata {
1732 role,
1733 sent_at: Local::now(),
1734 status,
1735 },
1736 );
1737 cx.emit(ConversationEvent::MessagesEdited);
1738 Some(message)
1739 } else {
1740 None
1741 }
1742 }
1743
1744 fn split_message(
1745 &mut self,
1746 range: Range<usize>,
1747 cx: &mut ModelContext<Self>,
1748 ) -> (Option<MessageAnchor>, Option<MessageAnchor>) {
1749 let start_message = self.message_for_offset(range.start, cx);
1750 let end_message = self.message_for_offset(range.end, cx);
1751 if let Some((start_message, end_message)) = start_message.zip(end_message) {
1752 // Prevent splitting when range spans multiple messages.
1753 if start_message.id != end_message.id {
1754 return (None, None);
1755 }
1756
1757 let message = start_message;
1758 let role = message.role;
1759 let mut edited_buffer = false;
1760
1761 let mut suffix_start = None;
1762 if range.start > message.offset_range.start && range.end < message.offset_range.end - 1
1763 {
1764 if self.buffer.read(cx).chars_at(range.end).next() == Some('\n') {
1765 suffix_start = Some(range.end + 1);
1766 } else if self.buffer.read(cx).reversed_chars_at(range.end).next() == Some('\n') {
1767 suffix_start = Some(range.end);
1768 }
1769 }
1770
1771 let suffix = if let Some(suffix_start) = suffix_start {
1772 MessageAnchor {
1773 id: MessageId(post_inc(&mut self.next_message_id.0)),
1774 start: self.buffer.read(cx).anchor_before(suffix_start),
1775 }
1776 } else {
1777 self.buffer.update(cx, |buffer, cx| {
1778 buffer.edit([(range.end..range.end, "\n")], None, cx);
1779 });
1780 edited_buffer = true;
1781 MessageAnchor {
1782 id: MessageId(post_inc(&mut self.next_message_id.0)),
1783 start: self.buffer.read(cx).anchor_before(range.end + 1),
1784 }
1785 };
1786
1787 self.message_anchors
1788 .insert(message.index_range.end + 1, suffix.clone());
1789 self.messages_metadata.insert(
1790 suffix.id,
1791 MessageMetadata {
1792 role,
1793 sent_at: Local::now(),
1794 status: MessageStatus::Done,
1795 },
1796 );
1797
1798 let new_messages =
1799 if range.start == range.end || range.start == message.offset_range.start {
1800 (None, Some(suffix))
1801 } else {
1802 let mut prefix_end = None;
1803 if range.start > message.offset_range.start
1804 && range.end < message.offset_range.end - 1
1805 {
1806 if self.buffer.read(cx).chars_at(range.start).next() == Some('\n') {
1807 prefix_end = Some(range.start + 1);
1808 } else if self.buffer.read(cx).reversed_chars_at(range.start).next()
1809 == Some('\n')
1810 {
1811 prefix_end = Some(range.start);
1812 }
1813 }
1814
1815 let selection = if let Some(prefix_end) = prefix_end {
1816 cx.emit(ConversationEvent::MessagesEdited);
1817 MessageAnchor {
1818 id: MessageId(post_inc(&mut self.next_message_id.0)),
1819 start: self.buffer.read(cx).anchor_before(prefix_end),
1820 }
1821 } else {
1822 self.buffer.update(cx, |buffer, cx| {
1823 buffer.edit([(range.start..range.start, "\n")], None, cx)
1824 });
1825 edited_buffer = true;
1826 MessageAnchor {
1827 id: MessageId(post_inc(&mut self.next_message_id.0)),
1828 start: self.buffer.read(cx).anchor_before(range.end + 1),
1829 }
1830 };
1831
1832 self.message_anchors
1833 .insert(message.index_range.end + 1, selection.clone());
1834 self.messages_metadata.insert(
1835 selection.id,
1836 MessageMetadata {
1837 role,
1838 sent_at: Local::now(),
1839 status: MessageStatus::Done,
1840 },
1841 );
1842 (Some(selection), Some(suffix))
1843 };
1844
1845 if !edited_buffer {
1846 cx.emit(ConversationEvent::MessagesEdited);
1847 }
1848 new_messages
1849 } else {
1850 (None, None)
1851 }
1852 }
1853
1854 fn summarize(&mut self, cx: &mut ModelContext<Self>) {
1855 if self.message_anchors.len() >= 2 && self.summary.is_none() {
1856 if !self.completion_provider.has_credentials() {
1857 return;
1858 }
1859
1860 let messages = self
1861 .messages(cx)
1862 .take(2)
1863 .map(|message| message.to_open_ai_message(self.buffer.read(cx)))
1864 .chain(Some(RequestMessage {
1865 role: Role::User,
1866 content: "Summarize the conversation into a short title without punctuation"
1867 .into(),
1868 }));
1869 let request: Box<dyn CompletionRequest> = Box::new(OpenAIRequest {
1870 model: self.model.full_name().to_string(),
1871 messages: messages.collect(),
1872 stream: true,
1873 stop: vec![],
1874 temperature: 1.0,
1875 });
1876
1877 let stream = self.completion_provider.complete(request);
1878 self.pending_summary = cx.spawn(|this, mut cx| {
1879 async move {
1880 let mut messages = stream.await?;
1881
1882 while let Some(message) = messages.next().await {
1883 let text = message?;
1884 this.update(&mut cx, |this, cx| {
1885 this.summary
1886 .get_or_insert(Default::default())
1887 .text
1888 .push_str(&text);
1889 cx.emit(ConversationEvent::SummaryChanged);
1890 });
1891 }
1892
1893 this.update(&mut cx, |this, cx| {
1894 if let Some(summary) = this.summary.as_mut() {
1895 summary.done = true;
1896 cx.emit(ConversationEvent::SummaryChanged);
1897 }
1898 });
1899
1900 anyhow::Ok(())
1901 }
1902 .log_err()
1903 });
1904 }
1905 }
1906
1907 fn message_for_offset(&self, offset: usize, cx: &AppContext) -> Option<Message> {
1908 self.messages_for_offsets([offset], cx).pop()
1909 }
1910
1911 fn messages_for_offsets(
1912 &self,
1913 offsets: impl IntoIterator<Item = usize>,
1914 cx: &AppContext,
1915 ) -> Vec<Message> {
1916 let mut result = Vec::new();
1917
1918 let mut messages = self.messages(cx).peekable();
1919 let mut offsets = offsets.into_iter().peekable();
1920 let mut current_message = messages.next();
1921 while let Some(offset) = offsets.next() {
1922 // Locate the message that contains the offset.
1923 while current_message.as_ref().map_or(false, |message| {
1924 !message.offset_range.contains(&offset) && messages.peek().is_some()
1925 }) {
1926 current_message = messages.next();
1927 }
1928 let Some(message) = current_message.as_ref() else {
1929 break;
1930 };
1931
1932 // Skip offsets that are in the same message.
1933 while offsets.peek().map_or(false, |offset| {
1934 message.offset_range.contains(offset) || messages.peek().is_none()
1935 }) {
1936 offsets.next();
1937 }
1938
1939 result.push(message.clone());
1940 }
1941 result
1942 }
1943
1944 fn messages<'a>(&'a self, cx: &'a AppContext) -> impl 'a + Iterator<Item = Message> {
1945 let buffer = self.buffer.read(cx);
1946 let mut message_anchors = self.message_anchors.iter().enumerate().peekable();
1947 iter::from_fn(move || {
1948 while let Some((start_ix, message_anchor)) = message_anchors.next() {
1949 let metadata = self.messages_metadata.get(&message_anchor.id)?;
1950 let message_start = message_anchor.start.to_offset(buffer);
1951 let mut message_end = None;
1952 let mut end_ix = start_ix;
1953 while let Some((_, next_message)) = message_anchors.peek() {
1954 if next_message.start.is_valid(buffer) {
1955 message_end = Some(next_message.start);
1956 break;
1957 } else {
1958 end_ix += 1;
1959 message_anchors.next();
1960 }
1961 }
1962 let message_end = message_end
1963 .unwrap_or(language::Anchor::MAX)
1964 .to_offset(buffer);
1965 return Some(Message {
1966 index_range: start_ix..end_ix,
1967 offset_range: message_start..message_end,
1968 id: message_anchor.id,
1969 anchor: message_anchor.start,
1970 role: metadata.role,
1971 sent_at: metadata.sent_at,
1972 status: metadata.status.clone(),
1973 });
1974 }
1975 None
1976 })
1977 }
1978
1979 fn save(
1980 &mut self,
1981 debounce: Option<Duration>,
1982 fs: Arc<dyn Fs>,
1983 cx: &mut ModelContext<Conversation>,
1984 ) {
1985 self.pending_save = cx.spawn(|this, mut cx| async move {
1986 if let Some(debounce) = debounce {
1987 cx.background_executor().timer(debounce).await;
1988 }
1989
1990 let (old_path, summary) = this.read_with(&cx, |this, _| {
1991 let path = this.path.clone();
1992 let summary = if let Some(summary) = this.summary.as_ref() {
1993 if summary.done {
1994 Some(summary.text.clone())
1995 } else {
1996 None
1997 }
1998 } else {
1999 None
2000 };
2001 (path, summary)
2002 });
2003
2004 if let Some(summary) = summary {
2005 let conversation = this.read_with(&cx, |this, cx| this.serialize(cx));
2006 let path = if let Some(old_path) = old_path {
2007 old_path
2008 } else {
2009 let mut discriminant = 1;
2010 let mut new_path;
2011 loop {
2012 new_path = CONVERSATIONS_DIR.join(&format!(
2013 "{} - {}.zed.json",
2014 summary.trim(),
2015 discriminant
2016 ));
2017 if fs.is_file(&new_path).await {
2018 discriminant += 1;
2019 } else {
2020 break;
2021 }
2022 }
2023 new_path
2024 };
2025
2026 fs.create_dir(CONVERSATIONS_DIR.as_ref()).await?;
2027 fs.atomic_write(path.clone(), serde_json::to_string(&conversation).unwrap())
2028 .await?;
2029 this.update(&mut cx, |this, _| this.path = Some(path));
2030 }
2031
2032 Ok(())
2033 });
2034 }
2035}
2036
2037struct PendingCompletion {
2038 id: usize,
2039 _task: Task<()>,
2040}
2041
2042enum ConversationEditorEvent {
2043 TabContentChanged,
2044}
2045
2046#[derive(Copy, Clone, Debug, PartialEq)]
2047struct ScrollPosition {
2048 offset_before_cursor: gpui::Point<f32>,
2049 cursor: Anchor,
2050}
2051
2052struct ConversationEditor {
2053 conversation: Model<Conversation>,
2054 fs: Arc<dyn Fs>,
2055 workspace: WeakView<Workspace>,
2056 editor: View<Editor>,
2057 blocks: HashSet<BlockId>,
2058 scroll_position: Option<ScrollPosition>,
2059 focus_handle: FocusHandle,
2060 _subscriptions: Vec<Subscription>,
2061}
2062
2063impl ConversationEditor {
2064 fn new(
2065 completion_provider: Arc<dyn CompletionProvider>,
2066 language_registry: Arc<LanguageRegistry>,
2067 fs: Arc<dyn Fs>,
2068 workspace: WeakView<Workspace>,
2069 cx: &mut ViewContext<Self>,
2070 ) -> Self {
2071 let conversation =
2072 cx.add_model(|cx| Conversation::new(language_registry, cx, completion_provider));
2073 Self::for_conversation(conversation, fs, workspace, cx)
2074 }
2075
2076 fn for_conversation(
2077 conversation: Model<Conversation>,
2078 fs: Arc<dyn Fs>,
2079 workspace: WeakView<Workspace>,
2080 cx: &mut ViewContext<Self>,
2081 ) -> Self {
2082 let editor = cx.add_view(|cx| {
2083 let mut editor = Editor::for_buffer(conversation.read(cx).buffer.clone(), None, cx);
2084 editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
2085 editor.set_show_gutter(false, cx);
2086 editor.set_show_wrap_guides(false, cx);
2087 editor
2088 });
2089
2090 let focus_handle = cx.focus_handle();
2091
2092 let _subscriptions = vec![
2093 cx.observe(&conversation, |_, _, cx| cx.notify()),
2094 cx.subscribe(&conversation, Self::handle_conversation_event),
2095 cx.subscribe(&editor, Self::handle_editor_event),
2096 cx.on_focus(&focus_handle, |this, _, cx| cx.focus(&this.editor)),
2097 ];
2098
2099 let mut this = Self {
2100 conversation,
2101 editor,
2102 blocks: Default::default(),
2103 scroll_position: None,
2104 fs,
2105 workspace,
2106 focus_handle,
2107 _subscriptions,
2108 };
2109 this.update_message_headers(cx);
2110 this
2111 }
2112
2113 fn assist(&mut self, _: &Assist, cx: &mut ViewContext<Self>) {
2114 report_assistant_event(
2115 self.workspace.clone(),
2116 self.conversation.read(cx).id.clone(),
2117 AssistantKind::Panel,
2118 cx,
2119 );
2120
2121 let cursors = self.cursors(cx);
2122
2123 let user_messages = self.conversation.update(cx, |conversation, cx| {
2124 let selected_messages = conversation
2125 .messages_for_offsets(cursors, cx)
2126 .into_iter()
2127 .map(|message| message.id)
2128 .collect();
2129 conversation.assist(selected_messages, cx)
2130 });
2131 let new_selections = user_messages
2132 .iter()
2133 .map(|message| {
2134 let cursor = message
2135 .start
2136 .to_offset(self.conversation.read(cx).buffer.read(cx));
2137 cursor..cursor
2138 })
2139 .collect::<Vec<_>>();
2140 if !new_selections.is_empty() {
2141 self.editor.update(cx, |editor, cx| {
2142 editor.change_selections(
2143 Some(Autoscroll::Strategy(AutoscrollStrategy::Fit)),
2144 cx,
2145 |selections| selections.select_ranges(new_selections),
2146 );
2147 });
2148 // Avoid scrolling to the new cursor position so the assistant's output is stable.
2149 cx.defer(|this, _| this.scroll_position = None);
2150 }
2151 }
2152
2153 fn cancel_last_assist(&mut self, _: &editor::Cancel, cx: &mut ViewContext<Self>) {
2154 if !self
2155 .conversation
2156 .update(cx, |conversation, _| conversation.cancel_last_assist())
2157 {
2158 cx.propagate_action();
2159 }
2160 }
2161
2162 fn cycle_message_role(&mut self, _: &CycleMessageRole, cx: &mut ViewContext<Self>) {
2163 let cursors = self.cursors(cx);
2164 self.conversation.update(cx, |conversation, cx| {
2165 let messages = conversation
2166 .messages_for_offsets(cursors, cx)
2167 .into_iter()
2168 .map(|message| message.id)
2169 .collect();
2170 conversation.cycle_message_roles(messages, cx)
2171 });
2172 }
2173
2174 fn cursors(&self, cx: &AppContext) -> Vec<usize> {
2175 let selections = self.editor.read(cx).selections.all::<usize>(cx);
2176 selections
2177 .into_iter()
2178 .map(|selection| selection.head())
2179 .collect()
2180 }
2181
2182 fn handle_conversation_event(
2183 &mut self,
2184 _: Model<Conversation>,
2185 event: &ConversationEvent,
2186 cx: &mut ViewContext<Self>,
2187 ) {
2188 match event {
2189 ConversationEvent::MessagesEdited => {
2190 self.update_message_headers(cx);
2191 self.conversation.update(cx, |conversation, cx| {
2192 conversation.save(Some(Duration::from_millis(500)), self.fs.clone(), cx);
2193 });
2194 }
2195 ConversationEvent::SummaryChanged => {
2196 cx.emit(ConversationEditorEvent::TabContentChanged);
2197 self.conversation.update(cx, |conversation, cx| {
2198 conversation.save(None, self.fs.clone(), cx);
2199 });
2200 }
2201 ConversationEvent::StreamedCompletion => {
2202 self.editor.update(cx, |editor, cx| {
2203 if let Some(scroll_position) = self.scroll_position {
2204 let snapshot = editor.snapshot(cx);
2205 let cursor_point = scroll_position.cursor.to_display_point(&snapshot);
2206 let scroll_top =
2207 cursor_point.row() as f32 - scroll_position.offset_before_cursor.y;
2208 editor.set_scroll_position(
2209 point(scroll_position.offset_before_cursor.x, scroll_top),
2210 cx,
2211 );
2212 }
2213 });
2214 }
2215 }
2216 }
2217
2218 fn handle_editor_event(
2219 &mut self,
2220 _: View<Editor>,
2221 event: &EditorEvent,
2222 cx: &mut ViewContext<Self>,
2223 ) {
2224 match event {
2225 EditorEvent::ScrollPositionChanged { autoscroll, .. } => {
2226 let cursor_scroll_position = self.cursor_scroll_position(cx);
2227 if *autoscroll {
2228 self.scroll_position = cursor_scroll_position;
2229 } else if self.scroll_position != cursor_scroll_position {
2230 self.scroll_position = None;
2231 }
2232 }
2233 EditorEvent::SelectionsChanged { .. } => {
2234 self.scroll_position = self.cursor_scroll_position(cx);
2235 }
2236 _ => {}
2237 }
2238 }
2239
2240 fn cursor_scroll_position(&self, cx: &mut ViewContext<Self>) -> Option<ScrollPosition> {
2241 self.editor.update(cx, |editor, cx| {
2242 let snapshot = editor.snapshot(cx);
2243 let cursor = editor.selections.newest_anchor().head();
2244 let cursor_row = cursor.to_display_point(&snapshot.display_snapshot).row() as f32;
2245 let scroll_position = editor
2246 .scroll_manager
2247 .anchor()
2248 .scroll_position(&snapshot.display_snapshot);
2249
2250 let scroll_bottom = scroll_position.y() + editor.visible_line_count().unwrap_or(0.);
2251 if (scroll_position.y()..scroll_bottom).contains(&cursor_row) {
2252 Some(ScrollPosition {
2253 cursor,
2254 offset_before_cursor: point(scroll_position.x, cursor_row - scroll_position.y),
2255 })
2256 } else {
2257 None
2258 }
2259 })
2260 }
2261
2262 fn update_message_headers(&mut self, cx: &mut ViewContext<Self>) {
2263 self.editor.update(cx, |editor, cx| {
2264 let buffer = editor.buffer().read(cx).snapshot(cx);
2265 let excerpt_id = *buffer.as_singleton().unwrap().0;
2266 let old_blocks = std::mem::take(&mut self.blocks);
2267 let new_blocks = self
2268 .conversation
2269 .read(cx)
2270 .messages(cx)
2271 .map(|message| BlockProperties {
2272 position: buffer.anchor_in_excerpt(excerpt_id, message.anchor),
2273 height: 2,
2274 style: BlockStyle::Sticky,
2275 render: Arc::new({
2276 let conversation = self.conversation.clone();
2277 move |cx| {
2278 let message_id = message.id;
2279 let sender = ButtonLike::new("role")
2280 .child(match message.role {
2281 Role::User => Label::new("You").color(Color::Default),
2282 Role::Assistant => {
2283 Label::new("Assistant").color(Color::Modified)
2284 }
2285 Role::System => Label::new("System").color(Color::Warning),
2286 })
2287 .on_click({
2288 let conversation = conversation.clone();
2289 move |_, _, cx| {
2290 conversation.update(cx, |conversation, cx| {
2291 conversation.cycle_message_roles(
2292 HashSet::from_iter(Some(message_id)),
2293 cx,
2294 )
2295 })
2296 }
2297 });
2298
2299 h_stack()
2300 .id(("message_header", message_id.0))
2301 .border()
2302 .border_color(gpui::red())
2303 .child(sender)
2304 .child(Label::new(message.sent_at.format("%I:%M%P").to_string()))
2305 .with_children(
2306 if let MessageStatus::Error(error) = &message.status {
2307 Some(
2308 div()
2309 .id("error")
2310 .tooltip(|cx| Tooltip::text(error, cx))
2311 .child(IconElement::new(Icon::XCircle)),
2312 )
2313 } else {
2314 None
2315 },
2316 )
2317 .into_any_element()
2318 }
2319 }),
2320 disposition: BlockDisposition::Above,
2321 })
2322 .collect::<Vec<_>>();
2323
2324 editor.remove_blocks(old_blocks, None, cx);
2325 let ids = editor.insert_blocks(new_blocks, None, cx);
2326 self.blocks = HashSet::from_iter(ids);
2327 });
2328 }
2329
2330 fn quote_selection(
2331 workspace: &mut Workspace,
2332 _: &QuoteSelection,
2333 cx: &mut ViewContext<Workspace>,
2334 ) {
2335 let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
2336 return;
2337 };
2338 let Some(editor) = workspace
2339 .active_item(cx)
2340 .and_then(|item| item.act_as::<Editor>(cx))
2341 else {
2342 return;
2343 };
2344
2345 let text = editor.read_with(cx, |editor, cx| {
2346 let range = editor.selections.newest::<usize>(cx).range();
2347 let buffer = editor.buffer().read(cx).snapshot(cx);
2348 let start_language = buffer.language_at(range.start);
2349 let end_language = buffer.language_at(range.end);
2350 let language_name = if start_language == end_language {
2351 start_language.map(|language| language.name())
2352 } else {
2353 None
2354 };
2355 let language_name = language_name.as_deref().unwrap_or("").to_lowercase();
2356
2357 let selected_text = buffer.text_for_range(range).collect::<String>();
2358 if selected_text.is_empty() {
2359 None
2360 } else {
2361 Some(if language_name == "markdown" {
2362 selected_text
2363 .lines()
2364 .map(|line| format!("> {}", line))
2365 .collect::<Vec<_>>()
2366 .join("\n")
2367 } else {
2368 format!("```{language_name}\n{selected_text}\n```")
2369 })
2370 }
2371 });
2372
2373 // Activate the panel
2374 if !panel.read(cx).has_focus(cx) {
2375 workspace.toggle_panel_focus::<AssistantPanel>(cx);
2376 }
2377
2378 if let Some(text) = text {
2379 panel.update(cx, |panel, cx| {
2380 let conversation = panel
2381 .active_editor()
2382 .cloned()
2383 .unwrap_or_else(|| panel.new_conversation(cx));
2384 conversation.update(cx, |conversation, cx| {
2385 conversation
2386 .editor
2387 .update(cx, |editor, cx| editor.insert(&text, cx))
2388 });
2389 });
2390 }
2391 }
2392
2393 fn copy(&mut self, _: &editor::Copy, cx: &mut ViewContext<Self>) {
2394 let editor = self.editor.read(cx);
2395 let conversation = self.conversation.read(cx);
2396 if editor.selections.count() == 1 {
2397 let selection = editor.selections.newest::<usize>(cx);
2398 let mut copied_text = String::new();
2399 let mut spanned_messages = 0;
2400 for message in conversation.messages(cx) {
2401 if message.offset_range.start >= selection.range().end {
2402 break;
2403 } else if message.offset_range.end >= selection.range().start {
2404 let range = cmp::max(message.offset_range.start, selection.range().start)
2405 ..cmp::min(message.offset_range.end, selection.range().end);
2406 if !range.is_empty() {
2407 spanned_messages += 1;
2408 write!(&mut copied_text, "## {}\n\n", message.role).unwrap();
2409 for chunk in conversation.buffer.read(cx).text_for_range(range) {
2410 copied_text.push_str(&chunk);
2411 }
2412 copied_text.push('\n');
2413 }
2414 }
2415 }
2416
2417 if spanned_messages > 1 {
2418 cx.platform()
2419 .write_to_clipboard(ClipboardItem::new(copied_text));
2420 return;
2421 }
2422 }
2423
2424 cx.propagate_action();
2425 }
2426
2427 fn split(&mut self, _: &Split, cx: &mut ViewContext<Self>) {
2428 self.conversation.update(cx, |conversation, cx| {
2429 let selections = self.editor.read(cx).selections.disjoint_anchors();
2430 for selection in selections.into_iter() {
2431 let buffer = self.editor.read(cx).buffer().read(cx).snapshot(cx);
2432 let range = selection
2433 .map(|endpoint| endpoint.to_offset(&buffer))
2434 .range();
2435 conversation.split_message(range, cx);
2436 }
2437 });
2438 }
2439
2440 fn save(&mut self, _: &Save, cx: &mut ViewContext<Self>) {
2441 self.conversation.update(cx, |conversation, cx| {
2442 conversation.save(None, self.fs.clone(), cx)
2443 });
2444 }
2445
2446 fn cycle_model(&mut self, cx: &mut ViewContext<Self>) {
2447 self.conversation.update(cx, |conversation, cx| {
2448 let new_model = conversation.model.cycle();
2449 conversation.set_model(new_model, cx);
2450 });
2451 }
2452
2453 fn title(&self, cx: &AppContext) -> String {
2454 self.conversation
2455 .read(cx)
2456 .summary
2457 .as_ref()
2458 .map(|summary| summary.text.clone())
2459 .unwrap_or_else(|| "New Conversation".into())
2460 }
2461
2462 fn render_current_model(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
2463 Button::new(
2464 "current_model",
2465 self.conversation.read(cx).model.short_name(),
2466 )
2467 .tooltip(move |cx| Tooltip::text("Change Model", cx))
2468 .on_click(cx.listener(|this, _, cx| this.cycle_model(cx)))
2469 }
2470
2471 fn render_remaining_tokens(&self, cx: &mut ViewContext<Self>) -> Option<impl IntoElement> {
2472 let remaining_tokens = self.conversation.read(cx).remaining_tokens()?;
2473 let remaining_tokens_color = if remaining_tokens <= 0 {
2474 Color::Error
2475 } else if remaining_tokens <= 500 {
2476 Color::Warning
2477 } else {
2478 Color::Default
2479 };
2480 Some(
2481 div()
2482 .border()
2483 .border_color(gpui::red())
2484 .child(Label::new(remaining_tokens.to_string()).color(remaining_tokens_color)),
2485 )
2486 }
2487}
2488
2489impl EventEmitter<ConversationEditorEvent> for ConversationEditor {}
2490
2491impl Render for ConversationEditor {
2492 type Element = Div;
2493
2494 fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
2495 div().relative().child(self.editor.clone()).child(
2496 h_stack()
2497 .absolute()
2498 .gap_1()
2499 .top_3()
2500 .right_5()
2501 .child(self.render_current_model(cx))
2502 .children(self.render_remaining_tokens(cx)),
2503 )
2504 }
2505}
2506
2507#[derive(Clone, Debug)]
2508struct MessageAnchor {
2509 id: MessageId,
2510 start: language::Anchor,
2511}
2512
2513#[derive(Clone, Debug)]
2514pub struct Message {
2515 offset_range: Range<usize>,
2516 index_range: Range<usize>,
2517 id: MessageId,
2518 anchor: language::Anchor,
2519 role: Role,
2520 sent_at: DateTime<Local>,
2521 status: MessageStatus,
2522}
2523
2524impl Message {
2525 fn to_open_ai_message(&self, buffer: &Buffer) -> RequestMessage {
2526 let content = buffer
2527 .text_for_range(self.offset_range.clone())
2528 .collect::<String>();
2529 RequestMessage {
2530 role: self.role,
2531 content: content.trim_end().into(),
2532 }
2533 }
2534}
2535
2536enum InlineAssistantEvent {
2537 Confirmed {
2538 prompt: String,
2539 include_conversation: bool,
2540 retrieve_context: bool,
2541 },
2542 Canceled,
2543 Dismissed,
2544 IncludeConversationToggled {
2545 include_conversation: bool,
2546 },
2547 RetrieveContextToggled {
2548 retrieve_context: bool,
2549 },
2550}
2551
2552struct InlineAssistant {
2553 id: usize,
2554 prompt_editor: View<Editor>,
2555 workspace: WeakView<Workspace>,
2556 confirmed: bool,
2557 focus_handle: FocusHandle,
2558 include_conversation: bool,
2559 measurements: Rc<Cell<BlockMeasurements>>,
2560 prompt_history: VecDeque<String>,
2561 prompt_history_ix: Option<usize>,
2562 pending_prompt: String,
2563 codegen: Model<Codegen>,
2564 _subscriptions: Vec<Subscription>,
2565 retrieve_context: bool,
2566 semantic_index: Option<Model<SemanticIndex>>,
2567 semantic_permissioned: Option<bool>,
2568 project: WeakModel<Project>,
2569 maintain_rate_limit: Option<Task<()>>,
2570}
2571
2572impl EventEmitter<InlineAssistantEvent> for InlineAssistant {}
2573
2574impl Render for InlineAssistant {
2575 type Element = Div;
2576
2577 fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
2578 let measurements = self.measurements.get();
2579 h_stack()
2580 .child(
2581 h_stack()
2582 .justify_center()
2583 .w(measurements.gutter_width)
2584 .child(
2585 IconButton::new("include_conversation", Icon::Ai)
2586 .action(ToggleIncludeConversation)
2587 .selected(self.include_conversation)
2588 .tooltip(Tooltip::for_action(
2589 "Include Conversation",
2590 &ToggleIncludeConversation,
2591 cx,
2592 )),
2593 )
2594 .children(if SemanticIndex::enabled(cx) {
2595 Some(
2596 IconButton::new("retrieve_context", Icon::MagnifyingGlass)
2597 .action(ToggleRetrieveContext)
2598 .selected(self.retrieve_context)
2599 .tooltip(Tooltip::for_action(
2600 "Retrieve Context",
2601 &ToggleRetrieveContext,
2602 cx,
2603 )),
2604 )
2605 } else {
2606 None
2607 })
2608 .children(if let Some(error) = self.codegen.read(cx).error() {
2609 Some(
2610 div()
2611 .id("error")
2612 .tooltip(|cx| Tooltip::text(error.to_string(), cx))
2613 .child(IconElement::new(Icon::XCircle).color(Color::Error)),
2614 )
2615 } else {
2616 None
2617 }),
2618 )
2619 .child(
2620 div()
2621 .ml(measurements.anchor_x - measurements.gutter_width)
2622 .child(self.prompt_editor.clone()),
2623 )
2624 .children(if self.retrieve_context {
2625 self.retrieve_context_status(cx)
2626 } else {
2627 None
2628 })
2629 }
2630}
2631
2632impl InlineAssistant {
2633 fn new(
2634 id: usize,
2635 measurements: Rc<Cell<BlockMeasurements>>,
2636 include_conversation: bool,
2637 prompt_history: VecDeque<String>,
2638 codegen: Model<Codegen>,
2639 workspace: WeakView<Workspace>,
2640 cx: &mut ViewContext<Self>,
2641 retrieve_context: bool,
2642 semantic_index: Option<Model<SemanticIndex>>,
2643 project: Model<Project>,
2644 ) -> Self {
2645 let prompt_editor = cx.build_view(|cx| {
2646 let mut editor = Editor::single_line(cx);
2647 let placeholder = match codegen.read(cx).kind() {
2648 CodegenKind::Transform { .. } => "Enter transformation prompt…",
2649 CodegenKind::Generate { .. } => "Enter generation prompt…",
2650 };
2651 editor.set_placeholder_text(placeholder, cx);
2652 editor
2653 });
2654
2655 let focus_handle = cx.focus_handle();
2656 let mut subscriptions = vec![
2657 cx.observe(&codegen, Self::handle_codegen_changed),
2658 cx.subscribe(&prompt_editor, Self::handle_prompt_editor_events),
2659 cx.on_focus(
2660 &focus_handle,
2661 cx.listener(|this, _, cx| cx.focus(&this.prompt_editor)),
2662 ),
2663 ];
2664
2665 if let Some(semantic_index) = semantic_index.clone() {
2666 subscriptions.push(cx.observe(&semantic_index, Self::semantic_index_changed));
2667 }
2668
2669 let assistant = Self {
2670 id,
2671 prompt_editor,
2672 workspace,
2673 confirmed: false,
2674 focus_handle,
2675 include_conversation,
2676 measurements,
2677 prompt_history,
2678 prompt_history_ix: None,
2679 pending_prompt: String::new(),
2680 codegen,
2681 _subscriptions: subscriptions,
2682 retrieve_context,
2683 semantic_permissioned: None,
2684 semantic_index,
2685 project: project.downgrade(),
2686 maintain_rate_limit: None,
2687 };
2688
2689 assistant.index_project(cx).log_err();
2690
2691 assistant
2692 }
2693
2694 fn semantic_permissioned(&self, cx: &mut ViewContext<Self>) -> Task<Result<bool>> {
2695 if let Some(value) = self.semantic_permissioned {
2696 return Task::ready(Ok(value));
2697 }
2698
2699 let Some(project) = self.project.upgrade() else {
2700 return Task::ready(Err(anyhow!("project was dropped")));
2701 };
2702
2703 self.semantic_index
2704 .as_ref()
2705 .map(|semantic| {
2706 semantic.update(cx, |this, cx| this.project_previously_indexed(&project, cx))
2707 })
2708 .unwrap_or(Task::ready(Ok(false)))
2709 }
2710
2711 fn handle_prompt_editor_events(
2712 &mut self,
2713 _: View<Editor>,
2714 event: &EditorEvent,
2715 cx: &mut ViewContext<Self>,
2716 ) {
2717 if let EditorEvent::Edited = event {
2718 self.pending_prompt = self.prompt_editor.read(cx).text(cx);
2719 cx.notify();
2720 }
2721 }
2722
2723 fn semantic_index_changed(
2724 &mut self,
2725 semantic_index: Model<SemanticIndex>,
2726 cx: &mut ViewContext<Self>,
2727 ) {
2728 let Some(project) = self.project.upgrade() else {
2729 return;
2730 };
2731
2732 let status = semantic_index.read(cx).status(&project);
2733 match status {
2734 SemanticIndexStatus::Indexing {
2735 rate_limit_expiry: Some(_),
2736 ..
2737 } => {
2738 if self.maintain_rate_limit.is_none() {
2739 self.maintain_rate_limit = Some(cx.spawn(|this, mut cx| async move {
2740 loop {
2741 cx.background_executor().timer(Duration::from_secs(1)).await;
2742 this.update(&mut cx, |_, cx| cx.notify()).log_err();
2743 }
2744 }));
2745 }
2746 return;
2747 }
2748 _ => {
2749 self.maintain_rate_limit = None;
2750 }
2751 }
2752 }
2753
2754 fn handle_codegen_changed(&mut self, _: Model<Codegen>, cx: &mut ViewContext<Self>) {
2755 let is_read_only = !self.codegen.read(cx).idle();
2756 self.prompt_editor.update(cx, |editor, cx| {
2757 let was_read_only = editor.read_only();
2758 if was_read_only != is_read_only {
2759 if is_read_only {
2760 editor.set_read_only(true);
2761 editor.set_field_editor_style(
2762 Some(Arc::new(|theme| {
2763 theme.assistant.inline.disabled_editor.clone()
2764 })),
2765 cx,
2766 );
2767 } else {
2768 self.confirmed = false;
2769 editor.set_read_only(false);
2770 editor.set_field_editor_style(
2771 Some(Arc::new(|theme| theme.assistant.inline.editor.clone())),
2772 cx,
2773 );
2774 }
2775 }
2776 });
2777 cx.notify();
2778 }
2779
2780 fn cancel(&mut self, _: &editor::Cancel, cx: &mut ViewContext<Self>) {
2781 cx.emit(InlineAssistantEvent::Canceled);
2782 }
2783
2784 fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
2785 if self.confirmed {
2786 cx.emit(InlineAssistantEvent::Dismissed);
2787 } else {
2788 report_assistant_event(self.workspace.clone(), None, AssistantKind::Inline, cx);
2789
2790 let prompt = self.prompt_editor.read(cx).text(cx);
2791 self.prompt_editor.update(cx, |editor, cx| {
2792 editor.set_read_only(true);
2793 editor.set_field_editor_style(
2794 Some(Arc::new(|theme| {
2795 theme.assistant.inline.disabled_editor.clone()
2796 })),
2797 cx,
2798 );
2799 });
2800 cx.emit(InlineAssistantEvent::Confirmed {
2801 prompt,
2802 include_conversation: self.include_conversation,
2803 retrieve_context: self.retrieve_context,
2804 });
2805 self.confirmed = true;
2806 cx.notify();
2807 }
2808 }
2809
2810 fn toggle_retrieve_context(&mut self, _: &ToggleRetrieveContext, cx: &mut ViewContext<Self>) {
2811 let semantic_permissioned = self.semantic_permissioned(cx);
2812
2813 let Some(project) = self.project.upgrade() else {
2814 return;
2815 };
2816
2817 let project_name = project
2818 .read(cx)
2819 .worktree_root_names(cx)
2820 .collect::<Vec<&str>>()
2821 .join("/");
2822 let is_plural = project_name.chars().filter(|letter| *letter == '/').count() > 0;
2823 let prompt_text = format!("Would you like to index the '{}' project{} for context retrieval? This requires sending code to the OpenAI API", project_name,
2824 if is_plural {
2825 "s"
2826 } else {""});
2827
2828 cx.spawn(|this, mut cx| async move {
2829 // If Necessary prompt user
2830 if !semantic_permissioned.await.unwrap_or(false) {
2831 let mut answer = this.update(&mut cx, |_, cx| {
2832 cx.prompt(
2833 PromptLevel::Info,
2834 prompt_text.as_str(),
2835 &["Continue", "Cancel"],
2836 )
2837 })?;
2838
2839 if answer.next().await == Some(0) {
2840 this.update(&mut cx, |this, _| {
2841 this.semantic_permissioned = Some(true);
2842 })?;
2843 } else {
2844 return anyhow::Ok(());
2845 }
2846 }
2847
2848 // If permissioned, update context appropriately
2849 this.update(&mut cx, |this, cx| {
2850 this.retrieve_context = !this.retrieve_context;
2851
2852 cx.emit(InlineAssistantEvent::RetrieveContextToggled {
2853 retrieve_context: this.retrieve_context,
2854 });
2855
2856 if this.retrieve_context {
2857 this.index_project(cx).log_err();
2858 }
2859
2860 cx.notify();
2861 })?;
2862
2863 anyhow::Ok(())
2864 })
2865 .detach_and_log_err(cx);
2866 }
2867
2868 fn index_project(&self, cx: &mut ViewContext<Self>) -> anyhow::Result<()> {
2869 let Some(project) = self.project.upgrade() else {
2870 return Err(anyhow!("project was dropped!"));
2871 };
2872
2873 let semantic_permissioned = self.semantic_permissioned(cx);
2874 if let Some(semantic_index) = SemanticIndex::global(cx) {
2875 cx.spawn(|_, mut cx| async move {
2876 // This has to be updated to accomodate for semantic_permissions
2877 if semantic_permissioned.await.unwrap_or(false) {
2878 semantic_index
2879 .update(&mut cx, |index, cx| index.index_project(project, cx))
2880 .await
2881 } else {
2882 Err(anyhow!("project is not permissioned for semantic indexing"))
2883 }
2884 })
2885 .detach_and_log_err(cx);
2886 }
2887
2888 anyhow::Ok(())
2889 }
2890
2891 fn retrieve_context_status(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
2892 enum ContextStatusIcon {}
2893
2894 let Some(project) = self.project.upgrade() else {
2895 return None;
2896 };
2897
2898 if let Some(semantic_index) = SemanticIndex::global(cx) {
2899 let status = semantic_index.update(cx, |index, _| index.status(&project));
2900 match status {
2901 SemanticIndexStatus::NotAuthenticated {} => Some(
2902 div()
2903 .id("error")
2904 .tooltip(|cx| Tooltip::text("Not Authenticated. Please ensure you have a valid 'OPENAI_API_KEY' in your environment variables.", cx))
2905 .child(IconElement::new(Icon::XCircle))
2906 .into_any_element()
2907 ),
2908
2909 SemanticIndexStatus::NotIndexed {} => Some(
2910 div()
2911 .id("error")
2912 .tooltip(|cx| Tooltip::text("Not Indexed", cx))
2913 .child(IconElement::new(Icon::XCircle))
2914 .into_any_element()
2915 ),
2916
2917 SemanticIndexStatus::Indexing {
2918 remaining_files,
2919 rate_limit_expiry,
2920 } => {
2921 let mut status_text = if remaining_files == 0 {
2922 "Indexing...".to_string()
2923 } else {
2924 format!("Remaining files to index: {remaining_files}")
2925 };
2926
2927 if let Some(rate_limit_expiry) = rate_limit_expiry {
2928 let remaining_seconds = rate_limit_expiry.duration_since(Instant::now());
2929 if remaining_seconds > Duration::from_secs(0) && remaining_files > 0 {
2930 write!(
2931 status_text,
2932 " (rate limit expires in {}s)",
2933 remaining_seconds.as_secs()
2934 )
2935 .unwrap();
2936 }
2937 }
2938 Some(
2939 div()
2940 .id("update")
2941 .tooltip(|cx| Tooltip::text(status_text, cx))
2942 .child(IconElement::new(Icon::Update).color(color))
2943 .into_any_element()
2944 Svg::new("icons/update.svg")
2945 .with_color(theme.assistant.inline.context_status.in_progress_icon.color)
2946 .constrained()
2947 .with_width(theme.assistant.inline.context_status.in_progress_icon.width)
2948 .contained()
2949 .with_style(theme.assistant.inline.context_status.in_progress_icon.container)
2950 .with_tooltip::<ContextStatusIcon>(
2951 self.id,
2952 status_text,
2953 None,
2954 theme.tooltip.clone(),
2955 cx,
2956 )
2957 .aligned()
2958 .into_any(),
2959 )
2960 }
2961
2962 SemanticIndexStatus::Indexed {} => Some(
2963 Svg::new("icons/check.svg")
2964 .with_color(theme.assistant.inline.context_status.complete_icon.color)
2965 .constrained()
2966 .with_width(theme.assistant.inline.context_status.complete_icon.width)
2967 .contained()
2968 .with_style(theme.assistant.inline.context_status.complete_icon.container)
2969 .with_tooltip::<ContextStatusIcon>(
2970 self.id,
2971 "Index up to date",
2972 None,
2973 theme.tooltip.clone(),
2974 cx,
2975 )
2976 .aligned()
2977 .into_any(),
2978 ),
2979 }
2980 } else {
2981 None
2982 }
2983 }
2984
2985 fn toggle_include_conversation(
2986 &mut self,
2987 _: &ToggleIncludeConversation,
2988 cx: &mut ViewContext<Self>,
2989 ) {
2990 self.include_conversation = !self.include_conversation;
2991 cx.emit(InlineAssistantEvent::IncludeConversationToggled {
2992 include_conversation: self.include_conversation,
2993 });
2994 cx.notify();
2995 }
2996
2997 fn move_up(&mut self, _: &MoveUp, cx: &mut ViewContext<Self>) {
2998 if let Some(ix) = self.prompt_history_ix {
2999 if ix > 0 {
3000 self.prompt_history_ix = Some(ix - 1);
3001 let prompt = self.prompt_history[ix - 1].clone();
3002 self.set_prompt(&prompt, cx);
3003 }
3004 } else if !self.prompt_history.is_empty() {
3005 self.prompt_history_ix = Some(self.prompt_history.len() - 1);
3006 let prompt = self.prompt_history[self.prompt_history.len() - 1].clone();
3007 self.set_prompt(&prompt, cx);
3008 }
3009 }
3010
3011 fn move_down(&mut self, _: &MoveDown, cx: &mut ViewContext<Self>) {
3012 if let Some(ix) = self.prompt_history_ix {
3013 if ix < self.prompt_history.len() - 1 {
3014 self.prompt_history_ix = Some(ix + 1);
3015 let prompt = self.prompt_history[ix + 1].clone();
3016 self.set_prompt(&prompt, cx);
3017 } else {
3018 self.prompt_history_ix = None;
3019 let pending_prompt = self.pending_prompt.clone();
3020 self.set_prompt(&pending_prompt, cx);
3021 }
3022 }
3023 }
3024
3025 fn set_prompt(&mut self, prompt: &str, cx: &mut ViewContext<Self>) {
3026 self.prompt_editor.update(cx, |editor, cx| {
3027 editor.buffer().update(cx, |buffer, cx| {
3028 let len = buffer.len(cx);
3029 buffer.edit([(0..len, prompt)], None, cx);
3030 });
3031 });
3032 }
3033}
3034
3035// This wouldn't need to exist if we could pass parameters when rendering child views.
3036#[derive(Copy, Clone, Default)]
3037struct BlockMeasurements {
3038 anchor_x: Pixels,
3039 gutter_width: Pixels,
3040}
3041
3042struct PendingInlineAssist {
3043 editor: WeakView<Editor>,
3044 inline_assistant: Option<(BlockId, View<InlineAssistant>)>,
3045 codegen: Model<Codegen>,
3046 _subscriptions: Vec<Subscription>,
3047 project: WeakModel<Project>,
3048}
3049
3050fn merge_ranges(ranges: &mut Vec<Range<Anchor>>, buffer: &MultiBufferSnapshot) {
3051 ranges.sort_unstable_by(|a, b| {
3052 a.start
3053 .cmp(&b.start, buffer)
3054 .then_with(|| b.end.cmp(&a.end, buffer))
3055 });
3056
3057 let mut ix = 0;
3058 while ix + 1 < ranges.len() {
3059 let b = ranges[ix + 1].clone();
3060 let a = &mut ranges[ix];
3061 if a.end.cmp(&b.start, buffer).is_gt() {
3062 if a.end.cmp(&b.end, buffer).is_lt() {
3063 a.end = b.end;
3064 }
3065 ranges.remove(ix + 1);
3066 } else {
3067 ix += 1;
3068 }
3069 }
3070}
3071
3072#[cfg(test)]
3073mod tests {
3074 use super::*;
3075 use crate::MessageId;
3076 use ai::test::FakeCompletionProvider;
3077 use gpui::AppContext;
3078
3079 #[gpui::test]
3080 fn test_inserting_and_removing_messages(cx: &mut AppContext) {
3081 cx.set_global(SettingsStore::test(cx));
3082 init(cx);
3083 let registry = Arc::new(LanguageRegistry::test());
3084
3085 let completion_provider = Arc::new(FakeCompletionProvider::new());
3086 let conversation = cx.add_model(|cx| Conversation::new(registry, cx, completion_provider));
3087 let buffer = conversation.read(cx).buffer.clone();
3088
3089 let message_1 = conversation.read(cx).message_anchors[0].clone();
3090 assert_eq!(
3091 messages(&conversation, cx),
3092 vec![(message_1.id, Role::User, 0..0)]
3093 );
3094
3095 let message_2 = conversation.update(cx, |conversation, cx| {
3096 conversation
3097 .insert_message_after(message_1.id, Role::Assistant, MessageStatus::Done, cx)
3098 .unwrap()
3099 });
3100 assert_eq!(
3101 messages(&conversation, cx),
3102 vec![
3103 (message_1.id, Role::User, 0..1),
3104 (message_2.id, Role::Assistant, 1..1)
3105 ]
3106 );
3107
3108 buffer.update(cx, |buffer, cx| {
3109 buffer.edit([(0..0, "1"), (1..1, "2")], None, cx)
3110 });
3111 assert_eq!(
3112 messages(&conversation, cx),
3113 vec![
3114 (message_1.id, Role::User, 0..2),
3115 (message_2.id, Role::Assistant, 2..3)
3116 ]
3117 );
3118
3119 let message_3 = conversation.update(cx, |conversation, cx| {
3120 conversation
3121 .insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx)
3122 .unwrap()
3123 });
3124 assert_eq!(
3125 messages(&conversation, cx),
3126 vec![
3127 (message_1.id, Role::User, 0..2),
3128 (message_2.id, Role::Assistant, 2..4),
3129 (message_3.id, Role::User, 4..4)
3130 ]
3131 );
3132
3133 let message_4 = conversation.update(cx, |conversation, cx| {
3134 conversation
3135 .insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx)
3136 .unwrap()
3137 });
3138 assert_eq!(
3139 messages(&conversation, cx),
3140 vec![
3141 (message_1.id, Role::User, 0..2),
3142 (message_2.id, Role::Assistant, 2..4),
3143 (message_4.id, Role::User, 4..5),
3144 (message_3.id, Role::User, 5..5),
3145 ]
3146 );
3147
3148 buffer.update(cx, |buffer, cx| {
3149 buffer.edit([(4..4, "C"), (5..5, "D")], None, cx)
3150 });
3151 assert_eq!(
3152 messages(&conversation, cx),
3153 vec![
3154 (message_1.id, Role::User, 0..2),
3155 (message_2.id, Role::Assistant, 2..4),
3156 (message_4.id, Role::User, 4..6),
3157 (message_3.id, Role::User, 6..7),
3158 ]
3159 );
3160
3161 // Deleting across message boundaries merges the messages.
3162 buffer.update(cx, |buffer, cx| buffer.edit([(1..4, "")], None, cx));
3163 assert_eq!(
3164 messages(&conversation, cx),
3165 vec![
3166 (message_1.id, Role::User, 0..3),
3167 (message_3.id, Role::User, 3..4),
3168 ]
3169 );
3170
3171 // Undoing the deletion should also undo the merge.
3172 buffer.update(cx, |buffer, cx| buffer.undo(cx));
3173 assert_eq!(
3174 messages(&conversation, cx),
3175 vec![
3176 (message_1.id, Role::User, 0..2),
3177 (message_2.id, Role::Assistant, 2..4),
3178 (message_4.id, Role::User, 4..6),
3179 (message_3.id, Role::User, 6..7),
3180 ]
3181 );
3182
3183 // Redoing the deletion should also redo the merge.
3184 buffer.update(cx, |buffer, cx| buffer.redo(cx));
3185 assert_eq!(
3186 messages(&conversation, cx),
3187 vec![
3188 (message_1.id, Role::User, 0..3),
3189 (message_3.id, Role::User, 3..4),
3190 ]
3191 );
3192
3193 // Ensure we can still insert after a merged message.
3194 let message_5 = conversation.update(cx, |conversation, cx| {
3195 conversation
3196 .insert_message_after(message_1.id, Role::System, MessageStatus::Done, cx)
3197 .unwrap()
3198 });
3199 assert_eq!(
3200 messages(&conversation, cx),
3201 vec![
3202 (message_1.id, Role::User, 0..3),
3203 (message_5.id, Role::System, 3..4),
3204 (message_3.id, Role::User, 4..5)
3205 ]
3206 );
3207 }
3208
3209 #[gpui::test]
3210 fn test_message_splitting(cx: &mut AppContext) {
3211 cx.set_global(SettingsStore::test(cx));
3212 init(cx);
3213 let registry = Arc::new(LanguageRegistry::test());
3214 let completion_provider = Arc::new(FakeCompletionProvider::new());
3215
3216 let conversation = cx.add_model(|cx| Conversation::new(registry, cx, completion_provider));
3217 let buffer = conversation.read(cx).buffer.clone();
3218
3219 let message_1 = conversation.read(cx).message_anchors[0].clone();
3220 assert_eq!(
3221 messages(&conversation, cx),
3222 vec![(message_1.id, Role::User, 0..0)]
3223 );
3224
3225 buffer.update(cx, |buffer, cx| {
3226 buffer.edit([(0..0, "aaa\nbbb\nccc\nddd\n")], None, cx)
3227 });
3228
3229 let (_, message_2) =
3230 conversation.update(cx, |conversation, cx| conversation.split_message(3..3, cx));
3231 let message_2 = message_2.unwrap();
3232
3233 // We recycle newlines in the middle of a split message
3234 assert_eq!(buffer.read(cx).text(), "aaa\nbbb\nccc\nddd\n");
3235 assert_eq!(
3236 messages(&conversation, cx),
3237 vec![
3238 (message_1.id, Role::User, 0..4),
3239 (message_2.id, Role::User, 4..16),
3240 ]
3241 );
3242
3243 let (_, message_3) =
3244 conversation.update(cx, |conversation, cx| conversation.split_message(3..3, cx));
3245 let message_3 = message_3.unwrap();
3246
3247 // We don't recycle newlines at the end of a split message
3248 assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\nccc\nddd\n");
3249 assert_eq!(
3250 messages(&conversation, cx),
3251 vec![
3252 (message_1.id, Role::User, 0..4),
3253 (message_3.id, Role::User, 4..5),
3254 (message_2.id, Role::User, 5..17),
3255 ]
3256 );
3257
3258 let (_, message_4) =
3259 conversation.update(cx, |conversation, cx| conversation.split_message(9..9, cx));
3260 let message_4 = message_4.unwrap();
3261 assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\nccc\nddd\n");
3262 assert_eq!(
3263 messages(&conversation, cx),
3264 vec![
3265 (message_1.id, Role::User, 0..4),
3266 (message_3.id, Role::User, 4..5),
3267 (message_2.id, Role::User, 5..9),
3268 (message_4.id, Role::User, 9..17),
3269 ]
3270 );
3271
3272 let (_, message_5) =
3273 conversation.update(cx, |conversation, cx| conversation.split_message(9..9, cx));
3274 let message_5 = message_5.unwrap();
3275 assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\n\nccc\nddd\n");
3276 assert_eq!(
3277 messages(&conversation, cx),
3278 vec![
3279 (message_1.id, Role::User, 0..4),
3280 (message_3.id, Role::User, 4..5),
3281 (message_2.id, Role::User, 5..9),
3282 (message_4.id, Role::User, 9..10),
3283 (message_5.id, Role::User, 10..18),
3284 ]
3285 );
3286
3287 let (message_6, message_7) = conversation.update(cx, |conversation, cx| {
3288 conversation.split_message(14..16, cx)
3289 });
3290 let message_6 = message_6.unwrap();
3291 let message_7 = message_7.unwrap();
3292 assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\n\nccc\ndd\nd\n");
3293 assert_eq!(
3294 messages(&conversation, cx),
3295 vec![
3296 (message_1.id, Role::User, 0..4),
3297 (message_3.id, Role::User, 4..5),
3298 (message_2.id, Role::User, 5..9),
3299 (message_4.id, Role::User, 9..10),
3300 (message_5.id, Role::User, 10..14),
3301 (message_6.id, Role::User, 14..17),
3302 (message_7.id, Role::User, 17..19),
3303 ]
3304 );
3305 }
3306
3307 #[gpui::test]
3308 fn test_messages_for_offsets(cx: &mut AppContext) {
3309 cx.set_global(SettingsStore::test(cx));
3310 init(cx);
3311 let registry = Arc::new(LanguageRegistry::test());
3312 let completion_provider = Arc::new(FakeCompletionProvider::new());
3313 let conversation = cx.add_model(|cx| Conversation::new(registry, cx, completion_provider));
3314 let buffer = conversation.read(cx).buffer.clone();
3315
3316 let message_1 = conversation.read(cx).message_anchors[0].clone();
3317 assert_eq!(
3318 messages(&conversation, cx),
3319 vec![(message_1.id, Role::User, 0..0)]
3320 );
3321
3322 buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "aaa")], None, cx));
3323 let message_2 = conversation
3324 .update(cx, |conversation, cx| {
3325 conversation.insert_message_after(message_1.id, Role::User, MessageStatus::Done, cx)
3326 })
3327 .unwrap();
3328 buffer.update(cx, |buffer, cx| buffer.edit([(4..4, "bbb")], None, cx));
3329
3330 let message_3 = conversation
3331 .update(cx, |conversation, cx| {
3332 conversation.insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx)
3333 })
3334 .unwrap();
3335 buffer.update(cx, |buffer, cx| buffer.edit([(8..8, "ccc")], None, cx));
3336
3337 assert_eq!(buffer.read(cx).text(), "aaa\nbbb\nccc");
3338 assert_eq!(
3339 messages(&conversation, cx),
3340 vec![
3341 (message_1.id, Role::User, 0..4),
3342 (message_2.id, Role::User, 4..8),
3343 (message_3.id, Role::User, 8..11)
3344 ]
3345 );
3346
3347 assert_eq!(
3348 message_ids_for_offsets(&conversation, &[0, 4, 9], cx),
3349 [message_1.id, message_2.id, message_3.id]
3350 );
3351 assert_eq!(
3352 message_ids_for_offsets(&conversation, &[0, 1, 11], cx),
3353 [message_1.id, message_3.id]
3354 );
3355
3356 let message_4 = conversation
3357 .update(cx, |conversation, cx| {
3358 conversation.insert_message_after(message_3.id, Role::User, MessageStatus::Done, cx)
3359 })
3360 .unwrap();
3361 assert_eq!(buffer.read(cx).text(), "aaa\nbbb\nccc\n");
3362 assert_eq!(
3363 messages(&conversation, cx),
3364 vec![
3365 (message_1.id, Role::User, 0..4),
3366 (message_2.id, Role::User, 4..8),
3367 (message_3.id, Role::User, 8..12),
3368 (message_4.id, Role::User, 12..12)
3369 ]
3370 );
3371 assert_eq!(
3372 message_ids_for_offsets(&conversation, &[0, 4, 8, 12], cx),
3373 [message_1.id, message_2.id, message_3.id, message_4.id]
3374 );
3375
3376 fn message_ids_for_offsets(
3377 conversation: &Model<Conversation>,
3378 offsets: &[usize],
3379 cx: &AppContext,
3380 ) -> Vec<MessageId> {
3381 conversation
3382 .read(cx)
3383 .messages_for_offsets(offsets.iter().copied(), cx)
3384 .into_iter()
3385 .map(|message| message.id)
3386 .collect()
3387 }
3388 }
3389
3390 #[gpui::test]
3391 fn test_serialization(cx: &mut AppContext) {
3392 cx.set_global(SettingsStore::test(cx));
3393 init(cx);
3394 let registry = Arc::new(LanguageRegistry::test());
3395 let completion_provider = Arc::new(FakeCompletionProvider::new());
3396 let conversation =
3397 cx.add_model(|cx| Conversation::new(registry.clone(), cx, completion_provider));
3398 let buffer = conversation.read(cx).buffer.clone();
3399 let message_0 = conversation.read(cx).message_anchors[0].id;
3400 let message_1 = conversation.update(cx, |conversation, cx| {
3401 conversation
3402 .insert_message_after(message_0, Role::Assistant, MessageStatus::Done, cx)
3403 .unwrap()
3404 });
3405 let message_2 = conversation.update(cx, |conversation, cx| {
3406 conversation
3407 .insert_message_after(message_1.id, Role::System, MessageStatus::Done, cx)
3408 .unwrap()
3409 });
3410 buffer.update(cx, |buffer, cx| {
3411 buffer.edit([(0..0, "a"), (1..1, "b\nc")], None, cx);
3412 buffer.finalize_last_transaction();
3413 });
3414 let _message_3 = conversation.update(cx, |conversation, cx| {
3415 conversation
3416 .insert_message_after(message_2.id, Role::System, MessageStatus::Done, cx)
3417 .unwrap()
3418 });
3419 buffer.update(cx, |buffer, cx| buffer.undo(cx));
3420 assert_eq!(buffer.read(cx).text(), "a\nb\nc\n");
3421 assert_eq!(
3422 messages(&conversation, cx),
3423 [
3424 (message_0, Role::User, 0..2),
3425 (message_1.id, Role::Assistant, 2..6),
3426 (message_2.id, Role::System, 6..6),
3427 ]
3428 );
3429
3430 let deserialized_conversation = cx.add_model(|cx| {
3431 Conversation::deserialize(
3432 conversation.read(cx).serialize(cx),
3433 Default::default(),
3434 registry.clone(),
3435 cx,
3436 )
3437 });
3438 let deserialized_buffer = deserialized_conversation.read(cx).buffer.clone();
3439 assert_eq!(deserialized_buffer.read(cx).text(), "a\nb\nc\n");
3440 assert_eq!(
3441 messages(&deserialized_conversation, cx),
3442 [
3443 (message_0, Role::User, 0..2),
3444 (message_1.id, Role::Assistant, 2..6),
3445 (message_2.id, Role::System, 6..6),
3446 ]
3447 );
3448 }
3449
3450 fn messages(
3451 conversation: &Model<Conversation>,
3452 cx: &AppContext,
3453 ) -> Vec<(MessageId, Role, Range<usize>)> {
3454 conversation
3455 .read(cx)
3456 .messages(cx)
3457 .map(|message| (message.id, message.role, message.offset_range))
3458 .collect()
3459 }
3460}
3461
3462fn report_assistant_event(
3463 workspace: WeakView<Workspace>,
3464 conversation_id: Option<String>,
3465 assistant_kind: AssistantKind,
3466 cx: &AppContext,
3467) {
3468 let Some(workspace) = workspace.upgrade() else {
3469 return;
3470 };
3471
3472 let client = workspace.read(cx).project().read(cx).client();
3473 let telemetry = client.telemetry();
3474
3475 let model = AssistantSettings::get_global(cx)
3476 .default_open_ai_model
3477 .clone();
3478
3479 let telemetry_settings = TelemetrySettings::get_global(cx);
3480
3481 telemetry.report_assistant_event(
3482 telemetry_settings,
3483 conversation_id,
3484 assistant_kind,
3485 model.full_name(),
3486 )
3487}