Skip to repository content767 lines · 27.7 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T04:46:19.866Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
edit_prediction_provider_setup.rs
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