Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T03:42:58.159Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

edit_prediction_provider_setup.rs

767 lines · 27.7 KB · rust
1use codestral::{CODESTRAL_API_URL, codestral_api_key_state, codestral_api_url};
2use edit_prediction::{
3    ApiKeyState,
4    mercury::{MERCURY_CREDENTIALS_URL, mercury_api_token},
5    open_ai_compatible::{open_ai_compatible_api_token, open_ai_compatible_api_url},
6};
7use edit_prediction_ui::{get_available_providers, set_completion_provider};
8use gpui::{App, Entity, ScrollHandle, TaskExt, prelude::*};
9use language::language_settings::AllLanguageSettings;
10
11use settings::Settings as _;
12use ui::{ButtonLink, ConfiguredApiCard, ContextMenu, DropdownMenu, DropdownStyle, prelude::*};
13use workspace::AppState;
14
15const OLLAMA_API_URL_PLACEHOLDER: &str = "http://localhost:11434";
16const OLLAMA_MODEL_PLACEHOLDER: &str = "qwen2.5-coder:3b-base";
17
18const OPEN_AI_COMPATIBLE_API_URL_PLACEHOLDER: &str = "http://localhost:8080/v1/completions";
19const OPEN_AI_COMPATIBLE_MODEL_PLACEHOLDER: &str = "qwen2.5-coder:3b-base";
20
21use crate::{
22    SettingField, SettingItem, SettingsFieldMetadata, SettingsPageItem, SettingsWindow, USER,
23    components::{SettingsInputField, SettingsSectionHeader},
24};
25
26pub(crate) fn render_edit_prediction_setup_page(
27    settings_window: &SettingsWindow,
28    scroll_handle: &ScrollHandle,
29    window: &mut Window,
30    cx: &mut Context<SettingsWindow>,
31) -> AnyElement {
32    let providers = [
33        Some(render_provider_dropdown(window, cx)),
34        render_github_copilot_provider(window, cx).map(IntoElement::into_any_element),
35        Some(
36            render_api_key_provider(
37                IconName::Inception,
38                "Mercury",
39                ApiKeyDocs::Link {
40                    dashboard_url: "https://platform.inceptionlabs.ai/dashboard/api-keys".into(),
41                },
42                mercury_api_token(cx),
43                |_cx| MERCURY_CREDENTIALS_URL,
44                None,
45                window,
46                cx,
47            )
48            .into_any_element(),
49        ),
50        Some(
51            render_api_key_provider(
52                IconName::AiMistral,
53                "Codestral",
54                ApiKeyDocs::Link {
55                    dashboard_url: "https://console.mistral.ai/codestral".into(),
56                },
57                codestral_api_key_state(cx),
58                |cx| codestral_api_url(cx),
59                Some(
60                    settings_window
61                        .render_sub_page_items_section(
62                            codestral_settings().iter().enumerate(),
63                            true,
64                            window,
65                            cx,
66                        )
67                        .into_any_element(),
68                ),
69                window,
70                cx,
71            )
72            .into_any_element(),
73        ),
74        Some(render_ollama_provider(settings_window, window, cx).into_any_element()),
75        Some(
76            render_api_key_provider(
77                IconName::AiOpenAiCompat,
78                "OpenAI Compatible API",
79                ApiKeyDocs::Custom {
80                    message: "The API key sent as Authorization: Bearer {key}.".into(),
81                },
82                open_ai_compatible_api_token(cx),
83                |cx| open_ai_compatible_api_url(cx),
84                Some(
85                    settings_window
86                        .render_sub_page_items_section(
87                            open_ai_compatible_settings().iter().enumerate(),
88                            true,
89                            window,
90                            cx,
91                        )
92                        .into_any_element(),
93                ),
94                window,
95                cx,
96            )
97            .into_any_element(),
98        ),
99    ];
100
101    div()
102        .size_full()
103        .child(
104            v_flex()
105                .id("ep-setup-page")
106                .min_w_0()
107                .size_full()
108                .px_8()
109                .pb_16()
110                .overflow_y_scroll()
111                .track_scroll(&scroll_handle)
112                .children(providers.into_iter().flatten()),
113        )
114        .into_any_element()
115}
116
117fn render_provider_dropdown(window: &mut Window, cx: &mut App) -> AnyElement {
118    let current_provider = AllLanguageSettings::get_global(cx)
119        .edit_predictions
120        .provider;
121    let current_provider_name = current_provider.display_name().unwrap_or("No provider set");
122
123    let menu = ContextMenu::build(window, cx, move |mut menu, _, cx| {
124        let available_providers = get_available_providers(cx);
125        let fs = <dyn fs::Fs>::global(cx);
126
127        for provider in available_providers {
128            let Some(name) = provider.display_name() else {
129                continue;
130            };
131            let is_current = provider == current_provider;
132
133            menu = menu.toggleable_entry(name, is_current, IconPosition::Start, None, {
134                let fs = fs.clone();
135                move |_, cx| {
136                    set_completion_provider(fs.clone(), cx, provider);
137                }
138            });
139        }
140        menu
141    });
142
143    v_flex()
144        .id("provider-selector")
145        .min_w_0()
146        .gap_1p5()
147        .child(SettingsSectionHeader::new("Active Provider").no_padding(true))
148        .child(
149            h_flex()
150                .pt_2p5()
151                .w_full()
152                .min_w_0()
153                .justify_between()
154                .child(
155                    v_flex()
156                        .w_full()
157                        .min_w_0()
158                        .max_w_1_2()
159                        .child(Label::new("Provider"))
160                        .child(
161                            Label::new("Select which provider to use for edit predictions.")
162                                .size(LabelSize::Small)
163                                .color(Color::Muted),
164                        ),
165                )
166                .child(
167                    DropdownMenu::new("provider-dropdown", current_provider_name, menu)
168                        .tab_index(0)
169                        .style(DropdownStyle::Outlined),
170                ),
171        )
172        .into_any_element()
173}
174
175enum ApiKeyDocs {
176    Link { dashboard_url: SharedString },
177    Custom { message: SharedString },
178}
179
180fn render_api_key_provider(
181    icon: IconName,
182    title: &'static str,
183    docs: ApiKeyDocs,
184    api_key_state: Entity<ApiKeyState>,
185    current_url: fn(&mut App) -> SharedString,
186    additional_fields: Option<AnyElement>,
187    window: &mut Window,
188    cx: &mut Context<SettingsWindow>,
189) -> impl IntoElement {
190    let weak_page = cx.weak_entity();
191    let credentials_provider = zed_credentials_provider::global(cx);
192    _ = window.use_keyed_state(current_url(cx), cx, |_, cx| {
193        let task = api_key_state.update(cx, |key_state, cx| {
194            key_state.load_if_needed(
195                current_url(cx),
196                |state| state,
197                credentials_provider.clone(),
198                cx,
199            )
200        });
201        cx.spawn(async move |_, cx| {
202            task.await.ok();
203            weak_page
204                .update(cx, |_, cx| {
205                    cx.notify();
206                })
207                .ok();
208        })
209    });
210
211    let (has_key, env_var_name, is_from_env_var) = api_key_state.read_with(cx, |state, _| {
212        (
213            state.has_key(),
214            Some(state.env_var_name().clone()),
215            state.is_from_env_var(),
216        )
217    });
218
219    let write_key = move |api_key: Option<String>, cx: &mut App| {
220        let credentials_provider = zed_credentials_provider::global(cx);
221        api_key_state
222            .update(cx, |key_state, cx| {
223                let url = current_url(cx);
224                key_state.store(
225                    url,
226                    api_key,
227                    |key_state| key_state,
228                    credentials_provider,
229                    cx,
230                )
231            })
232            .detach_and_log_err(cx);
233    };
234
235    let base_container = v_flex().id(title).min_w_0().pt_8().gap_1p5();
236
237    let header = SettingsSectionHeader::new(title)
238        .icon(icon)
239        .no_padding(true);
240
241    let description = match docs {
242        ApiKeyDocs::Custom { message } => div().min_w_0().w_full().child(
243            Label::new(message)
244                .size(LabelSize::Small)
245                .color(Color::Muted),
246        ),
247        ApiKeyDocs::Link { dashboard_url } => h_flex()
248            .w_full()
249            .min_w_0()
250            .flex_wrap()
251            .gap_0p5()
252            .child(
253                Label::new("Visit the")
254                    .size(LabelSize::Small)
255                    .color(Color::Muted),
256            )
257            .child(
258                ButtonLink::new(format!("{title} dashboard"), dashboard_url)
259                    .no_icon(true)
260                    .label_size(LabelSize::Small)
261                    .label_color(Color::Muted),
262            )
263            .child(
264                Label::new("to generate an API key.")
265                    .size(LabelSize::Small)
266                    .color(Color::Muted),
267            ),
268    };
269
270    let configured_card_label = if is_from_env_var {
271        "API Key Set in Environment Variable"
272    } else {
273        "API Key Configured"
274    };
275
276    let container = if has_key {
277        base_container.child(header).child(
278            ConfiguredApiCard::new(format!("{title}-reset-key"), configured_card_label)
279                .button_label("Reset Key")
280                .button_tab_index(0)
281                .disabled(is_from_env_var)
282                .when_some(env_var_name, |this, env_var_name| {
283                    this.when(is_from_env_var, |this| {
284                        this.tooltip_label(format!(
285                            "To reset your API key, unset the {} environment variable.",
286                            env_var_name
287                        ))
288                    })
289                })
290                .on_click(move |_, _, cx| {
291                    write_key(None, cx);
292                }),
293        )
294    } else {
295        base_container.child(header).child(
296            h_flex()
297                .pt_2p5()
298                .w_full()
299                .min_w_0()
300                .justify_between()
301                .child(
302                    v_flex()
303                        .w_full()
304                        .min_w_0()
305                        .max_w_1_2()
306                        .gap_0p5()
307                        .child(Label::new("API Key"))
308                        .child(description)
309                        .when_some(env_var_name, |this, env_var_name| {
310                            this.child({
311                                let label = format!(
312                                    "Or set the {} env var and restart Omega.",
313                                    env_var_name.as_ref()
314                                );
315                                Label::new(label).size(LabelSize::Small).color(Color::Muted)
316                            })
317                        }),
318                )
319                .child(
320                    SettingsInputField::new(format!("{}-api-key-input", title))
321                        .tab_index(0)
322                        .with_placeholder("xxxxxxxxxxxxxxxxxxxx")
323                        .aria_label(format!("{} API Key", title))
324                        .on_confirm(move |api_key, _window, cx| {
325                            write_key(api_key.filter(|key| !key.is_empty()), cx);
326                        }),
327                ),
328        )
329    };
330
331    container.when_some(additional_fields, |this, additional_fields| {
332        this.child(
333            div()
334                .map(|this| if has_key { this.mt_1() } else { this.mt_4() })
335                .px_neg_8()
336                .border_t_1()
337                .border_color(cx.theme().colors().border_variant)
338                .child(additional_fields),
339        )
340    })
341}
342
343fn render_ollama_provider(
344    settings_window: &SettingsWindow,
345    window: &mut Window,
346    cx: &mut Context<SettingsWindow>,
347) -> impl IntoElement {
348    let ollama_settings = ollama_settings();
349    let additional_fields = settings_window
350        .render_sub_page_items_section(ollama_settings.iter().enumerate(), true, window, cx)
351        .into_any_element();
352
353    v_flex()
354        .id("ollama")
355        .min_w_0()
356        .pt_8()
357        .gap_1p5()
358        .child(
359            SettingsSectionHeader::new("Ollama")
360                .icon(IconName::AiOllama)
361                .no_padding(true),
362        )
363        .child(div().px_neg_8().child(additional_fields))
364}
365
366fn ollama_settings() -> Box<[SettingsPageItem]> {
367    Box::new([
368        SettingsPageItem::SettingItem(SettingItem {
369            title: "API URL",
370            description: "The base URL of your Ollama server.",
371            field: Box::new(SettingField {
372                organization_override: None,
373                pick: |settings| {
374                    settings
375                        .project
376                        .all_languages
377                        .edit_predictions
378                        .as_ref()?
379                        .ollama
380                        .as_ref()?
381                        .api_url
382                        .as_ref()
383                },
384                write: |settings, value, _app: &App| {
385                    settings
386                        .project
387                        .all_languages
388                        .edit_predictions
389                        .get_or_insert_default()
390                        .ollama
391                        .get_or_insert_default()
392                        .api_url = value;
393                },
394                json_path: Some("edit_predictions.ollama.api_url"),
395            }),
396            metadata: Some(Box::new(SettingsFieldMetadata {
397                placeholder: Some(OLLAMA_API_URL_PLACEHOLDER),
398                ..Default::default()
399            })),
400            files: USER,
401        }),
402        SettingsPageItem::SettingItem(SettingItem {
403            title: "Model",
404            description: "The Ollama model to use for edit predictions.",
405            field: Box::new(SettingField {
406                organization_override: None,
407                pick: |settings| {
408                    settings
409                        .project
410                        .all_languages
411                        .edit_predictions
412                        .as_ref()?
413                        .ollama
414                        .as_ref()?
415                        .model
416                        .as_ref()
417                },
418                write: |settings, value, _app: &App| {
419                    settings
420                        .project
421                        .all_languages
422                        .edit_predictions
423                        .get_or_insert_default()
424                        .ollama
425                        .get_or_insert_default()
426                        .model = value;
427                },
428                json_path: Some("edit_predictions.ollama.model"),
429            }),
430            metadata: Some(Box::new(SettingsFieldMetadata {
431                placeholder: Some(OLLAMA_MODEL_PLACEHOLDER),
432                ..Default::default()
433            })),
434            files: USER,
435        }),
436        SettingsPageItem::SettingItem(SettingItem {
437            title: "Prompt Format",
438            description: "The prompt format to use when requesting predictions. Set to Infer to have the format inferred based on the model name.",
439            field: Box::new(SettingField {
440                organization_override: None,
441                pick: |settings| {
442                    settings
443                        .project
444                        .all_languages
445                        .edit_predictions
446                        .as_ref()?
447                        .ollama
448                        .as_ref()?
449                        .prompt_format
450                        .as_ref()
451                },
452                write: |settings, value, _app: &App| {
453                    settings
454                        .project
455                        .all_languages
456                        .edit_predictions
457                        .get_or_insert_default()
458                        .ollama
459                        .get_or_insert_default()
460                        .prompt_format = value;
461                },
462                json_path: Some("edit_predictions.ollama.prompt_format"),
463            }),
464            files: USER,
465            metadata: None,
466        }),
467        SettingsPageItem::SettingItem(SettingItem {
468            title: "Max Output Tokens",
469            description: "The maximum number of tokens to generate.",
470            field: Box::new(SettingField {
471                organization_override: None,
472                pick: |settings| {
473                    settings
474                        .project
475                        .all_languages
476                        .edit_predictions
477                        .as_ref()?
478                        .ollama
479                        .as_ref()?
480                        .max_output_tokens
481                        .as_ref()
482                },
483                write: |settings, value, _app: &App| {
484                    settings
485                        .project
486                        .all_languages
487                        .edit_predictions
488                        .get_or_insert_default()
489                        .ollama
490                        .get_or_insert_default()
491                        .max_output_tokens = value;
492                },
493                json_path: Some("edit_predictions.ollama.max_output_tokens"),
494            }),
495            metadata: None,
496            files: USER,
497        }),
498    ])
499}
500
501fn open_ai_compatible_settings() -> Box<[SettingsPageItem]> {
502    Box::new([
503        SettingsPageItem::SettingItem(SettingItem {
504            title: "API URL",
505            description: "The URL of your OpenAI-compatible server's completions API.",
506            field: Box::new(SettingField {
507                organization_override: None,
508                pick: |settings| {
509                    settings
510                        .project
511                        .all_languages
512                        .edit_predictions
513                        .as_ref()?
514                        .open_ai_compatible_api
515                        .as_ref()?
516                        .api_url
517                        .as_ref()
518                },
519                write: |settings, value, _app: &App| {
520                    settings
521                        .project
522                        .all_languages
523                        .edit_predictions
524                        .get_or_insert_default()
525                        .open_ai_compatible_api
526                        .get_or_insert_default()
527                        .api_url = value;
528                },
529                json_path: Some("edit_predictions.open_ai_compatible_api.api_url"),
530            }),
531            metadata: Some(Box::new(SettingsFieldMetadata {
532                placeholder: Some(OPEN_AI_COMPATIBLE_API_URL_PLACEHOLDER),
533                ..Default::default()
534            })),
535            files: USER,
536        }),
537        SettingsPageItem::SettingItem(SettingItem {
538            title: "Model",
539            description: "The model string to pass to the OpenAI-compatible server.",
540            field: Box::new(SettingField {
541                organization_override: None,
542                pick: |settings| {
543                    settings
544                        .project
545                        .all_languages
546                        .edit_predictions
547                        .as_ref()?
548                        .open_ai_compatible_api
549                        .as_ref()?
550                        .model
551                        .as_ref()
552                },
553                write: |settings, value, _app: &App| {
554                    settings
555                        .project
556                        .all_languages
557                        .edit_predictions
558                        .get_or_insert_default()
559                        .open_ai_compatible_api
560                        .get_or_insert_default()
561                        .model = value;
562                },
563                json_path: Some("edit_predictions.open_ai_compatible_api.model"),
564            }),
565            metadata: Some(Box::new(SettingsFieldMetadata {
566                placeholder: Some(OPEN_AI_COMPATIBLE_MODEL_PLACEHOLDER),
567                ..Default::default()
568            })),
569            files: USER,
570        }),
571        SettingsPageItem::SettingItem(SettingItem {
572            title: "Prompt Format",
573            description: "The prompt format to use when requesting predictions. Set to Infer to have the format inferred based on the model name.",
574            field: Box::new(SettingField {
575                organization_override: None,
576                pick: |settings| {
577                    settings
578                        .project
579                        .all_languages
580                        .edit_predictions
581                        .as_ref()?
582                        .open_ai_compatible_api
583                        .as_ref()?
584                        .prompt_format
585                        .as_ref()
586                },
587                write: |settings, value, _app: &App| {
588                    settings
589                        .project
590                        .all_languages
591                        .edit_predictions
592                        .get_or_insert_default()
593                        .open_ai_compatible_api
594                        .get_or_insert_default()
595                        .prompt_format = value;
596                },
597                json_path: Some("edit_predictions.open_ai_compatible_api.prompt_format"),
598            }),
599            files: USER,
600            metadata: None,
601        }),
602        SettingsPageItem::SettingItem(SettingItem {
603            title: "Max Output Tokens",
604            description: "The maximum number of tokens to generate.",
605            field: Box::new(SettingField {
606                organization_override: None,
607                pick: |settings| {
608                    settings
609                        .project
610                        .all_languages
611                        .edit_predictions
612                        .as_ref()?
613                        .open_ai_compatible_api
614                        .as_ref()?
615                        .max_output_tokens
616                        .as_ref()
617                },
618                write: |settings, value, _app: &App| {
619                    settings
620                        .project
621                        .all_languages
622                        .edit_predictions
623                        .get_or_insert_default()
624                        .open_ai_compatible_api
625                        .get_or_insert_default()
626                        .max_output_tokens = value;
627                },
628                json_path: Some("edit_predictions.open_ai_compatible_api.max_output_tokens"),
629            }),
630            metadata: None,
631            files: USER,
632        }),
633    ])
634}
635
636fn codestral_settings() -> Box<[SettingsPageItem]> {
637    Box::new([
638        SettingsPageItem::SettingItem(SettingItem {
639            title: "API URL",
640            description: "The API URL to use for Codestral.",
641            field: Box::new(SettingField {
642                organization_override: None,
643                pick: |settings| {
644                    settings
645                        .project
646                        .all_languages
647                        .edit_predictions
648                        .as_ref()?
649                        .codestral
650                        .as_ref()?
651                        .api_url
652                        .as_ref()
653                },
654                write: |settings, value, _app: &App| {
655                    settings
656                        .project
657                        .all_languages
658                        .edit_predictions
659                        .get_or_insert_default()
660                        .codestral
661                        .get_or_insert_default()
662                        .api_url = value;
663                },
664                json_path: Some("edit_predictions.codestral.api_url"),
665            }),
666            metadata: Some(Box::new(SettingsFieldMetadata {
667                placeholder: Some(CODESTRAL_API_URL),
668                ..Default::default()
669            })),
670            files: USER,
671        }),
672        SettingsPageItem::SettingItem(SettingItem {
673            title: "Max Tokens",
674            description: "The maximum number of tokens to generate.",
675            field: Box::new(SettingField {
676                organization_override: None,
677                pick: |settings| {
678                    settings
679                        .project
680                        .all_languages
681                        .edit_predictions
682                        .as_ref()?
683                        .codestral
684                        .as_ref()?
685                        .max_tokens
686                        .as_ref()
687                },
688                write: |settings, value, _app: &App| {
689                    settings
690                        .project
691                        .all_languages
692                        .edit_predictions
693                        .get_or_insert_default()
694                        .codestral
695                        .get_or_insert_default()
696                        .max_tokens = value;
697                },
698                json_path: Some("edit_predictions.codestral.max_tokens"),
699            }),
700            metadata: None,
701            files: USER,
702        }),
703        SettingsPageItem::SettingItem(SettingItem {
704            title: "Model",
705            description: "The Codestral model id to use.",
706            field: Box::new(SettingField {
707                organization_override: None,
708                pick: |settings| {
709                    settings
710                        .project
711                        .all_languages
712                        .edit_predictions
713                        .as_ref()?
714                        .codestral
715                        .as_ref()?
716                        .model
717                        .as_ref()
718                },
719                write: |settings, value, _app: &App| {
720                    settings
721                        .project
722                        .all_languages
723                        .edit_predictions
724                        .get_or_insert_default()
725                        .codestral
726                        .get_or_insert_default()
727                        .model = value;
728                },
729                json_path: Some("edit_predictions.codestral.model"),
730            }),
731            metadata: Some(Box::new(SettingsFieldMetadata {
732                placeholder: Some("codestral-latest"),
733                ..Default::default()
734            })),
735            files: USER,
736        }),
737    ])
738}
739
740fn render_github_copilot_provider(window: &mut Window, cx: &mut App) -> Option<impl IntoElement> {
741    let configuration_view = window.use_state(cx, |_, cx| {
742        copilot_ui::ConfigurationView::new(
743            move |cx| {
744                let app_state = AppState::global(cx);
745                copilot::GlobalCopilotAuth::try_get_or_init(app_state, cx)
746                    .is_some_and(|copilot| copilot.0.read(cx).is_authenticated())
747            },
748            copilot_ui::ConfigurationMode::EditPrediction,
749            cx,
750        )
751    });
752
753    Some(
754        v_flex()
755            .id("github-copilot")
756            .min_w_0()
757            .pt_8()
758            .gap_1p5()
759            .child(
760                SettingsSectionHeader::new("GitHub Copilot")
761                    .icon(IconName::Copilot)
762                    .no_padding(true),
763            )
764            .child(configuration_view),
765    )
766}
767
Served at tenant.openagents/omega Member data and write actions are omitted.