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