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