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