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