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