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