@@ -3,16 +3,42 @@ use std::sync::Arc;
use anyhow::Result;
use collections::HashSet;
use fs::Fs;
-use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Render, Task};
+use gpui::{
+ DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Render, ScrollHandle, Task,
+};
use language_model::LanguageModelRegistry;
use language_models::provider::open_ai_compatible::{AvailableModel, ModelCapabilities};
use settings::{OpenAiCompatibleSettingsContent, update_settings_file};
use ui::{
- Banner, Checkbox, KeyBinding, Modal, ModalFooter, ModalHeader, Section, ToggleState, prelude::*,
+ Banner, Checkbox, KeyBinding, Modal, ModalFooter, ModalHeader, Section, ToggleState,
+ WithScrollbar, prelude::*,
};
use ui_input::InputField;
use workspace::{ModalView, Workspace};
+fn single_line_input(
+ label: impl Into<SharedString>,
+ placeholder: impl Into<SharedString>,
+ text: Option<&str>,
+ tab_index: isize,
+ window: &mut Window,
+ cx: &mut App,
+) -> Entity<InputField> {
+ cx.new(|cx| {
+ let input = InputField::new(window, cx, placeholder)
+ .label(label)
+ .tab_index(tab_index)
+ .tab_stop(true);
+
+ if let Some(text) = text {
+ input
+ .editor()
+ .update(cx, |editor, cx| editor.set_text(text, window, cx));
+ }
+ input
+ })
+}
+
#[derive(Clone, Copy)]
pub enum LlmCompatibleProvider {
OpenAi,
@@ -41,12 +67,14 @@ struct AddLlmProviderInput {
impl AddLlmProviderInput {
fn new(provider: LlmCompatibleProvider, window: &mut Window, cx: &mut App) -> Self {
- let provider_name = single_line_input("Provider Name", provider.name(), None, window, cx);
- let api_url = single_line_input("API URL", provider.api_url(), None, window, cx);
+ let provider_name =
+ single_line_input("Provider Name", provider.name(), None, 1, window, cx);
+ let api_url = single_line_input("API URL", provider.api_url(), None, 2, window, cx);
let api_key = single_line_input(
"API Key",
"000000000000000000000000000000000000000000000000",
None,
+ 3,
window,
cx,
);
@@ -55,12 +83,13 @@ impl AddLlmProviderInput {
provider_name,
api_url,
api_key,
- models: vec![ModelInput::new(window, cx)],
+ models: vec![ModelInput::new(0, window, cx)],
}
}
fn add_model(&mut self, window: &mut Window, cx: &mut App) {
- self.models.push(ModelInput::new(window, cx));
+ let model_index = self.models.len();
+ self.models.push(ModelInput::new(model_index, window, cx));
}
fn remove_model(&mut self, index: usize) {
@@ -84,11 +113,14 @@ struct ModelInput {
}
impl ModelInput {
- fn new(window: &mut Window, cx: &mut App) -> Self {
+ fn new(model_index: usize, window: &mut Window, cx: &mut App) -> Self {
+ let base_tab_index = (3 + (model_index * 4)) as isize;
+
let model_name = single_line_input(
"Model Name",
"e.g. gpt-4o, claude-opus-4, gemini-2.5-pro",
None,
+ base_tab_index + 1,
window,
cx,
);
@@ -96,6 +128,7 @@ impl ModelInput {
"Max Completion Tokens",
"200000",
Some("200000"),
+ base_tab_index + 2,
window,
cx,
);
@@ -103,16 +136,26 @@ impl ModelInput {
"Max Output Tokens",
"Max Output Tokens",
Some("32000"),
+ base_tab_index + 3,
window,
cx,
);
- let max_tokens = single_line_input("Max Tokens", "Max Tokens", Some("200000"), window, cx);
+ let max_tokens = single_line_input(
+ "Max Tokens",
+ "Max Tokens",
+ Some("200000"),
+ base_tab_index + 4,
+ window,
+ cx,
+ );
+
let ModelCapabilities {
tools,
images,
parallel_tool_calls,
prompt_cache_key,
} = ModelCapabilities::default();
+
Self {
name: model_name,
max_completion_tokens,
@@ -165,24 +208,6 @@ impl ModelInput {
}
}
-fn single_line_input(
- label: impl Into<SharedString>,
- placeholder: impl Into<SharedString>,
- text: Option<&str>,
- window: &mut Window,
- cx: &mut App,
-) -> Entity<InputField> {
- cx.new(|cx| {
- let input = InputField::new(window, cx, placeholder).label(label);
- if let Some(text) = text {
- input
- .editor()
- .update(cx, |editor, cx| editor.set_text(text, window, cx));
- }
- input
- })
-}
-
fn save_provider_to_settings(
input: &AddLlmProviderInput,
cx: &mut App,
@@ -258,6 +283,7 @@ fn save_provider_to_settings(
pub struct AddLlmProviderModal {
provider: LlmCompatibleProvider,
input: AddLlmProviderInput,
+ scroll_handle: ScrollHandle,
focus_handle: FocusHandle,
last_error: Option<SharedString>,
}
@@ -278,6 +304,7 @@ impl AddLlmProviderModal {
provider,
last_error: None,
focus_handle: cx.focus_handle(),
+ scroll_handle: ScrollHandle::new(),
}
}
@@ -418,6 +445,19 @@ impl AddLlmProviderModal {
)
})
}
+
+ fn on_tab(&mut self, _: &menu::SelectNext, window: &mut Window, _: &mut Context<Self>) {
+ window.focus_next();
+ }
+
+ fn on_tab_prev(
+ &mut self,
+ _: &menu::SelectPrevious,
+ window: &mut Window,
+ _: &mut Context<Self>,
+ ) {
+ window.focus_prev();
+ }
}
impl EventEmitter<DismissEvent> for AddLlmProviderModal {}
@@ -431,15 +471,27 @@ impl Focusable for AddLlmProviderModal {
impl ModalView for AddLlmProviderModal {}
impl Render for AddLlmProviderModal {
- fn render(&mut self, _window: &mut ui::Window, cx: &mut ui::Context<Self>) -> impl IntoElement {
+ fn render(&mut self, window: &mut ui::Window, cx: &mut ui::Context<Self>) -> impl IntoElement {
let focus_handle = self.focus_handle(cx);
- div()
+ let window_size = window.viewport_size();
+ let rem_size = window.rem_size();
+ let is_large_window = window_size.height / rem_size > rems_from_px(600.).0;
+
+ let modal_max_height = if is_large_window {
+ rems_from_px(450.)
+ } else {
+ rems_from_px(200.)
+ };
+
+ v_flex()
.id("add-llm-provider-modal")
.key_context("AddLlmProviderModal")
.w(rems(34.))
.elevation_3(cx)
.on_action(cx.listener(Self::cancel))
+ .on_action(cx.listener(Self::on_tab))
+ .on_action(cx.listener(Self::on_tab_prev))
.capture_any_mouse_down(cx.listener(|this, _, window, cx| {
this.focus_handle(cx).focus(window);
}))
@@ -462,17 +514,25 @@ impl Render for AddLlmProviderModal {
)
})
.child(
- v_flex()
- .id("modal_content")
+ div()
.size_full()
- .max_h_128()
- .overflow_y_scroll()
- .px(DynamicSpacing::Base12.rems(cx))
- .gap(DynamicSpacing::Base04.rems(cx))
- .child(self.input.provider_name.clone())
- .child(self.input.api_url.clone())
- .child(self.input.api_key.clone())
- .child(self.render_model_section(cx)),
+ .vertical_scrollbar_for(self.scroll_handle.clone(), window, cx)
+ .child(
+ v_flex()
+ .id("modal_content")
+ .size_full()
+ .tab_group()
+ .max_h(modal_max_height)
+ .pl_3()
+ .pr_4()
+ .gap_2()
+ .overflow_y_scroll()
+ .track_scroll(&self.scroll_handle)
+ .child(self.input.provider_name.clone())
+ .child(self.input.api_url.clone())
+ .child(self.input.api_key.clone())
+ .child(self.render_model_section(cx)),
+ ),
)
.footer(
ModalFooter::new().end_slot(
@@ -642,7 +702,7 @@ mod tests {
let cx = setup_test(cx).await;
cx.update(|window, cx| {
- let model_input = ModelInput::new(window, cx);
+ let model_input = ModelInput::new(0, window, cx);
model_input.name.update(cx, |input, cx| {
input.editor().update(cx, |editor, cx| {
editor.set_text("somemodel", window, cx);
@@ -678,7 +738,7 @@ mod tests {
let cx = setup_test(cx).await;
cx.update(|window, cx| {
- let mut model_input = ModelInput::new(window, cx);
+ let mut model_input = ModelInput::new(0, window, cx);
model_input.name.update(cx, |input, cx| {
input.editor().update(cx, |editor, cx| {
editor.set_text("somemodel", window, cx);
@@ -703,7 +763,7 @@ mod tests {
let cx = setup_test(cx).await;
cx.update(|window, cx| {
- let mut model_input = ModelInput::new(window, cx);
+ let mut model_input = ModelInput::new(0, window, cx);
model_input.name.update(cx, |input, cx| {
input.editor().update(cx, |editor, cx| {
editor.set_text("somemodel", window, cx);
@@ -767,7 +827,7 @@ mod tests {
models.iter().enumerate()
{
if i >= input.models.len() {
- input.models.push(ModelInput::new(window, cx));
+ input.models.push(ModelInput::new(i, window, cx));
}
let model = &mut input.models[i];
set_text(&model.name, name, window, cx);