Compare commits

...

12 commits

Author SHA1 Message Date
Danilo Leal
f6f7762f32 ai onboarding: Add overall fixes to the whole flow (#34996)
Closes https://github.com/zed-industries/zed/issues/34979

Release Notes:

- N/A

---------

Co-authored-by: Agus Zubiaga <hi@aguz.me>
Co-authored-by: Ben Kunkle <Ben.kunkle@gmail.com>
2025-07-24 11:20:26 -04:00
Oleksiy Syvokon
c015ef64dc
linux: Fix ctrl-0..9, ctrl-[, ctrl-^ (#35028)
There were two different underlying reasons for the issues with
ctrl-number and ctrl-punctuation:

1. Some keys in the ctrl-0..9 range send codes in the `\1b`..`\1f`
range. For example, `ctrl-2` sends keycode for `ctrl-[` (0x1b), but we
want to map it to `2`, not to `[`.

2. `ctrl-[` and four other ctrl-punctuation were incorrectly mapped,
since the expected conversion is by adding 0x40

Closes #35012

Release Notes:

- N/A
2025-07-24 09:45:57 -04:00
Joseph T. Lyons
d3b2f604a9 Differentiate between file and selection diff events (#35014)
Release Notes:

- N/A
2025-07-24 04:43:56 -04:00
Joseph T. Lyons
b8849d83e6 Fix some bugs with editor: diff clipboard with selection (#34999)
Improves testing around `editor: diff clipboard with selection` as well.

Release Notes:

- Fixed some bugs with `editor: diff clipboard with selection`
2025-07-24 02:53:01 -04:00
versecafe
77dda2eca8
ollama: Add Magistral to Ollama (#35000)
See also: #34983

Release Notes:

- Added magistral support to ollama
2025-07-24 00:19:11 -04:00
Peter Tripp
ece9dd2c43
mistral: Add support for magistral-small and magistral-medium (#34983)
Release Notes:

- mistral: Added support for magistral-small and magistral-medium
2025-07-23 23:14:37 -04:00
Renato Lochetti
c60f37a044
mistral: Add support for Mistral Devstral Medium (#34888)
Mistral released their new DevstralMedium model to be used via API:
https://mistral.ai/news/devstral-2507

Release Notes:

- Add support for Mistral Devstral Medium
2025-07-23 23:13:16 -04:00
Joseph T. Lyons
ca646e2951 zed 0.197.1 2025-07-23 17:26:52 -04:00
Umesh Yadav
b5433a9a54 agent_ui: Show keybindings for NewThread and NewTextThread in new thread button (#34967)
I believe in this PR: #34829 we moved to context menu entry from action
but the side effect of that was we also removed the Keybindings from
showing it in the new thread button dropdown. This PR fixes that. cc
@danilo-leal

| Before | After |
|--------|--------|
| <img width="900" height="1962" alt="CleanShot 2025-07-23 at 23 36
28@2x"
src="https://github.com/user-attachments/assets/760cbe75-09b9-404b-9d33-1db73785234f"
/> | <img width="850" height="1964" alt="CleanShot 2025-07-23 at 23 37
17@2x"
src="https://github.com/user-attachments/assets/24a7e871-aebc-475c-845f-b76f02527b8f"
/> |

Release Notes:

- N/A
2025-07-23 17:19:04 -04:00
gcp-cherry-pick-bot[bot]
4727ae35d2
Fix telemetry event type names (cherry-pick #34974) (#34975)
Cherry-picked Fix telemetry event type names (#34974)

Release Notes:

- N/A

Co-authored-by: Joseph T. Lyons <JosephTLyons@gmail.com>
2025-07-23 17:09:42 -04:00
Danilo Leal
d61db1fae7 agent: Fix follow button disabled state (#34978)
Release Notes:

- N/A
2025-07-23 17:09:15 -04:00
Joseph T. Lyons
45d211a555 v0.197.x preview 2025-07-23 13:48:22 -04:00
20 changed files with 549 additions and 416 deletions

2
Cargo.lock generated
View file

@ -20170,7 +20170,7 @@ dependencies = [
[[package]] [[package]]
name = "zed" name = "zed"
version = "0.197.0" version = "0.197.1"
dependencies = [ dependencies = [
"activity_indicator", "activity_indicator",
"agent", "agent",

View file

@ -41,6 +41,9 @@ use std::{
}; };
use util::ResultExt as _; use util::ResultExt as _;
pub static ZED_STATELESS: std::sync::LazyLock<bool> =
std::sync::LazyLock::new(|| std::env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty()));
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum DataType { pub enum DataType {
#[serde(rename = "json")] #[serde(rename = "json")]
@ -874,7 +877,11 @@ impl ThreadsDatabase {
let needs_migration_from_heed = mdb_path.exists(); let needs_migration_from_heed = mdb_path.exists();
let connection = Connection::open_file(&sqlite_path.to_string_lossy()); let connection = if *ZED_STATELESS {
Connection::open_memory(Some("THREAD_FALLBACK_DB"))
} else {
Connection::open_file(&sqlite_path.to_string_lossy())
};
connection.exec(indoc! {" connection.exec(indoc! {"
CREATE TABLE IF NOT EXISTS threads ( CREATE TABLE IF NOT EXISTS threads (

View file

@ -185,6 +185,13 @@ impl AgentConfiguration {
None None
}; };
let is_signed_in = self
.workspace
.read_with(cx, |workspace, _| {
workspace.client().status().borrow().is_connected()
})
.unwrap_or(false);
v_flex() v_flex()
.when(is_expanded, |this| this.mb_2()) .when(is_expanded, |this| this.mb_2())
.child( .child(
@ -230,8 +237,8 @@ impl AgentConfiguration {
.size(LabelSize::Large), .size(LabelSize::Large),
) )
.map(|this| { .map(|this| {
if is_zed_provider { if is_zed_provider && is_signed_in {
this.gap_2().child( this.child(
self.render_zed_plan_info(current_plan, cx), self.render_zed_plan_info(current_plan, cx),
) )
} else { } else {

View file

@ -564,6 +564,17 @@ impl AgentPanel {
let inline_assist_context_store = let inline_assist_context_store =
cx.new(|_cx| ContextStore::new(project.downgrade(), Some(thread_store.downgrade()))); cx.new(|_cx| ContextStore::new(project.downgrade(), Some(thread_store.downgrade())));
let thread_id = thread.read(cx).id().clone();
let history_store = cx.new(|cx| {
HistoryStore::new(
thread_store.clone(),
context_store.clone(),
[HistoryEntryId::Thread(thread_id)],
cx,
)
});
let message_editor = cx.new(|cx| { let message_editor = cx.new(|cx| {
MessageEditor::new( MessageEditor::new(
fs.clone(), fs.clone(),
@ -573,22 +584,13 @@ impl AgentPanel {
prompt_store.clone(), prompt_store.clone(),
thread_store.downgrade(), thread_store.downgrade(),
context_store.downgrade(), context_store.downgrade(),
Some(history_store.downgrade()),
thread.clone(), thread.clone(),
window, window,
cx, cx,
) )
}); });
let thread_id = thread.read(cx).id().clone();
let history_store = cx.new(|cx| {
HistoryStore::new(
thread_store.clone(),
context_store.clone(),
[HistoryEntryId::Thread(thread_id)],
cx,
)
});
cx.observe(&history_store, |_, _, cx| cx.notify()).detach(); cx.observe(&history_store, |_, _, cx| cx.notify()).detach();
let active_thread = cx.new(|cx| { let active_thread = cx.new(|cx| {
@ -851,6 +853,7 @@ impl AgentPanel {
self.prompt_store.clone(), self.prompt_store.clone(),
self.thread_store.downgrade(), self.thread_store.downgrade(),
self.context_store.downgrade(), self.context_store.downgrade(),
Some(self.history_store.downgrade()),
thread.clone(), thread.clone(),
window, window,
cx, cx,
@ -1124,6 +1127,7 @@ impl AgentPanel {
self.prompt_store.clone(), self.prompt_store.clone(),
self.thread_store.downgrade(), self.thread_store.downgrade(),
self.context_store.downgrade(), self.context_store.downgrade(),
Some(self.history_store.downgrade()),
thread.clone(), thread.clone(),
window, window,
cx, cx,
@ -1901,85 +1905,96 @@ impl AgentPanel {
) )
.anchor(Corner::TopRight) .anchor(Corner::TopRight)
.with_handle(self.new_thread_menu_handle.clone()) .with_handle(self.new_thread_menu_handle.clone())
.menu(move |window, cx| { .menu({
let active_thread = active_thread.clone(); let focus_handle = focus_handle.clone();
Some(ContextMenu::build(window, cx, |mut menu, _window, cx| { move |window, cx| {
menu = menu let active_thread = active_thread.clone();
.when(cx.has_flag::<feature_flags::AcpFeatureFlag>(), |this| { Some(ContextMenu::build(window, cx, |mut menu, _window, cx| {
this.header("Zed Agent") menu = menu
}) .context(focus_handle.clone())
.item( .when(cx.has_flag::<feature_flags::AcpFeatureFlag>(), |this| {
ContextMenuEntry::new("New Thread") this.header("Zed Agent")
.icon(IconName::NewThread) })
.icon_color(Color::Muted) .item(
.handler(move |window, cx| { ContextMenuEntry::new("New Thread")
window.dispatch_action(NewThread::default().boxed_clone(), cx); .icon(IconName::NewThread)
}), .icon_color(Color::Muted)
) .action(NewThread::default().boxed_clone())
.item( .handler(move |window, cx| {
ContextMenuEntry::new("New Text Thread") window.dispatch_action(
.icon(IconName::NewTextThread) NewThread::default().boxed_clone(),
.icon_color(Color::Muted) cx,
.handler(move |window, cx| { );
window.dispatch_action(NewTextThread.boxed_clone(), cx); }),
}), )
) .item(
.when_some(active_thread, |this, active_thread| { ContextMenuEntry::new("New Text Thread")
let thread = active_thread.read(cx); .icon(IconName::NewTextThread)
.icon_color(Color::Muted)
.action(NewTextThread.boxed_clone())
.handler(move |window, cx| {
window.dispatch_action(NewTextThread.boxed_clone(), cx);
}),
)
.when_some(active_thread, |this, active_thread| {
let thread = active_thread.read(cx);
if !thread.is_empty() { if !thread.is_empty() {
let thread_id = thread.id().clone(); let thread_id = thread.id().clone();
this.item( this.item(
ContextMenuEntry::new("New From Summary") ContextMenuEntry::new("New From Summary")
.icon(IconName::NewFromSummary) .icon(IconName::NewFromSummary)
.icon_color(Color::Muted) .icon_color(Color::Muted)
.handler(move |window, cx| { .handler(move |window, cx| {
window.dispatch_action( window.dispatch_action(
Box::new(NewThread { Box::new(NewThread {
from_thread_id: Some(thread_id.clone()), from_thread_id: Some(thread_id.clone()),
}), }),
cx, cx,
); );
}), }),
) )
} else { } else {
this this
} }
}) })
.when(cx.has_flag::<feature_flags::AcpFeatureFlag>(), |this| { .when(cx.has_flag::<feature_flags::AcpFeatureFlag>(), |this| {
this.separator() this.separator()
.header("External Agents") .header("External Agents")
.item( .item(
ContextMenuEntry::new("New Gemini Thread") ContextMenuEntry::new("New Gemini Thread")
.icon(IconName::AiGemini) .icon(IconName::AiGemini)
.icon_color(Color::Muted) .icon_color(Color::Muted)
.handler(move |window, cx| { .handler(move |window, cx| {
window.dispatch_action( window.dispatch_action(
NewExternalAgentThread { NewExternalAgentThread {
agent: Some(crate::ExternalAgent::Gemini), agent: Some(crate::ExternalAgent::Gemini),
} }
.boxed_clone(), .boxed_clone(),
cx, cx,
); );
}), }),
) )
.item( .item(
ContextMenuEntry::new("New Claude Code Thread") ContextMenuEntry::new("New Claude Code Thread")
.icon(IconName::AiClaude) .icon(IconName::AiClaude)
.icon_color(Color::Muted) .icon_color(Color::Muted)
.handler(move |window, cx| { .handler(move |window, cx| {
window.dispatch_action( window.dispatch_action(
NewExternalAgentThread { NewExternalAgentThread {
agent: Some(crate::ExternalAgent::ClaudeCode), agent: Some(
} crate::ExternalAgent::ClaudeCode,
.boxed_clone(), ),
cx, }
); .boxed_clone(),
}), cx,
) );
}); }),
menu )
})) });
menu
}))
}
}); });
let agent_panel_menu = PopoverMenu::new("agent-options-menu") let agent_panel_menu = PopoverMenu::new("agent-options-menu")
@ -2272,20 +2287,21 @@ impl AgentPanel {
} }
match &self.active_view { match &self.active_view {
ActiveView::Thread { thread, .. } => thread ActiveView::Thread { .. } | ActiveView::TextThread { .. } => {
.read(cx) let history_is_empty = self
.thread() .history_store
.read(cx) .update(cx, |store, cx| store.recent_entries(1, cx).is_empty());
.configured_model()
.map_or(true, |model| { let has_configured_non_zed_providers = LanguageModelRegistry::read_global(cx)
model.provider.id() == language_model::ZED_CLOUD_PROVIDER_ID .providers()
}), .iter()
ActiveView::TextThread { .. } => LanguageModelRegistry::global(cx) .any(|provider| {
.read(cx) provider.is_authenticated(cx)
.default_model() && provider.id() != language_model::ZED_CLOUD_PROVIDER_ID
.map_or(true, |model| { });
model.provider.id() == language_model::ZED_CLOUD_PROVIDER_ID
}), history_is_empty || !has_configured_non_zed_providers
}
ActiveView::ExternalAgentThread { .. } ActiveView::ExternalAgentThread { .. }
| ActiveView::History | ActiveView::History
| ActiveView::Configuration => false, | ActiveView::Configuration => false,
@ -2306,9 +2322,8 @@ impl AgentPanel {
Some( Some(
div() div()
.size_full()
.when(thread_view, |this| { .when(thread_view, |this| {
this.bg(cx.theme().colors().panel_background) this.size_full().bg(cx.theme().colors().panel_background)
}) })
.when(text_thread_view, |this| { .when(text_thread_view, |this| {
this.bg(cx.theme().colors().editor_background) this.bg(cx.theme().colors().editor_background)

View file

@ -9,6 +9,7 @@ use crate::ui::{
MaxModeTooltip, MaxModeTooltip,
preview::{AgentPreview, UsageCallout}, preview::{AgentPreview, UsageCallout},
}; };
use agent::history_store::HistoryStore;
use agent::{ use agent::{
context::{AgentContextKey, ContextLoadResult, load_context}, context::{AgentContextKey, ContextLoadResult, load_context},
context_store::ContextStoreEvent, context_store::ContextStoreEvent,
@ -29,8 +30,9 @@ use fs::Fs;
use futures::future::Shared; use futures::future::Shared;
use futures::{FutureExt as _, future}; use futures::{FutureExt as _, future};
use gpui::{ use gpui::{
Animation, AnimationExt, App, Entity, EventEmitter, Focusable, KeyContext, Subscription, Task, Animation, AnimationExt, App, Entity, EventEmitter, Focusable, IntoElement, KeyContext,
TextStyle, WeakEntity, linear_color_stop, linear_gradient, point, pulsating_between, Subscription, Task, TextStyle, WeakEntity, linear_color_stop, linear_gradient, point,
pulsating_between,
}; };
use language::{Buffer, Language, Point}; use language::{Buffer, Language, Point};
use language_model::{ use language_model::{
@ -80,6 +82,7 @@ pub struct MessageEditor {
user_store: Entity<UserStore>, user_store: Entity<UserStore>,
context_store: Entity<ContextStore>, context_store: Entity<ContextStore>,
prompt_store: Option<Entity<PromptStore>>, prompt_store: Option<Entity<PromptStore>>,
history_store: Option<WeakEntity<HistoryStore>>,
context_strip: Entity<ContextStrip>, context_strip: Entity<ContextStrip>,
context_picker_menu_handle: PopoverMenuHandle<ContextPicker>, context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
model_selector: Entity<AgentModelSelector>, model_selector: Entity<AgentModelSelector>,
@ -161,6 +164,7 @@ impl MessageEditor {
prompt_store: Option<Entity<PromptStore>>, prompt_store: Option<Entity<PromptStore>>,
thread_store: WeakEntity<ThreadStore>, thread_store: WeakEntity<ThreadStore>,
text_thread_store: WeakEntity<TextThreadStore>, text_thread_store: WeakEntity<TextThreadStore>,
history_store: Option<WeakEntity<HistoryStore>>,
thread: Entity<Thread>, thread: Entity<Thread>,
window: &mut Window, window: &mut Window,
cx: &mut Context<Self>, cx: &mut Context<Self>,
@ -233,6 +237,7 @@ impl MessageEditor {
workspace, workspace,
context_store, context_store,
prompt_store, prompt_store,
history_store,
context_strip, context_strip,
context_picker_menu_handle, context_picker_menu_handle,
load_context_task: None, load_context_task: None,
@ -625,7 +630,7 @@ impl MessageEditor {
.unwrap_or(false); .unwrap_or(false);
IconButton::new("follow-agent", IconName::Crosshair) IconButton::new("follow-agent", IconName::Crosshair)
.disabled(is_model_selected) .disabled(!is_model_selected)
.icon_size(IconSize::Small) .icon_size(IconSize::Small)
.icon_color(Color::Muted) .icon_color(Color::Muted)
.toggle_state(following) .toggle_state(following)
@ -1661,32 +1666,36 @@ impl Render for MessageEditor {
let line_height = TextSize::Small.rems(cx).to_pixels(window.rem_size()) * 1.5; let line_height = TextSize::Small.rems(cx).to_pixels(window.rem_size()) * 1.5;
let in_pro_trial = matches!( let has_configured_providers = LanguageModelRegistry::read_global(cx)
self.user_store.read(cx).current_plan(), .providers()
Some(proto::Plan::ZedProTrial) .iter()
); .filter(|provider| {
provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID
})
.count()
> 0;
let pro_user = matches!( let is_signed_out = self
self.user_store.read(cx).current_plan(), .workspace
Some(proto::Plan::ZedPro) .read_with(cx, |workspace, _| {
); workspace.client().status().borrow().is_signed_out()
})
.unwrap_or(true);
let configured_providers: Vec<(IconName, SharedString)> = let has_history = self
LanguageModelRegistry::read_global(cx) .history_store
.providers() .as_ref()
.iter() .and_then(|hs| hs.update(cx, |hs, cx| hs.entries(cx).len() > 0).ok())
.filter(|provider| { .unwrap_or(false)
provider.is_authenticated(cx) && provider.id() != ZED_CLOUD_PROVIDER_ID || self
}) .thread
.map(|provider| (provider.icon(), provider.name().0.clone())) .read_with(cx, |thread, _| thread.messages().len() > 0);
.collect();
let has_existing_providers = configured_providers.len() > 0;
v_flex() v_flex()
.size_full() .size_full()
.bg(cx.theme().colors().panel_background) .bg(cx.theme().colors().panel_background)
.when( .when(
has_existing_providers && !in_pro_trial && !pro_user, !has_history && is_signed_out && has_configured_providers,
|this| this.child(cx.new(ApiKeysWithProviders::new)), |this| this.child(cx.new(ApiKeysWithProviders::new)),
) )
.when(changed_buffers.len() > 0, |parent| { .when(changed_buffers.len() > 0, |parent| {
@ -1778,6 +1787,7 @@ impl AgentPreview for MessageEditor {
None, None,
thread_store.downgrade(), thread_store.downgrade(),
text_thread_store.downgrade(), text_thread_store.downgrade(),
None,
thread, thread,
window, window,
cx, cx,

View file

@ -5,7 +5,6 @@ mod end_trial_upsell;
mod new_thread_button; mod new_thread_button;
mod onboarding_modal; mod onboarding_modal;
pub mod preview; pub mod preview;
mod upsell;
pub use agent_notification::*; pub use agent_notification::*;
pub use burn_mode_tooltip::*; pub use burn_mode_tooltip::*;

View file

@ -3,7 +3,7 @@ use std::sync::Arc;
use ai_onboarding::{AgentPanelOnboardingCard, BulletItem}; use ai_onboarding::{AgentPanelOnboardingCard, BulletItem};
use client::zed_urls; use client::zed_urls;
use gpui::{AnyElement, App, IntoElement, RenderOnce, Window}; use gpui::{AnyElement, App, IntoElement, RenderOnce, Window};
use ui::{Divider, List, prelude::*}; use ui::{Divider, List, Tooltip, prelude::*};
#[derive(IntoElement, RegisterComponent)] #[derive(IntoElement, RegisterComponent)]
pub struct EndTrialUpsell { pub struct EndTrialUpsell {
@ -33,14 +33,19 @@ impl RenderOnce for EndTrialUpsell {
) )
.child( .child(
List::new() List::new()
.child(BulletItem::new("500 prompts per month with Claude models")) .child(BulletItem::new("500 prompts with Claude models"))
.child(BulletItem::new("Unlimited edit predictions")), .child(BulletItem::new(
"Unlimited edit predictions with Zeta, our open-source model",
)),
) )
.child( .child(
Button::new("cta-button", "Upgrade to Zed Pro") Button::new("cta-button", "Upgrade to Zed Pro")
.full_width() .full_width()
.style(ButtonStyle::Tinted(ui::TintColor::Accent)) .style(ButtonStyle::Tinted(ui::TintColor::Accent))
.on_click(|_, _, cx| cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx))), .on_click(move |_, _window, cx| {
telemetry::event!("Upgrade To Pro Clicked", state = "end-of-trial");
cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx))
}),
); );
let free_section = v_flex() let free_section = v_flex()
@ -55,37 +60,43 @@ impl RenderOnce for EndTrialUpsell {
.color(Color::Muted) .color(Color::Muted)
.buffer_font(cx), .buffer_font(cx),
) )
.child(
Label::new("(Current Plan)")
.size(LabelSize::Small)
.color(Color::Custom(cx.theme().colors().text_muted.opacity(0.6)))
.buffer_font(cx),
)
.child(Divider::horizontal()), .child(Divider::horizontal()),
) )
.child( .child(
List::new() List::new()
.child(BulletItem::new( .child(BulletItem::new("50 prompts with the Claude models"))
"50 prompts per month with the Claude models", .child(BulletItem::new("2,000 accepted edit predictions")),
))
.child(BulletItem::new(
"2000 accepted edit predictions using our open-source Zeta model",
)),
)
.child(
Button::new("dismiss-button", "Stay on Free")
.full_width()
.style(ButtonStyle::Outlined)
.on_click({
let callback = self.dismiss_upsell.clone();
move |_, window, cx| callback(window, cx)
}),
); );
AgentPanelOnboardingCard::new() AgentPanelOnboardingCard::new()
.child(Headline::new("Your Zed Pro trial has expired.")) .child(Headline::new("Your Zed Pro Trial has expired"))
.child( .child(
Label::new("You've been automatically reset to the Free plan.") Label::new("You've been automatically reset to the Free plan.")
.size(LabelSize::Small)
.color(Color::Muted) .color(Color::Muted)
.mb_1(), .mb_2(),
) )
.child(pro_section) .child(pro_section)
.child(free_section) .child(free_section)
.child(
h_flex().absolute().top_4().right_4().child(
IconButton::new("dismiss_onboarding", IconName::Close)
.icon_size(IconSize::Small)
.tooltip(Tooltip::text("Dismiss"))
.on_click({
let callback = self.dismiss_upsell.clone();
move |_, window, cx| {
telemetry::event!("Banner Dismissed", source = "AI Onboarding");
callback(window, cx)
}
}),
),
)
} }
} }

View file

@ -1,163 +0,0 @@
use component::{Component, ComponentScope, single_example};
use gpui::{
AnyElement, App, ClickEvent, IntoElement, ParentElement, RenderOnce, SharedString, Styled,
Window,
};
use theme::ActiveTheme;
use ui::{
Button, ButtonCommon, ButtonStyle, Checkbox, Clickable, Color, Label, LabelCommon,
RegisterComponent, ToggleState, h_flex, v_flex,
};
/// A component that displays an upsell message with a call-to-action button
///
/// # Example
/// ```
/// let upsell = Upsell::new(
/// "Upgrade to Zed Pro",
/// "Get access to advanced AI features and more",
/// "Upgrade Now",
/// Box::new(|_, _window, cx| {
/// cx.open_url("https://zed.dev/pricing");
/// }),
/// Box::new(|_, _window, cx| {
/// // Handle dismiss
/// }),
/// Box::new(|checked, window, cx| {
/// // Handle don't show again
/// }),
/// );
/// ```
#[derive(IntoElement, RegisterComponent)]
pub struct Upsell {
title: SharedString,
message: SharedString,
cta_text: SharedString,
on_click: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
on_dismiss: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
on_dont_show_again: Box<dyn Fn(bool, &mut Window, &mut App) + 'static>,
}
impl Upsell {
/// Create a new upsell component
pub fn new(
title: impl Into<SharedString>,
message: impl Into<SharedString>,
cta_text: impl Into<SharedString>,
on_click: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
on_dismiss: Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>,
on_dont_show_again: Box<dyn Fn(bool, &mut Window, &mut App) + 'static>,
) -> Self {
Self {
title: title.into(),
message: message.into(),
cta_text: cta_text.into(),
on_click,
on_dismiss,
on_dont_show_again,
}
}
}
impl RenderOnce for Upsell {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
v_flex()
.w_full()
.p_4()
.gap_3()
.bg(cx.theme().colors().surface_background)
.rounded_md()
.border_1()
.border_color(cx.theme().colors().border)
.child(
v_flex()
.gap_1()
.child(
Label::new(self.title)
.size(ui::LabelSize::Large)
.weight(gpui::FontWeight::BOLD),
)
.child(Label::new(self.message).color(Color::Muted)),
)
.child(
h_flex()
.w_full()
.justify_between()
.items_center()
.child(
h_flex()
.items_center()
.gap_1()
.child(
Checkbox::new("dont-show-again", ToggleState::Unselected).on_click(
move |_, window, cx| {
(self.on_dont_show_again)(true, window, cx);
},
),
)
.child(
Label::new("Don't show again")
.color(Color::Muted)
.size(ui::LabelSize::Small),
),
)
.child(
h_flex()
.gap_2()
.child(
Button::new("dismiss-button", "No Thanks")
.style(ButtonStyle::Subtle)
.on_click(self.on_dismiss),
)
.child(
Button::new("cta-button", self.cta_text)
.style(ButtonStyle::Filled)
.on_click(self.on_click),
),
),
)
}
}
impl Component for Upsell {
fn scope() -> ComponentScope {
ComponentScope::Agent
}
fn name() -> &'static str {
"Upsell"
}
fn description() -> Option<&'static str> {
Some("A promotional component that displays a message with a call-to-action.")
}
fn preview(window: &mut Window, cx: &mut App) -> Option<AnyElement> {
let examples = vec![
single_example(
"Default",
Upsell::new(
"Upgrade to Zed Pro",
"Get unlimited access to AI features and more with Zed Pro. Unlock advanced AI capabilities and other premium features.",
"Upgrade Now",
Box::new(|_, _, _| {}),
Box::new(|_, _, _| {}),
Box::new(|_, _, _| {}),
).render(window, cx).into_any_element(),
),
single_example(
"Short Message",
Upsell::new(
"Try Zed Pro for free",
"Start your 7-day trial today.",
"Start Trial",
Box::new(|_, _, _| {}),
Box::new(|_, _, _| {}),
Box::new(|_, _, _| {}),
).render(window, cx).into_any_element(),
),
];
Some(v_flex().gap_4().children(examples).into_any_element())
}
}

View file

@ -61,6 +61,11 @@ impl Render for AgentPanelOnboarding {
Some(proto::Plan::ZedProTrial) Some(proto::Plan::ZedProTrial)
); );
let is_pro_user = matches!(
self.user_store.read(cx).current_plan(),
Some(proto::Plan::ZedPro)
);
AgentPanelOnboardingCard::new() AgentPanelOnboardingCard::new()
.child( .child(
ZedAiOnboarding::new( ZedAiOnboarding::new(
@ -75,7 +80,7 @@ impl Render for AgentPanelOnboarding {
}), }),
) )
.map(|this| { .map(|this| {
if enrolled_in_trial || self.configured_providers.len() >= 1 { if enrolled_in_trial || is_pro_user || self.configured_providers.len() >= 1 {
this this
} else { } else {
this.child(ApiKeysWithoutProviders::new()) this.child(ApiKeysWithoutProviders::new())

View file

@ -16,6 +16,7 @@ use client::{Client, UserStore, zed_urls};
use gpui::{AnyElement, Entity, IntoElement, ParentElement, SharedString}; use gpui::{AnyElement, Entity, IntoElement, ParentElement, SharedString};
use ui::{Divider, List, ListItem, RegisterComponent, TintColor, Tooltip, prelude::*}; use ui::{Divider, List, ListItem, RegisterComponent, TintColor, Tooltip, prelude::*};
#[derive(IntoElement)]
pub struct BulletItem { pub struct BulletItem {
label: SharedString, label: SharedString,
} }
@ -28,18 +29,27 @@ impl BulletItem {
} }
} }
impl IntoElement for BulletItem { impl RenderOnce for BulletItem {
type Element = AnyElement; fn render(self, window: &mut Window, _cx: &mut App) -> impl IntoElement {
let line_height = 0.85 * window.line_height();
fn into_element(self) -> Self::Element {
ListItem::new("list-item") ListItem::new("list-item")
.selectable(false) .selectable(false)
.start_slot( .child(
Icon::new(IconName::Dash) h_flex()
.size(IconSize::XSmall) .w_full()
.color(Color::Hidden), .min_w_0()
.gap_1()
.items_start()
.child(
h_flex().h(line_height).justify_center().child(
Icon::new(IconName::Dash)
.size(IconSize::XSmall)
.color(Color::Hidden),
),
)
.child(div().w_full().min_w_0().child(Label::new(self.label))),
) )
.child(div().w_full().child(Label::new(self.label)))
.into_any_element() .into_any_element()
} }
} }
@ -237,7 +247,7 @@ impl ZedAiOnboarding {
.icon_color(Color::Muted) .icon_color(Color::Muted)
.icon_size(IconSize::XSmall) .icon_size(IconSize::XSmall)
.on_click(move |_, _window, cx| { .on_click(move |_, _window, cx| {
telemetry::event!("Review Terms of Service Click"); telemetry::event!("Review Terms of Service Clicked");
cx.open_url(&zed_urls::terms_of_service(cx)) cx.open_url(&zed_urls::terms_of_service(cx))
}), }),
) )
@ -248,7 +258,7 @@ impl ZedAiOnboarding {
.on_click({ .on_click({
let callback = self.accept_terms_of_service.clone(); let callback = self.accept_terms_of_service.clone();
move |_, window, cx| { move |_, window, cx| {
telemetry::event!("Accepted Terms of Service"); telemetry::event!("Terms of Service Accepted");
(callback)(window, cx)} (callback)(window, cx)}
}), }),
) )
@ -373,7 +383,9 @@ impl ZedAiOnboarding {
.child( .child(
List::new() List::new()
.child(BulletItem::new("500 prompts with Claude models")) .child(BulletItem::new("500 prompts with Claude models"))
.child(BulletItem::new("Unlimited edit predictions")), .child(BulletItem::new(
"Unlimited edit predictions with Zeta, our open-source model",
)),
) )
.child( .child(
Button::new("pro", "Continue with Zed Pro") Button::new("pro", "Continue with Zed Pro")

View file

@ -767,6 +767,11 @@ impl ContextStore {
fn reload(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> { fn reload(&mut self, cx: &mut Context<Self>) -> Task<Result<()>> {
let fs = self.fs.clone(); let fs = self.fs.clone();
cx.spawn(async move |this, cx| { cx.spawn(async move |this, cx| {
pub static ZED_STATELESS: LazyLock<bool> =
LazyLock::new(|| std::env::var("ZED_STATELESS").map_or(false, |v| !v.is_empty()));
if *ZED_STATELESS {
return Ok(());
}
fs.create_dir(contexts_dir()).await?; fs.create_dir(contexts_dir()).await?;
let mut paths = fs.read_dir(contexts_dir()).await?; let mut paths = fs.read_dir(contexts_dir()).await?;

View file

@ -765,12 +765,14 @@ impl UserStore {
pub fn current_plan(&self) -> Option<proto::Plan> { pub fn current_plan(&self) -> Option<proto::Plan> {
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
if let Ok(plan) = std::env::var("ZED_SIMULATE_ZED_PRO_PLAN").as_ref() { if let Ok(plan) = std::env::var("ZED_SIMULATE_PLAN").as_ref() {
return match plan.as_str() { return match plan.as_str() {
"free" => Some(proto::Plan::Free), "free" => Some(proto::Plan::Free),
"trial" => Some(proto::Plan::ZedProTrial), "trial" => Some(proto::Plan::ZedProTrial),
"pro" => Some(proto::Plan::ZedPro), "pro" => Some(proto::Plan::ZedPro),
_ => None, _ => {
panic!("ZED_SIMULATE_PLAN must be one of 'free', 'trial', or 'pro'");
}
}; };
} }

View file

@ -16837,7 +16837,7 @@ async fn test_multibuffer_reverts(cx: &mut TestAppContext) {
} }
#[gpui::test] #[gpui::test]
async fn test_mutlibuffer_in_navigation_history(cx: &mut TestAppContext) { async fn test_multibuffer_in_navigation_history(cx: &mut TestAppContext) {
init_test(cx, |_| {}); init_test(cx, |_| {});
let cols = 4; let cols = 4;

View file

@ -12,6 +12,7 @@ use language::{self, Buffer, Point};
use project::Project; use project::Project;
use std::{ use std::{
any::{Any, TypeId}, any::{Any, TypeId},
cmp,
ops::Range, ops::Range,
pin::pin, pin::pin,
sync::Arc, sync::Arc,
@ -45,38 +46,60 @@ impl TextDiffView {
) -> Option<Task<Result<Entity<Self>>>> { ) -> Option<Task<Result<Entity<Self>>>> {
let source_editor = diff_data.editor.clone(); let source_editor = diff_data.editor.clone();
let source_editor_buffer_and_range = source_editor.update(cx, |editor, cx| { let selection_data = source_editor.update(cx, |editor, cx| {
let multibuffer = editor.buffer().read(cx); let multibuffer = editor.buffer().read(cx);
let source_buffer = multibuffer.as_singleton()?.clone(); let source_buffer = multibuffer.as_singleton()?.clone();
let selections = editor.selections.all::<Point>(cx); let selections = editor.selections.all::<Point>(cx);
let buffer_snapshot = source_buffer.read(cx); let buffer_snapshot = source_buffer.read(cx);
let first_selection = selections.first()?; let first_selection = selections.first()?;
let selection_range = if first_selection.is_empty() { let max_point = buffer_snapshot.max_point();
Point::new(0, 0)..buffer_snapshot.max_point()
} else {
first_selection.start..first_selection.end
};
Some((source_buffer, selection_range)) if first_selection.is_empty() {
let full_range = Point::new(0, 0)..max_point;
return Some((source_buffer, full_range));
}
let start = first_selection.start;
let end = first_selection.end;
let expanded_start = Point::new(start.row, 0);
let expanded_end = if end.column > 0 {
let next_row = end.row + 1;
cmp::min(max_point, Point::new(next_row, 0))
} else {
end
};
Some((source_buffer, expanded_start..expanded_end))
}); });
let Some((source_buffer, selected_range)) = source_editor_buffer_and_range else { let Some((source_buffer, expanded_selection_range)) = selection_data else {
log::warn!("There should always be at least one selection in Zed. This is a bug."); log::warn!("There should always be at least one selection in Zed. This is a bug.");
return None; return None;
}; };
let clipboard_text = diff_data.clipboard_text.clone(); source_editor.update(cx, |source_editor, cx| {
source_editor.change_selections(Default::default(), window, cx, |s| {
let workspace = workspace.weak_handle(); s.select_ranges(vec![
expanded_selection_range.start..expanded_selection_range.end,
let diff_buffer = cx.new(|cx| { ]);
let source_buffer_snapshot = source_buffer.read(cx).snapshot(); })
let diff = BufferDiff::new(&source_buffer_snapshot.text, cx);
diff
}); });
let clipboard_buffer = let source_buffer_snapshot = source_buffer.read(cx).snapshot();
build_clipboard_buffer(clipboard_text, &source_buffer, selected_range.clone(), cx); let mut clipboard_text = diff_data.clipboard_text.clone();
if !clipboard_text.ends_with("\n") {
clipboard_text.push_str("\n");
}
let workspace = workspace.weak_handle();
let diff_buffer = cx.new(|cx| BufferDiff::new(&source_buffer_snapshot.text, cx));
let clipboard_buffer = build_clipboard_buffer(
clipboard_text,
&source_buffer,
expanded_selection_range.clone(),
cx,
);
let task = window.spawn(cx, async move |cx| { let task = window.spawn(cx, async move |cx| {
let project = workspace.update(cx, |workspace, _| workspace.project().clone())?; let project = workspace.update(cx, |workspace, _| workspace.project().clone())?;
@ -89,7 +112,7 @@ impl TextDiffView {
clipboard_buffer, clipboard_buffer,
source_editor, source_editor,
source_buffer, source_buffer,
selected_range, expanded_selection_range,
diff_buffer, diff_buffer,
project, project,
window, window,
@ -208,9 +231,9 @@ impl TextDiffView {
} }
fn build_clipboard_buffer( fn build_clipboard_buffer(
clipboard_text: String, text: String,
source_buffer: &Entity<Buffer>, source_buffer: &Entity<Buffer>,
selected_range: Range<Point>, replacement_range: Range<Point>,
cx: &mut App, cx: &mut App,
) -> Entity<Buffer> { ) -> Entity<Buffer> {
let source_buffer_snapshot = source_buffer.read(cx).snapshot(); let source_buffer_snapshot = source_buffer.read(cx).snapshot();
@ -219,9 +242,9 @@ fn build_clipboard_buffer(
let language = source_buffer.read(cx).language().cloned(); let language = source_buffer.read(cx).language().cloned();
buffer.set_language(language, cx); buffer.set_language(language, cx);
let range_start = source_buffer_snapshot.point_to_offset(selected_range.start); let range_start = source_buffer_snapshot.point_to_offset(replacement_range.start);
let range_end = source_buffer_snapshot.point_to_offset(selected_range.end); let range_end = source_buffer_snapshot.point_to_offset(replacement_range.end);
buffer.edit([(range_start..range_end, clipboard_text)], None, cx); buffer.edit([(range_start..range_end, text)], None, cx);
buffer buffer
}) })
@ -293,7 +316,7 @@ impl Item for TextDiffView {
} }
fn telemetry_event_text(&self) -> Option<&'static str> { fn telemetry_event_text(&self) -> Option<&'static str> {
Some("Diff View Opened") Some("Selection Diff View Opened")
} }
fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) { fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
@ -395,21 +418,13 @@ pub fn selection_location_text(editor: &Editor, cx: &App) -> Option<String> {
let buffer_snapshot = buffer.snapshot(cx); let buffer_snapshot = buffer.snapshot(cx);
let first_selection = editor.selections.disjoint.first()?; let first_selection = editor.selections.disjoint.first()?;
let (start_row, start_column, end_row, end_column) = let selection_start = first_selection.start.to_point(&buffer_snapshot);
if first_selection.start == first_selection.end { let selection_end = first_selection.end.to_point(&buffer_snapshot);
let max_point = buffer_snapshot.max_point();
(0, 0, max_point.row, max_point.column)
} else {
let selection_start = first_selection.start.to_point(&buffer_snapshot);
let selection_end = first_selection.end.to_point(&buffer_snapshot);
( let start_row = selection_start.row;
selection_start.row, let start_column = selection_start.column;
selection_start.column, let end_row = selection_end.row;
selection_end.row, let end_column = selection_end.column;
selection_end.column,
)
};
let range_text = if start_row == end_row { let range_text = if start_row == end_row {
format!("L{}:{}-{}", start_row + 1, start_column + 1, end_column + 1) format!("L{}:{}-{}", start_row + 1, start_column + 1, end_column + 1)
@ -435,14 +450,13 @@ impl Render for TextDiffView {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use editor::test::editor_test_context::assert_state_with_diff;
use editor::{actions, test::editor_test_context::assert_state_with_diff};
use gpui::{TestAppContext, VisualContext}; use gpui::{TestAppContext, VisualContext};
use project::{FakeFs, Project}; use project::{FakeFs, Project};
use serde_json::json; use serde_json::json;
use settings::{Settings, SettingsStore}; use settings::{Settings, SettingsStore};
use unindent::unindent; use unindent::unindent;
use util::path; use util::{path, test::marked_text_ranges};
fn init_test(cx: &mut TestAppContext) { fn init_test(cx: &mut TestAppContext) {
cx.update(|cx| { cx.update(|cx| {
@ -457,52 +471,236 @@ mod tests {
} }
#[gpui::test] #[gpui::test]
async fn test_diffing_clipboard_against_specific_selection(cx: &mut TestAppContext) { async fn test_diffing_clipboard_against_empty_selection_uses_full_buffer_selection(
base_test(true, cx).await; cx: &mut TestAppContext,
) {
base_test(
path!("/test"),
path!("/test/text.txt"),
"def process_incoming_inventory(items, warehouse_id):\n pass\n",
"def process_outgoing_inventory(items, warehouse_id):\n passˇ\n",
&unindent(
"
- def process_incoming_inventory(items, warehouse_id):
+ ˇdef process_outgoing_inventory(items, warehouse_id):
pass
",
),
"Clipboard ↔ text.txt @ L1:1-L3:1",
&format!("Clipboard ↔ {} @ L1:1-L3:1", path!("test/text.txt")),
cx,
)
.await;
} }
#[gpui::test] #[gpui::test]
async fn test_diffing_clipboard_against_empty_selection_uses_full_buffer( async fn test_diffing_clipboard_against_multiline_selection_expands_to_full_lines(
cx: &mut TestAppContext, cx: &mut TestAppContext,
) { ) {
base_test(false, cx).await; base_test(
path!("/test"),
path!("/test/text.txt"),
"def process_incoming_inventory(items, warehouse_id):\n pass\n",
"«def process_outgoing_inventory(items, warehouse_id):\n passˇ»\n",
&unindent(
"
- def process_incoming_inventory(items, warehouse_id):
+ ˇdef process_outgoing_inventory(items, warehouse_id):
pass
",
),
"Clipboard ↔ text.txt @ L1:1-L3:1",
&format!("Clipboard ↔ {} @ L1:1-L3:1", path!("test/text.txt")),
cx,
)
.await;
} }
async fn base_test(select_all_text: bool, cx: &mut TestAppContext) { #[gpui::test]
async fn test_diffing_clipboard_against_single_line_selection(cx: &mut TestAppContext) {
base_test(
path!("/test"),
path!("/test/text.txt"),
"a",
"«bbˇ»",
&unindent(
"
- a
+ ˇbb",
),
"Clipboard ↔ text.txt @ L1:1-3",
&format!("Clipboard ↔ {} @ L1:1-3", path!("test/text.txt")),
cx,
)
.await;
}
#[gpui::test]
async fn test_diffing_clipboard_with_leading_whitespace_against_line(cx: &mut TestAppContext) {
base_test(
path!("/test"),
path!("/test/text.txt"),
" a",
"«bbˇ»",
&unindent(
"
- a
+ ˇbb",
),
"Clipboard ↔ text.txt @ L1:1-3",
&format!("Clipboard ↔ {} @ L1:1-3", path!("test/text.txt")),
cx,
)
.await;
}
#[gpui::test]
async fn test_diffing_clipboard_against_line_with_leading_whitespace(cx: &mut TestAppContext) {
base_test(
path!("/test"),
path!("/test/text.txt"),
"a",
" «bbˇ»",
&unindent(
"
- a
+ ˇ bb",
),
"Clipboard ↔ text.txt @ L1:1-7",
&format!("Clipboard ↔ {} @ L1:1-7", path!("test/text.txt")),
cx,
)
.await;
}
#[gpui::test]
async fn test_diffing_clipboard_against_line_with_leading_whitespace_included_in_selection(
cx: &mut TestAppContext,
) {
base_test(
path!("/test"),
path!("/test/text.txt"),
"a",
"« bbˇ»",
&unindent(
"
- a
+ ˇ bb",
),
"Clipboard ↔ text.txt @ L1:1-7",
&format!("Clipboard ↔ {} @ L1:1-7", path!("test/text.txt")),
cx,
)
.await;
}
#[gpui::test]
async fn test_diffing_clipboard_with_leading_whitespace_against_line_with_leading_whitespace(
cx: &mut TestAppContext,
) {
base_test(
path!("/test"),
path!("/test/text.txt"),
" a",
" «bbˇ»",
&unindent(
"
- a
+ ˇ bb",
),
"Clipboard ↔ text.txt @ L1:1-7",
&format!("Clipboard ↔ {} @ L1:1-7", path!("test/text.txt")),
cx,
)
.await;
}
#[gpui::test]
async fn test_diffing_clipboard_with_leading_whitespace_against_line_with_leading_whitespace_included_in_selection(
cx: &mut TestAppContext,
) {
base_test(
path!("/test"),
path!("/test/text.txt"),
" a",
"« bbˇ»",
&unindent(
"
- a
+ ˇ bb",
),
"Clipboard ↔ text.txt @ L1:1-7",
&format!("Clipboard ↔ {} @ L1:1-7", path!("test/text.txt")),
cx,
)
.await;
}
#[gpui::test]
async fn test_diffing_clipboard_against_partial_selection_expands_to_include_trailing_characters(
cx: &mut TestAppContext,
) {
base_test(
path!("/test"),
path!("/test/text.txt"),
"a",
"«bˇ»b",
&unindent(
"
- a
+ ˇbb",
),
"Clipboard ↔ text.txt @ L1:1-3",
&format!("Clipboard ↔ {} @ L1:1-3", path!("test/text.txt")),
cx,
)
.await;
}
async fn base_test(
project_root: &str,
file_path: &str,
clipboard_text: &str,
editor_text: &str,
expected_diff: &str,
expected_tab_title: &str,
expected_tab_tooltip: &str,
cx: &mut TestAppContext,
) {
init_test(cx); init_test(cx);
let file_name = std::path::Path::new(file_path)
.file_name()
.unwrap()
.to_str()
.unwrap();
let fs = FakeFs::new(cx.executor()); let fs = FakeFs::new(cx.executor());
fs.insert_tree( fs.insert_tree(
path!("/test"), project_root,
json!({ json!({
"a": { file_name: editor_text
"b": {
"text.txt": "new line 1\nline 2\nnew line 3\nline 4"
}
}
}), }),
) )
.await; .await;
let project = Project::test(fs, [path!("/test").as_ref()], cx).await; let project = Project::test(fs, [project_root.as_ref()], cx).await;
let (workspace, mut cx) = let (workspace, mut cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx)); cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
let buffer = project let buffer = project
.update(cx, |project, cx| { .update(cx, |project, cx| project.open_local_buffer(file_path, cx))
project.open_local_buffer(path!("/test/a/b/text.txt"), cx)
})
.await .await
.unwrap(); .unwrap();
let editor = cx.new_window_entity(|window, cx| { let editor = cx.new_window_entity(|window, cx| {
let mut editor = Editor::for_buffer(buffer, None, window, cx); let mut editor = Editor::for_buffer(buffer, None, window, cx);
editor.set_text("new line 1\nline 2\nnew line 3\nline 4\n", window, cx); let (unmarked_text, selection_ranges) = marked_text_ranges(editor_text, false);
editor.set_text(unmarked_text, window, cx);
if select_all_text { editor.change_selections(Default::default(), window, cx, |s| {
editor.select_all(&actions::SelectAll, window, cx); s.select_ranges(selection_ranges)
} });
editor editor
}); });
@ -511,7 +709,7 @@ mod tests {
.update_in(cx, |workspace, window, cx| { .update_in(cx, |workspace, window, cx| {
TextDiffView::open( TextDiffView::open(
&DiffClipboardWithSelectionData { &DiffClipboardWithSelectionData {
clipboard_text: "old line 1\nline 2\nold line 3\nline 4\n".to_string(), clipboard_text: clipboard_text.to_string(),
editor, editor,
}, },
workspace, workspace,
@ -528,26 +726,14 @@ mod tests {
assert_state_with_diff( assert_state_with_diff(
&diff_view.read_with(cx, |diff_view, _| diff_view.diff_editor.clone()), &diff_view.read_with(cx, |diff_view, _| diff_view.diff_editor.clone()),
&mut cx, &mut cx,
&unindent( expected_diff,
"
- old line 1
+ ˇnew line 1
line 2
- old line 3
+ new line 3
line 4
",
),
); );
diff_view.read_with(cx, |diff_view, cx| { diff_view.read_with(cx, |diff_view, cx| {
assert_eq!( assert_eq!(diff_view.tab_content_text(0, cx), expected_tab_title);
diff_view.tab_content_text(0, cx),
"Clipboard ↔ text.txt @ L1:1-L5:1"
);
assert_eq!( assert_eq!(
diff_view.tab_tooltip_text(cx).unwrap(), diff_view.tab_tooltip_text(cx).unwrap(),
format!("Clipboard ↔ {}", path!("test/a/b/text.txt @ L1:1-L5:1")) expected_tab_tooltip
); );
}); });
} }

View file

@ -845,9 +845,15 @@ impl crate::Keystroke {
{ {
if key.is_ascii_graphic() { if key.is_ascii_graphic() {
key_utf8.to_lowercase() key_utf8.to_lowercase()
// map ctrl-a to a // map ctrl-a to `a`
} else if key_utf32 <= 0x1f { // ctrl-0..9 may emit control codes like ctrl-[, but
((key_utf32 as u8 + 0x60) as char).to_string() // we don't want to map them to `[`
} else if key_utf32 <= 0x1f
&& !name.chars().next().is_some_and(|c| c.is_ascii_digit())
{
((key_utf32 as u8 + 0x40) as char)
.to_ascii_lowercase()
.to_string()
} else { } else {
name name
} }

View file

@ -1159,19 +1159,20 @@ impl RenderOnce for ZedAiConfiguration {
let manage_subscription_buttons = if is_pro { let manage_subscription_buttons = if is_pro {
Button::new("manage_settings", "Manage Subscription") Button::new("manage_settings", "Manage Subscription")
.full_width()
.style(ButtonStyle::Tinted(TintColor::Accent)) .style(ButtonStyle::Tinted(TintColor::Accent))
.on_click(|_, _, cx| cx.open_url(&zed_urls::account_url(cx))) .on_click(|_, _, cx| cx.open_url(&zed_urls::account_url(cx)))
.into_any_element() .into_any_element()
} else if self.plan.is_none() || self.eligible_for_trial { } else if self.plan.is_none() || self.eligible_for_trial {
Button::new("start_trial", "Start 14-day Free Pro Trial") Button::new("start_trial", "Start 14-day Free Pro Trial")
.style(ui::ButtonStyle::Tinted(ui::TintColor::Accent))
.full_width() .full_width()
.style(ui::ButtonStyle::Tinted(ui::TintColor::Accent))
.on_click(|_, _, cx| cx.open_url(&zed_urls::start_trial_url(cx))) .on_click(|_, _, cx| cx.open_url(&zed_urls::start_trial_url(cx)))
.into_any_element() .into_any_element()
} else { } else {
Button::new("upgrade", "Upgrade to Pro") Button::new("upgrade", "Upgrade to Pro")
.style(ui::ButtonStyle::Tinted(ui::TintColor::Accent))
.full_width() .full_width()
.style(ui::ButtonStyle::Tinted(ui::TintColor::Accent))
.on_click(|_, _, cx| cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx))) .on_click(|_, _, cx| cx.open_url(&zed_urls::upgrade_to_zed_pro_url(cx)))
.into_any_element() .into_any_element()
}; };

View file

@ -48,18 +48,29 @@ pub enum Model {
#[serde(rename = "codestral-latest", alias = "codestral-latest")] #[serde(rename = "codestral-latest", alias = "codestral-latest")]
#[default] #[default]
CodestralLatest, CodestralLatest,
#[serde(rename = "mistral-large-latest", alias = "mistral-large-latest")] #[serde(rename = "mistral-large-latest", alias = "mistral-large-latest")]
MistralLargeLatest, MistralLargeLatest,
#[serde(rename = "mistral-medium-latest", alias = "mistral-medium-latest")] #[serde(rename = "mistral-medium-latest", alias = "mistral-medium-latest")]
MistralMediumLatest, MistralMediumLatest,
#[serde(rename = "mistral-small-latest", alias = "mistral-small-latest")] #[serde(rename = "mistral-small-latest", alias = "mistral-small-latest")]
MistralSmallLatest, MistralSmallLatest,
#[serde(rename = "magistral-medium-latest", alias = "magistral-medium-latest")]
MagistralMediumLatest,
#[serde(rename = "magistral-small-latest", alias = "magistral-small-latest")]
MagistralSmallLatest,
#[serde(rename = "open-mistral-nemo", alias = "open-mistral-nemo")] #[serde(rename = "open-mistral-nemo", alias = "open-mistral-nemo")]
OpenMistralNemo, OpenMistralNemo,
#[serde(rename = "open-codestral-mamba", alias = "open-codestral-mamba")] #[serde(rename = "open-codestral-mamba", alias = "open-codestral-mamba")]
OpenCodestralMamba, OpenCodestralMamba,
#[serde(rename = "devstral-medium-latest", alias = "devstral-medium-latest")]
DevstralMediumLatest,
#[serde(rename = "devstral-small-latest", alias = "devstral-small-latest")] #[serde(rename = "devstral-small-latest", alias = "devstral-small-latest")]
DevstralSmallLatest, DevstralSmallLatest,
#[serde(rename = "pixtral-12b-latest", alias = "pixtral-12b-latest")] #[serde(rename = "pixtral-12b-latest", alias = "pixtral-12b-latest")]
Pixtral12BLatest, Pixtral12BLatest,
#[serde(rename = "pixtral-large-latest", alias = "pixtral-large-latest")] #[serde(rename = "pixtral-large-latest", alias = "pixtral-large-latest")]
@ -89,8 +100,11 @@ impl Model {
"mistral-large-latest" => Ok(Self::MistralLargeLatest), "mistral-large-latest" => Ok(Self::MistralLargeLatest),
"mistral-medium-latest" => Ok(Self::MistralMediumLatest), "mistral-medium-latest" => Ok(Self::MistralMediumLatest),
"mistral-small-latest" => Ok(Self::MistralSmallLatest), "mistral-small-latest" => Ok(Self::MistralSmallLatest),
"magistral-medium-latest" => Ok(Self::MagistralMediumLatest),
"magistral-small-latest" => Ok(Self::MagistralSmallLatest),
"open-mistral-nemo" => Ok(Self::OpenMistralNemo), "open-mistral-nemo" => Ok(Self::OpenMistralNemo),
"open-codestral-mamba" => Ok(Self::OpenCodestralMamba), "open-codestral-mamba" => Ok(Self::OpenCodestralMamba),
"devstral-medium-latest" => Ok(Self::DevstralMediumLatest),
"devstral-small-latest" => Ok(Self::DevstralSmallLatest), "devstral-small-latest" => Ok(Self::DevstralSmallLatest),
"pixtral-12b-latest" => Ok(Self::Pixtral12BLatest), "pixtral-12b-latest" => Ok(Self::Pixtral12BLatest),
"pixtral-large-latest" => Ok(Self::PixtralLargeLatest), "pixtral-large-latest" => Ok(Self::PixtralLargeLatest),
@ -104,8 +118,11 @@ impl Model {
Self::MistralLargeLatest => "mistral-large-latest", Self::MistralLargeLatest => "mistral-large-latest",
Self::MistralMediumLatest => "mistral-medium-latest", Self::MistralMediumLatest => "mistral-medium-latest",
Self::MistralSmallLatest => "mistral-small-latest", Self::MistralSmallLatest => "mistral-small-latest",
Self::MagistralMediumLatest => "magistral-medium-latest",
Self::MagistralSmallLatest => "magistral-small-latest",
Self::OpenMistralNemo => "open-mistral-nemo", Self::OpenMistralNemo => "open-mistral-nemo",
Self::OpenCodestralMamba => "open-codestral-mamba", Self::OpenCodestralMamba => "open-codestral-mamba",
Self::DevstralMediumLatest => "devstral-medium-latest",
Self::DevstralSmallLatest => "devstral-small-latest", Self::DevstralSmallLatest => "devstral-small-latest",
Self::Pixtral12BLatest => "pixtral-12b-latest", Self::Pixtral12BLatest => "pixtral-12b-latest",
Self::PixtralLargeLatest => "pixtral-large-latest", Self::PixtralLargeLatest => "pixtral-large-latest",
@ -119,8 +136,11 @@ impl Model {
Self::MistralLargeLatest => "mistral-large-latest", Self::MistralLargeLatest => "mistral-large-latest",
Self::MistralMediumLatest => "mistral-medium-latest", Self::MistralMediumLatest => "mistral-medium-latest",
Self::MistralSmallLatest => "mistral-small-latest", Self::MistralSmallLatest => "mistral-small-latest",
Self::MagistralMediumLatest => "magistral-medium-latest",
Self::MagistralSmallLatest => "magistral-small-latest",
Self::OpenMistralNemo => "open-mistral-nemo", Self::OpenMistralNemo => "open-mistral-nemo",
Self::OpenCodestralMamba => "open-codestral-mamba", Self::OpenCodestralMamba => "open-codestral-mamba",
Self::DevstralMediumLatest => "devstral-medium-latest",
Self::DevstralSmallLatest => "devstral-small-latest", Self::DevstralSmallLatest => "devstral-small-latest",
Self::Pixtral12BLatest => "pixtral-12b-latest", Self::Pixtral12BLatest => "pixtral-12b-latest",
Self::PixtralLargeLatest => "pixtral-large-latest", Self::PixtralLargeLatest => "pixtral-large-latest",
@ -136,8 +156,11 @@ impl Model {
Self::MistralLargeLatest => 131000, Self::MistralLargeLatest => 131000,
Self::MistralMediumLatest => 128000, Self::MistralMediumLatest => 128000,
Self::MistralSmallLatest => 32000, Self::MistralSmallLatest => 32000,
Self::MagistralMediumLatest => 40000,
Self::MagistralSmallLatest => 40000,
Self::OpenMistralNemo => 131000, Self::OpenMistralNemo => 131000,
Self::OpenCodestralMamba => 256000, Self::OpenCodestralMamba => 256000,
Self::DevstralMediumLatest => 128000,
Self::DevstralSmallLatest => 262144, Self::DevstralSmallLatest => 262144,
Self::Pixtral12BLatest => 128000, Self::Pixtral12BLatest => 128000,
Self::PixtralLargeLatest => 128000, Self::PixtralLargeLatest => 128000,
@ -160,8 +183,11 @@ impl Model {
| Self::MistralLargeLatest | Self::MistralLargeLatest
| Self::MistralMediumLatest | Self::MistralMediumLatest
| Self::MistralSmallLatest | Self::MistralSmallLatest
| Self::MagistralMediumLatest
| Self::MagistralSmallLatest
| Self::OpenMistralNemo | Self::OpenMistralNemo
| Self::OpenCodestralMamba | Self::OpenCodestralMamba
| Self::DevstralMediumLatest
| Self::DevstralSmallLatest | Self::DevstralSmallLatest
| Self::Pixtral12BLatest | Self::Pixtral12BLatest
| Self::PixtralLargeLatest => true, | Self::PixtralLargeLatest => true,
@ -177,8 +203,11 @@ impl Model {
| Self::MistralSmallLatest => true, | Self::MistralSmallLatest => true,
Self::CodestralLatest Self::CodestralLatest
| Self::MistralLargeLatest | Self::MistralLargeLatest
| Self::MagistralMediumLatest
| Self::MagistralSmallLatest
| Self::OpenMistralNemo | Self::OpenMistralNemo
| Self::OpenCodestralMamba | Self::OpenCodestralMamba
| Self::DevstralMediumLatest
| Self::DevstralSmallLatest => false, | Self::DevstralSmallLatest => false,
Self::Custom { Self::Custom {
supports_images, .. supports_images, ..

View file

@ -55,6 +55,7 @@ fn get_max_tokens(name: &str) -> u64 {
"codellama" | "starcoder2" => 16384, "codellama" | "starcoder2" => 16384,
"mistral" | "codestral" | "mixstral" | "llava" | "qwen2" | "qwen2.5-coder" "mistral" | "codestral" | "mixstral" | "llava" | "qwen2" | "qwen2.5-coder"
| "dolphin-mixtral" => 32768, | "dolphin-mixtral" => 32768,
"magistral" => 40000,
"llama3.1" | "llama3.2" | "llama3.3" | "phi3" | "phi3.5" | "phi4" | "command-r" "llama3.1" | "llama3.2" | "llama3.3" | "phi3" | "phi3.5" | "phi4" | "command-r"
| "qwen3" | "gemma3" | "deepseek-coder-v2" | "deepseek-v3" | "deepseek-r1" | "yi-coder" | "qwen3" | "gemma3" | "deepseek-coder-v2" | "deepseek-v3" | "deepseek-r1" | "yi-coder"
| "devstral" => 128000, | "devstral" => 128000,

View file

@ -2,7 +2,7 @@
description = "The fast, collaborative code editor." description = "The fast, collaborative code editor."
edition.workspace = true edition.workspace = true
name = "zed" name = "zed"
version = "0.197.0" version = "0.197.1"
publish.workspace = true publish.workspace = true
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
authors = ["Zed Team <hi@zed.dev>"] authors = ["Zed Team <hi@zed.dev>"]

View file

@ -1 +1 @@
dev preview