2694 lines · 112.8 KB · Rust
RawDownload
use crate::AssistantPanel;use crate::context::{AssistantContext, ContextId};use crate::context_picker::MentionLink;use crate::thread::{    LastRestoreCheckpoint, MessageId, MessageSegment, RequestKind, Thread, ThreadError,    ThreadEvent, ThreadFeedback,};use crate::thread_store::ThreadStore;use crate::tool_use::{PendingToolUseStatus, ToolUse, ToolUseStatus};use crate::ui::{AddedContext, AgentNotification, AgentNotificationEvent, ContextPill};use anyhow::Context as _;use assistant_settings::{AssistantSettings, NotifyWhenAgentWaiting};use collections::{HashMap, HashSet};use editor::scroll::Autoscroll;use editor::{Editor, MultiBuffer};use gpui::{    AbsoluteLength, Animation, AnimationExt, AnyElement, App, ClickEvent, ClipboardItem,    DefiniteLength, EdgesRefinement, Empty, Entity, Focusable, Hsla, ListAlignment, ListState,    MouseButton, PlatformDisplay, ScrollHandle, Stateful, StyleRefinement, Subscription, Task,    TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, WindowHandle,    linear_color_stop, linear_gradient, list, percentage, pulsating_between,};use language::{Buffer, LanguageRegistry};use language_model::{ConfiguredModel, LanguageModelRegistry, LanguageModelToolUseId, Role};use markdown::parser::CodeBlockKind;use markdown::{Markdown, MarkdownElement, MarkdownStyle, ParsedMarkdown, without_fences};use project::ProjectItem as _;use settings::{Settings as _, update_settings_file};use std::ops::Range;use std::path::Path;use std::rc::Rc;use std::sync::Arc;use std::time::Duration;use text::ToPoint;use theme::ThemeSettings;use ui::{Disclosure, IconButton, KeyBinding, Scrollbar, ScrollbarState, Tooltip, prelude::*};use util::ResultExt as _;use workspace::{OpenOptions, Workspace}; use crate::context_store::ContextStore; pub struct ActiveThread {    language_registry: Arc<LanguageRegistry>,    thread_store: Entity<ThreadStore>,    thread: Entity<Thread>,    context_store: Entity<ContextStore>,    workspace: WeakEntity<Workspace>,    save_thread_task: Option<Task<()>>,    messages: Vec<MessageId>,    list_state: ListState,    scrollbar_state: ScrollbarState,    show_scrollbar: bool,    hide_scrollbar_task: Option<Task<()>>,    rendered_messages_by_id: HashMap<MessageId, RenderedMessage>,    rendered_tool_uses: HashMap<LanguageModelToolUseId, RenderedToolUse>,    editing_message: Option<(MessageId, EditMessageState)>,    expanded_tool_uses: HashMap<LanguageModelToolUseId, bool>,    expanded_thinking_segments: HashMap<(MessageId, usize), bool>,    last_error: Option<ThreadError>,    notifications: Vec<WindowHandle<AgentNotification>>,    copied_code_block_ids: HashSet<usize>,    _subscriptions: Vec<Subscription>,    notification_subscriptions: HashMap<WindowHandle<AgentNotification>, Vec<Subscription>>,    feedback_message_editor: Option<Entity<Editor>>,} struct RenderedMessage {    language_registry: Arc<LanguageRegistry>,    segments: Vec<RenderedMessageSegment>,} #[derive(Clone)]struct RenderedToolUse {    label: Entity<Markdown>,    input: Entity<Markdown>,    output: Entity<Markdown>,} impl RenderedMessage {    fn from_segments(        segments: &[MessageSegment],        language_registry: Arc<LanguageRegistry>,        cx: &mut App,    ) -> Self {        let mut this = Self {            language_registry,            segments: Vec::with_capacity(segments.len()),        };        for segment in segments {            this.push_segment(segment, cx);        }        this    }     fn append_thinking(&mut self, text: &String, cx: &mut App) {        if let Some(RenderedMessageSegment::Thinking {            content,            scroll_handle,        }) = self.segments.last_mut()        {            content.update(cx, |markdown, cx| {                markdown.append(text, cx);            });            scroll_handle.scroll_to_bottom();        } else {            self.segments.push(RenderedMessageSegment::Thinking {                content: parse_markdown(text.into(), self.language_registry.clone(), cx),                scroll_handle: ScrollHandle::default(),            });        }    }     fn append_text(&mut self, text: &String, cx: &mut App) {        if let Some(RenderedMessageSegment::Text(markdown)) = self.segments.last_mut() {            markdown.update(cx, |markdown, cx| markdown.append(text, cx));        } else {            self.segments                .push(RenderedMessageSegment::Text(parse_markdown(                    SharedString::from(text),                    self.language_registry.clone(),                    cx,                )));        }    }     fn push_segment(&mut self, segment: &MessageSegment, cx: &mut App) {        let rendered_segment = match segment {            MessageSegment::Thinking(text) => RenderedMessageSegment::Thinking {                content: parse_markdown(text.into(), self.language_registry.clone(), cx),                scroll_handle: ScrollHandle::default(),            },            MessageSegment::Text(text) => RenderedMessageSegment::Text(parse_markdown(                text.into(),                self.language_registry.clone(),                cx,            )),        };        self.segments.push(rendered_segment);    }} enum RenderedMessageSegment {    Thinking {        content: Entity<Markdown>,        scroll_handle: ScrollHandle,    },    Text(Entity<Markdown>),} fn parse_markdown(    text: SharedString,    language_registry: Arc<LanguageRegistry>,    cx: &mut App,) -> Entity<Markdown> {    cx.new(|cx| Markdown::new(text, Some(language_registry), None, cx))} fn default_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {    let theme_settings = ThemeSettings::get_global(cx);    let colors = cx.theme().colors();    let ui_font_size = TextSize::Default.rems(cx);    let buffer_font_size = TextSize::Small.rems(cx);    let mut text_style = window.text_style();     text_style.refine(&TextStyleRefinement {        font_family: Some(theme_settings.ui_font.family.clone()),        font_fallbacks: theme_settings.ui_font.fallbacks.clone(),        font_features: Some(theme_settings.ui_font.features.clone()),        font_size: Some(ui_font_size.into()),        color: Some(cx.theme().colors().text),        ..Default::default()    });     MarkdownStyle {        base_text_style: text_style,        syntax: cx.theme().syntax().clone(),        selection_background_color: cx.theme().players().local().selection,        code_block_overflow_x_scroll: true,        table_overflow_x_scroll: true,        code_block: StyleRefinement {            padding: EdgesRefinement {                top: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),                left: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),                right: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),                bottom: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),            },            background: Some(colors.editor_background.into()),            text: Some(TextStyleRefinement {                font_family: Some(theme_settings.buffer_font.family.clone()),                font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),                font_features: Some(theme_settings.buffer_font.features.clone()),                font_size: Some(buffer_font_size.into()),                ..Default::default()            }),            ..Default::default()        },        inline_code: TextStyleRefinement {            font_family: Some(theme_settings.buffer_font.family.clone()),            font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),            font_features: Some(theme_settings.buffer_font.features.clone()),            font_size: Some(buffer_font_size.into()),            background_color: Some(colors.editor_foreground.opacity(0.08)),            ..Default::default()        },        link: TextStyleRefinement {            background_color: Some(colors.editor_foreground.opacity(0.025)),            underline: Some(UnderlineStyle {                color: Some(colors.text_accent.opacity(0.5)),                thickness: px(1.),                ..Default::default()            }),            ..Default::default()        },        link_callback: Some(Rc::new(move |url, cx| {            if MentionLink::is_valid(url) {                let colors = cx.theme().colors();                Some(TextStyleRefinement {                    background_color: Some(colors.element_background),                    ..Default::default()                })            } else {                None            }        })),        ..Default::default()    }} fn render_tool_use_markdown(    text: SharedString,    language_registry: Arc<LanguageRegistry>,    cx: &mut App,) -> Entity<Markdown> {    cx.new(|cx| Markdown::new(text, Some(language_registry), None, cx))} fn tool_use_markdown_style(window: &Window, cx: &mut App) -> MarkdownStyle {    let theme_settings = ThemeSettings::get_global(cx);    let colors = cx.theme().colors();    let ui_font_size = TextSize::Default.rems(cx);    let buffer_font_size = TextSize::Small.rems(cx);    let mut text_style = window.text_style();     text_style.refine(&TextStyleRefinement {        font_family: Some(theme_settings.ui_font.family.clone()),        font_fallbacks: theme_settings.ui_font.fallbacks.clone(),        font_features: Some(theme_settings.ui_font.features.clone()),        font_size: Some(ui_font_size.into()),        color: Some(cx.theme().colors().text),        ..Default::default()    });     MarkdownStyle {        base_text_style: text_style,        syntax: cx.theme().syntax().clone(),        selection_background_color: cx.theme().players().local().selection,        code_block_overflow_x_scroll: true,        code_block: StyleRefinement {            margin: EdgesRefinement::default(),            padding: EdgesRefinement::default(),            background: Some(colors.editor_background.into()),            border_color: None,            border_widths: EdgesRefinement::default(),            text: Some(TextStyleRefinement {                font_family: Some(theme_settings.buffer_font.family.clone()),                font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),                font_features: Some(theme_settings.buffer_font.features.clone()),                font_size: Some(buffer_font_size.into()),                ..Default::default()            }),            ..Default::default()        },        inline_code: TextStyleRefinement {            font_family: Some(theme_settings.buffer_font.family.clone()),            font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),            font_features: Some(theme_settings.buffer_font.features.clone()),            font_size: Some(TextSize::XSmall.rems(cx).into()),            ..Default::default()        },        heading: StyleRefinement {            text: Some(TextStyleRefinement {                font_size: Some(ui_font_size.into()),                ..Default::default()            }),            ..Default::default()        },        ..Default::default()    }} fn render_markdown_code_block(    id: usize,    kind: &CodeBlockKind,    parsed_markdown: &ParsedMarkdown,    codeblock_range: Range<usize>,    active_thread: Entity<ActiveThread>,    workspace: WeakEntity<Workspace>,    _window: &mut Window,    cx: &App,) -> Div {    let label = match kind {        CodeBlockKind::Indented => None,        CodeBlockKind::Fenced => Some(            h_flex()                .gap_1()                .child(                    Icon::new(IconName::Code)                        .color(Color::Muted)                        .size(IconSize::XSmall),                )                .child(Label::new("untitled").size(LabelSize::Small))                .into_any_element(),        ),        CodeBlockKind::FencedLang(raw_language_name) => Some(            h_flex()                .gap_1()                .children(                    parsed_markdown                        .languages_by_name                        .get(raw_language_name)                        .and_then(|language| {                            language                                .config()                                .matcher                                .path_suffixes                                .iter()                                .find_map(|extension| {                                    file_icons::FileIcons::get_icon(Path::new(extension), cx)                                })                                .map(Icon::from_path)                                .map(|icon| icon.color(Color::Muted).size(IconSize::Small))                        }),                )                .child(                    Label::new(                        parsed_markdown                            .languages_by_name                            .get(raw_language_name)                            .map(|language| language.name().into())                            .clone()                            .unwrap_or_else(|| raw_language_name.clone()),                    )                    .size(LabelSize::Small),                )                .into_any_element(),        ),        CodeBlockKind::FencedSrc(path_range) => path_range.path.file_name().map(|file_name| {            let content = if let Some(parent) = path_range.path.parent() {                h_flex()                    .ml_1()                    .gap_1()                    .child(                        Label::new(file_name.to_string_lossy().to_string()).size(LabelSize::Small),                    )                    .child(                        Label::new(parent.to_string_lossy().to_string())                            .color(Color::Muted)                            .size(LabelSize::Small),                    )                    .into_any_element()            } else {                Label::new(path_range.path.to_string_lossy().to_string())                    .size(LabelSize::Small)                    .ml_1()                    .into_any_element()            };             h_flex()                .id(("code-block-header-label", id))                .w_full()                .max_w_full()                .px_1()                .gap_0p5()                .cursor_pointer()                .rounded_sm()                .hover(|item| item.bg(cx.theme().colors().element_hover.opacity(0.5)))                .tooltip(Tooltip::text("Jump to file"))                .children(                    file_icons::FileIcons::get_icon(&path_range.path, cx)                        .map(Icon::from_path)                        .map(|icon| icon.color(Color::Muted).size(IconSize::XSmall)),                )                .child(content)                .child(                    Icon::new(IconName::ArrowUpRight)                        .size(IconSize::XSmall)                        .color(Color::Ignored),                )                .on_click({                    let path_range = path_range.clone();                    move |_, window, cx| {                        workspace                            .update(cx, {                                |workspace, cx| {                                    if let Some(project_path) = workspace                                        .project()                                        .read(cx)                                        .find_project_path(&path_range.path, cx)                                    {                                        workspace                                            .open_path(project_path, None, true, window, cx)                                            .detach_and_log_err(cx);                                    }                                }                            })                            .ok();                    }                })                .into_any_element()        }),    };     let codeblock_header_bg = cx        .theme()        .colors()        .element_background        .blend(cx.theme().colors().editor_foreground.opacity(0.01));     let codeblock_was_copied = active_thread.read(cx).copied_code_block_ids.contains(&id);     let codeblock_header = h_flex()        .p_1()        .gap_1()        .justify_between()        .border_b_1()        .border_color(cx.theme().colors().border_variant)        .bg(codeblock_header_bg)        .rounded_t_md()        .children(label)        .child(            IconButton::new(                ("copy-markdown-code", id),                if codeblock_was_copied {                    IconName::Check                } else {                    IconName::Copy                },            )            .icon_color(Color::Muted)            .shape(ui::IconButtonShape::Square)            .tooltip(Tooltip::text("Copy Code"))            .visible_on_hover("markdown-code-block")            .on_click({                let active_thread = active_thread.clone();                let parsed_markdown = parsed_markdown.clone();                move |_event, _window, cx| {                    active_thread.update(cx, |this, cx| {                        this.copied_code_block_ids.insert(id);                         let code =                            without_fences(&parsed_markdown.source()[codeblock_range.clone()])                                .to_string();                         cx.write_to_clipboard(ClipboardItem::new_string(code.clone()));                         cx.spawn(async move |this, cx| {                            cx.background_executor().timer(Duration::from_secs(2)).await;                             cx.update(|cx| {                                this.update(cx, |this, cx| {                                    this.copied_code_block_ids.remove(&id);                                    cx.notify();                                })                            })                            .ok();                        })                        .detach();                    });                }            }),        );     v_flex()        .mb_2()        .relative()        .overflow_hidden()        .rounded_lg()        .border_1()        .border_color(cx.theme().colors().border_variant)        .child(codeblock_header)} fn open_markdown_link(    text: SharedString,    workspace: WeakEntity<Workspace>,    window: &mut Window,    cx: &mut App,) {    let Some(workspace) = workspace.upgrade() else {        cx.open_url(&text);        return;    };     match MentionLink::try_parse(&text, &workspace, cx) {        Some(MentionLink::File(path, entry)) => workspace.update(cx, |workspace, cx| {            if entry.is_dir() {                workspace.project().update(cx, |_, cx| {                    cx.emit(project::Event::RevealInProjectPanel(entry.id));                })            } else {

Lines 1–500 of 2694.