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