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