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