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