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