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