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