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