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