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