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