| @@ -17,7 +17,7 @@ use gpui_kit::prelude::FluentBuilder as _; |
| 17 | 17 | use gpui_kit::*; |
| 18 | 18 | |
| 19 | 19 | use super::SettingsView; |
| 20 | | - use super::widgets::{Section, SettingRow, WIDE_WIDTH, ago, code, focus_later, note, reset, search_field, stepper, text_field}; |
| 20 | + use super::widgets::{Section, SettingRow, WIDE_WIDTH, ago, code, content_width, focus_later, note, reset, search_field, stepper, text_field}; |
| 21 | 21 | use crate::claude; |
| 22 | 22 | use crate::models::{self, Catalog, InstanceState, ModelEntry, ProbeStatus, Provider}; |
| 23 | 23 | use crate::settings::{self, CustomModel, EnvVar, ProviderInstance}; |
| @@ -52,6 +52,8 @@ struct ProviderForm { |
| 52 | 52 | custom_slug: Entity<InputState>, |
| 53 | 53 | adding_custom: bool, |
| 54 | 54 | custom_error: Option<SharedString>, |
| 55 | + /// The custom model whose display name is being edited, and the field. |
| 56 | + editing_custom: Option<(String, Entity<InputState>)>, |
| 55 | 57 | accent_open: bool, |
| 56 | 58 | auto_compact_error: bool, |
| 57 | 59 | _subscriptions: Vec<Subscription>, |
| @@ -237,6 +239,7 @@ impl SettingsView { |
| 237 | 239 | custom_slug, |
| 238 | 240 | adding_custom: false, |
| 239 | 241 | custom_error: None, |
| 242 | + editing_custom: None, |
| 240 | 243 | accent_open: false, |
| 241 | 244 | auto_compact_error: false, |
| 242 | 245 | _subscriptions: subscriptions, |
| @@ -448,6 +451,42 @@ impl SettingsView { |
| 448 | 451 | self.edit_instance(&id, window, cx, |instance| instance.custom_models.push(CustomModel { slug, name: String::new() })); |
| 449 | 452 | } |
| 450 | 453 | |
| 454 | + /// Opens the display name field under a custom model. |
| 455 | + fn edit_custom_model(&mut self, slug: String, window: &mut Window, cx: &mut Context<Self>) { |
| 456 | + let Some(id) = self.providers.form.as_ref().map(|form| form.id.clone()) else { return }; |
| 457 | + let name = settings::get(|settings| { |
| 458 | + settings |
| 459 | + .instance(&id) |
| 460 | + .and_then(|instance| instance.custom_models.iter().find(|custom| custom.slug == slug)) |
| 461 | + .map(|custom| custom.name.clone()) |
| 462 | + }) |
| 463 | + .unwrap_or_default(); |
| 464 | + let input = self.instance_input(&name, &slug, window, cx); |
| 465 | + let subscription = cx.subscribe_in(&input, window, |this, _, event: &InputEvent, window, cx| { |
| 466 | + if matches!(event, InputEvent::PressEnter { .. }) { |
| 467 | + this.save_custom_name(window, cx); |
| 468 | + } |
| 469 | + }); |
| 470 | + focus_later(&input, window); |
| 471 | + if let Some(form) = self.providers.form.as_mut() { |
| 472 | + form.editing_custom = Some((slug, input)); |
| 473 | + form._subscriptions.push(subscription); |
| 474 | + } |
| 475 | + cx.notify(); |
| 476 | + } |
| 477 | + |
| 478 | + fn save_custom_name(&mut self, window: &mut Window, cx: &mut Context<Self>) { |
| 479 | + let Some(form) = self.providers.form.as_mut() else { return }; |
| 480 | + let Some((slug, input)) = form.editing_custom.take() else { return }; |
| 481 | + let name = input.read(cx).value().trim().to_string(); |
| 482 | + let id = form.id.clone(); |
| 483 | + self.edit_instance(&id, window, cx, |instance| { |
| 484 | + if let Some(custom) = instance.custom_models.iter_mut().find(|custom| custom.slug == slug) { |
| 485 | + custom.name = name; |
| 486 | + } |
| 487 | + }); |
| 488 | + } |
| 489 | + |
| 451 | 490 | fn remove_custom_model(&mut self, slug: String, window: &mut Window, cx: &mut Context<Self>) { |
| 452 | 491 | let Some(id) = self.providers.form.as_ref().map(|form| form.id.clone()) else { return }; |
| 453 | 492 | self.edit_instance(&id, window, cx, |instance| instance.custom_models.retain(|custom| custom.slug != slug)); |
| @@ -496,11 +535,11 @@ impl SettingsView { |
| 496 | 535 | let checking = instances.iter().any(|state| state.checking); |
| 497 | 536 | let view = cx.weak_entity(); |
| 498 | 537 | |
| 538 | + // Below 768px the list sits over the editor, as in T3 Code. |
| 539 | + let two_panes = content_width(window, WIDE_WIDTH) >= 768.; |
| 499 | 540 | let list = v_flex() |
| 500 | | - .w(px(LIST_WIDTH)) |
| 541 | + .map(|this| if two_panes { this.w(px(LIST_WIDTH)).self_stretch().rounded_l(px(18.)) } else { this.w_full().rounded_t(px(18.)) }) |
| 501 | 542 | .flex_none() |
| 502 | | - .self_stretch() |
| 503 | | - .rounded_l(px(18.)) |
| 504 | 543 | .region(p) |
| 505 | 544 | .children(instances.iter().map(|state| { |
| 506 | 545 | let id = state.id().to_string(); |
| @@ -600,13 +639,13 @@ impl SettingsView { |
| 600 | 639 | ) |
| 601 | 640 | .header_action(refresh) |
| 602 | 641 | .row( |
| 603 | | - h_flex() |
| 604 | | - .items_start() |
| 605 | | - .min_h(px(512.)) |
| 642 | + div() |
| 643 | + .flex() |
| 644 | + .map(|this| if two_panes { this.flex_row().items_start().min_h(px(512.)) } else { this.flex_col() }) |
| 606 | 645 | .rounded(px(18.)) |
| 607 | 646 | .raised(p) |
| 608 | 647 | .child(list) |
| 609 | | - .child(div().w(px(1.)).self_stretch().bg(p.rule)) |
| 648 | + .child(if two_panes { div().w(px(1.)).self_stretch().bg(p.rule) } else { div().h(px(1.)).w_full().bg(p.rule) }) |
| 610 | 649 | .child(div().flex_1().min_w_0().child(detail)), |
| 611 | 650 | ) |
| 612 | 651 | .plain() |
| @@ -637,7 +676,10 @@ impl SettingsView { |
| 637 | 676 | let config = state.config.clone(); |
| 638 | 677 | let default_instance = id == kind.key(); |
| 639 | 678 | let updating = self.providers.updating.contains(&id); |
| 640 | | - let row = |title: &'static str| SettingRow::new(title).page_width(WIDE_WIDTH - LIST_WIDTH); |
| 679 | + // The editor's own width decides when its rows stack. |
| 680 | + let content = content_width(window, WIDE_WIDTH); |
| 681 | + let editor_width = if content >= 768. { content - LIST_WIDTH - 33. } else { content - 32. }; |
| 682 | + let row = |title: &'static str| SettingRow::new(title).page_width(editor_width); |
| 641 | 683 | let (_, headline) = status_headline(state); |
| 642 | 684 | |
| 643 | 685 | // ---- the instance's header ---- |
| @@ -954,7 +996,16 @@ impl SettingsView { |
| 954 | 996 | .on_click(cx.listener(move |this, _, window, cx| this.move_model(down_keys.clone(), ix, 1, window, cx))), |
| 955 | 997 | ) |
| 956 | 998 | .when(custom, |this| { |
| 999 | + let edit_slug = model.id.clone(); |
| 957 | 1000 | this.child( |
| 1001 | + MetalButton::new(SharedString::from(format!("model-edit-{key}"))) |
| 1002 | + .ghost() |
| 1003 | + .mini() |
| 1004 | + .icon(Lucide::Pencil) |
| 1005 | + .tooltip("Edit name") |
| 1006 | + .on_click(cx.listener(move |this, _, window, cx| this.edit_custom_model(edit_slug.clone(), window, cx))), |
| 1007 | + ) |
| 1008 | + .child( |
| 958 | 1009 | MetalButton::new(SharedString::from(format!("model-remove-{key}"))) |
| 959 | 1010 | .ghost() |
| 960 | 1011 | .mini() |
| @@ -1006,13 +1057,41 @@ impl SettingsView { |
| 1006 | 1057 | if !shown(model) { |
| 1007 | 1058 | continue; |
| 1008 | 1059 | } |
| 1009 | | - let row = model_row(model, ix, cx); |
| 1060 | + let mut rows = vec![model_row(model, ix, cx)]; |
| 1061 | + if let Some((slug, input)) = &form.editing_custom |
| 1062 | + && *slug == model.id |
| 1063 | + { |
| 1064 | + rows.push( |
| 1065 | + h_flex() |
| 1066 | + .px(px(12.)) |
| 1067 | + .py(px(6.)) |
| 1068 | + .gap(px(8.)) |
| 1069 | + .child(div().text_size(px(12.5)).text_color(p.ink2).child("Display name")) |
| 1070 | + .child(text_field(input, 0., window, cx).flex_1()) |
| 1071 | + .child( |
| 1072 | + MetalButton::new("custom-name-save") |
| 1073 | + .primary() |
| 1074 | + .compact() |
| 1075 | + .label("Save") |
| 1076 | + .on_click(cx.listener(|this, _, window, cx| this.save_custom_name(window, cx))), |
| 1077 | + ) |
| 1078 | + .child(MetalButton::new("custom-name-cancel").ghost().compact().label("Cancel").on_click(cx.listener( |
| 1079 | + |this, _, _, cx| { |
| 1080 | + if let Some(form) = this.providers.form.as_mut() { |
| 1081 | + form.editing_custom = None; |
| 1082 | + } |
| 1083 | + cx.notify(); |
| 1084 | + }, |
| 1085 | + ))) |
| 1086 | + .into_any_element(), |
| 1087 | + ); |
| 1088 | + } |
| 1010 | 1089 | if hidden.contains(&model.key()) && !state.is_custom(model) { |
| 1011 | | - hidden_rows.push(row); |
| 1090 | + hidden_rows.extend(rows); |
| 1012 | 1091 | } else if is_favorite(model) { |
| 1013 | | - favorite_rows.push(row); |
| 1092 | + favorite_rows.extend(rows); |
| 1014 | 1093 | } else { |
| 1015 | | - all_rows.push(row); |
| 1094 | + all_rows.extend(rows); |
| 1016 | 1095 | } |
| 1017 | 1096 | } |
| 1018 | 1097 | |