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