9#include "absl/strings/ascii.h"
10#include "absl/strings/str_format.h"
11#include "absl/strings/str_join.h"
12#include "absl/time/clock.h"
19#include "imgui/imgui.h"
21#if defined(__EMSCRIPTEN__)
22#include <emscripten/emscripten.h>
32 return absl::StrFormat(
"%d B", bytes);
33 if (bytes < 1024 * 1024)
34 return absl::StrFormat(
"%.1f KB", bytes / 1024.0);
35 if (bytes < 1024 * 1024 * 1024)
36 return absl::StrFormat(
"%.1f MB", bytes / (1024.0 * 1024.0));
37 return absl::StrFormat(
"%.1f GB", bytes / (1024.0 * 1024.0 * 1024.0));
41 if (timestamp == absl::InfinitePast())
43 auto delta = absl::Now() - timestamp;
44 if (delta < absl::Seconds(60))
46 if (delta < absl::Minutes(60))
47 return absl::StrFormat(
"%.0fm ago", absl::ToDoubleMinutes(delta));
48 if (delta < absl::Hours(24))
49 return absl::StrFormat(
"%.0fh ago", absl::ToDoubleHours(delta));
50 return absl::StrFormat(
"%.0fd ago", absl::ToDoubleHours(delta) / 24.0);
53bool ContainsText(
const std::string& haystack,
const std::string& needle) {
54 return haystack.find(needle) != std::string::npos;
57bool StartsWithText(
const std::string& text,
const std::string& prefix) {
58 return text.rfind(prefix, 0) == 0;
62 if (base_url.empty()) {
65 std::string lower = absl::AsciiStrToLower(base_url);
76 if (base_url.empty()) {
79 const std::string lower = absl::AsciiStrToLower(base_url);
86 ToastManager* toast_manager) {
89 gui::StyledChild config_child(
"AgentConfig", ImVec2(0, 0),
90 {.bg = theme.panel_bg_color},
true);
92 theme.command_text_color);
95 ImGuiTreeNodeFlags_DefaultOpen)) {
102 ImGuiTreeNodeFlags_DefaultOpen)) {
107 ImGuiTreeNodeFlags_DefaultOpen)) {
119 if (callbacks.update_config) {
120 callbacks.update_config(context->agent_config());
126 const Callbacks& callbacks,
127 ToastManager* toast_manager) {
129 ImGuiStyle& style = ImGui::GetStyle();
130 auto& config = context->agent_config();
131 auto& model_cache = context->model_cache();
132 static bool filter_by_provider =
false;
134 if (model_cache.last_provider != config.ai_provider ||
135 model_cache.last_openai_base != config.openai_base_url ||
136 model_cache.last_ollama_host != config.ollama_host) {
137 model_cache.auto_refresh_requested =
false;
138 model_cache.last_provider = config.ai_provider;
139 model_cache.last_openai_base = config.openai_base_url;
140 model_cache.last_ollama_host = config.ollama_host;
143 if (callbacks.refresh_models && !model_cache.loading &&
144 !model_cache.auto_refresh_requested) {
145 model_cache.auto_refresh_requested =
true;
146 callbacks.refresh_models(
false);
148 const float label_width = 120.0f;
149 const ImVec2 compact_padding(style.FramePadding.x,
150 std::max(2.0f, style.FramePadding.y * 0.6f));
151 const ImVec2 compact_spacing(style.ItemSpacing.x,
152 std::max(3.0f, style.ItemSpacing.y * 0.6f));
153 const float env_button_width =
154 ImGui::CalcTextSize(
ICON_MD_SYNC " Env").x + compact_padding.x * 2.0f;
156 auto set_openai_base = [&](
const std::string& base_url) {
157 std::snprintf(config.openai_base_url_buffer,
158 sizeof(config.openai_base_url_buffer),
"%s",
160 config.openai_base_url = config.openai_base_url_buffer;
164 auto provider_button = [&](
const char* label,
const char* value,
165 const ImVec4& color) {
166 bool active = config.ai_provider == value;
167 ImGui::TableNextColumn();
168 std::optional<gui::StyleColorGuard> btn_guard;
170 btn_guard.emplace(std::initializer_list<gui::StyleColorGuard::Entry>{
171 {ImGuiCol_Button, color},
172 {ImGuiCol_ButtonHovered, ImVec4(color.x * 1.15f, color.y * 1.15f,
173 color.z * 1.15f, color.w)}});
175 if (ImGui::Button(label, ImVec2(-FLT_MIN, 28))) {
176 config.ai_provider = value;
177 std::snprintf(config.provider_buffer,
sizeof(config.provider_buffer),
183 gui::StyleVarGuard provider_var_guard(
184 {{ImGuiStyleVar_FramePadding, compact_padding},
185 {ImGuiStyleVar_ItemSpacing, compact_spacing}});
186 if (ImGui::BeginTable(
187 "AgentProviderConfigTable", 2,
188 ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_BordersInnerV)) {
189 ImGui::TableSetupColumn(
"Label", ImGuiTableColumnFlags_WidthFixed,
191 ImGui::TableSetupColumn(
"Value", ImGuiTableColumnFlags_WidthStretch);
193 ImGui::TableNextRow();
194 ImGui::TableSetColumnIndex(0);
195 ImGui::TextDisabled(
"Provider");
196 ImGui::TableSetColumnIndex(1);
197 float provider_width = ImGui::GetContentRegionAvail().x;
198 int provider_columns = provider_width > 560.0f ? 3
199 : provider_width > 360.0f ? 2
201 if (ImGui::BeginTable(
"AgentProviderButtons", provider_columns,
202 ImGuiTableFlags_SizingStretchSame)) {
204 theme.provider_mock);
206 theme.provider_ollama);
208 theme.provider_gemini);
212 theme.provider_openai);
216 ImGui::TableNextRow();
217 ImGui::TableSetColumnIndex(0);
218 ImGui::TextDisabled(
"Ollama Host");
219 ImGui::TableSetColumnIndex(1);
220 ImGui::SetNextItemWidth(-1);
221 if (ImGui::InputTextWithHint(
"##ollama_host",
"http://localhost:11434",
222 config.ollama_host_buffer,
223 IM_ARRAYSIZE(config.ollama_host_buffer))) {
224 config.ollama_host = config.ollama_host_buffer;
227 auto key_row = [&](
const char* label,
const char* hint,
char* buffer,
228 size_t buffer_len, std::string* target,
229 const char* env_var,
const char* input_id,
230 const char* button_id) {
231 ImGui::TableNextRow();
232 ImGui::TableSetColumnIndex(0);
233 ImGui::TextDisabled(
"%s", label);
234 ImGui::TableSetColumnIndex(1);
235 float input_width = ImGui::GetContentRegionAvail().x -
236 env_button_width - style.ItemSpacing.x;
237 bool stack = input_width < 140.0f;
238 ImGui::SetNextItemWidth(stack ? -1 : input_width);
239 if (ImGui::InputTextWithHint(input_id, hint, buffer, buffer_len,
240 ImGuiInputTextFlags_Password)) {
248 if (ImGui::SmallButton(button_id)) {
249 const char* env_key = std::getenv(env_var);
251 std::snprintf(buffer, buffer_len,
"%s", env_key);
257 absl::StrFormat(
"Loaded %s from environment", env_var),
260 }
else if (toast_manager) {
261 toast_manager->Show(absl::StrFormat(
"%s not set", env_var),
267 key_row(
"Gemini Key",
"API key...", config.gemini_key_buffer,
268 IM_ARRAYSIZE(config.gemini_key_buffer), &config.gemini_api_key,
269 "GEMINI_API_KEY",
"##gemini_key",
ICON_MD_SYNC " Env##gemini");
270 key_row(
"Anthropic Key",
"API key...", config.anthropic_key_buffer,
271 IM_ARRAYSIZE(config.anthropic_key_buffer),
272 &config.anthropic_api_key,
"ANTHROPIC_API_KEY",
"##anthropic_key",
274 key_row(
"OpenAI Key",
"API key...", config.openai_key_buffer,
275 IM_ARRAYSIZE(config.openai_key_buffer), &config.openai_api_key,
276 "OPENAI_API_KEY",
"##openai_key",
ICON_MD_SYNC " Env##openai");
278 ImGui::TableNextRow();
279 ImGui::TableSetColumnIndex(0);
280 ImGui::TextDisabled(
"OpenAI Base");
281 ImGui::TableSetColumnIndex(1);
282 float openai_button_width =
283 ImGui::CalcTextSize(
"OpenAI").x + compact_padding.x * 2.0f;
284 float lm_button_width =
285 ImGui::CalcTextSize(
"LM Studio").x + compact_padding.x * 2.0f;
286 float afs_button_width =
287 ImGui::CalcTextSize(
"AFS Bridge").x + compact_padding.x * 2.0f;
288 float reset_button_width =
289 ImGui::CalcTextSize(
"Reset").x + compact_padding.x * 2.0f;
290 float total_buttons = openai_button_width + lm_button_width +
291 afs_button_width + reset_button_width +
292 style.ItemSpacing.x * 3.0f;
293 float base_available = ImGui::GetContentRegionAvail().x;
294 bool base_stack = base_available < total_buttons + 160.0f;
295 ImGui::SetNextItemWidth(base_stack ? -1
296 : base_available - total_buttons -
297 style.ItemSpacing.x);
298 if (ImGui::InputTextWithHint(
299 "##openai_base",
"http://localhost:1234",
300 config.openai_base_url_buffer,
301 IM_ARRAYSIZE(config.openai_base_url_buffer))) {
302 config.openai_base_url = config.openai_base_url_buffer;
309 if (ImGui::SmallButton(
"OpenAI")) {
310 set_openai_base(
"https://api.openai.com");
313 if (ImGui::SmallButton(
"LM Studio")) {
314 set_openai_base(
"http://localhost:1234");
317 if (ImGui::SmallButton(
"AFS Bridge")) {
318 set_openai_base(
"https://halext.org");
321 if (ImGui::SmallButton(
"Reset")) {
322 set_openai_base(
"https://api.openai.com");
330 ImGui::TextColored(theme.command_text_color,
333 ImGui::TextColored(theme.status_success,
336 ImGui::TextColored(theme.text_secondary_color,
343 gui::StyleVarGuard model_var_guard(
344 {{ImGuiStyleVar_FramePadding, compact_padding},
345 {ImGuiStyleVar_ItemSpacing, compact_spacing}});
346 if (ImGui::BeginTable(
347 "AgentModelControls", 2,
348 ImGuiTableFlags_SizingFixedFit | ImGuiTableFlags_BordersInnerV)) {
349 ImGui::TableSetupColumn(
"Label", ImGuiTableColumnFlags_WidthFixed,
351 ImGui::TableSetupColumn(
"Value", ImGuiTableColumnFlags_WidthStretch);
353 ImGui::TableNextRow();
354 ImGui::TableSetColumnIndex(0);
355 ImGui::TextDisabled(
"Model");
356 ImGui::TableSetColumnIndex(1);
357 ImGui::SetNextItemWidth(-1);
358 if (ImGui::InputTextWithHint(
"##ai_model",
"Model name...",
360 IM_ARRAYSIZE(config.model_buffer))) {
361 config.ai_model = config.model_buffer;
364 ImGui::TableNextRow();
365 ImGui::TableSetColumnIndex(0);
366 ImGui::TextDisabled(
"Filter");
367 ImGui::TableSetColumnIndex(1);
368 ImGui::Checkbox(
"Provider", &filter_by_provider);
373 float refresh_width =
376 ImGui::CalcTextSize(
ICON_MD_CLEAR).x + compact_padding.x * 2.0f;
377 float search_width = ImGui::GetContentRegionAvail().x - refresh_width -
379 if (model_cache.search_buffer[0] !=
'\0') {
380 search_width -= clear_width + style.ItemSpacing.x;
382 ImGui::SetNextItemWidth(search_width);
383 ImGui::InputTextWithHint(
"##model_search",
"Search models...",
384 model_cache.search_buffer,
385 IM_ARRAYSIZE(model_cache.search_buffer));
387 if (model_cache.search_buffer[0] !=
'\0') {
389 model_cache.search_buffer[0] =
'\0';
391 if (ImGui::IsItemHovered()) {
392 ImGui::SetTooltip(
"Clear search");
396 if (ImGui::SmallButton(model_cache.loading ?
ICON_MD_SYNC
398 if (callbacks.refresh_models) {
399 callbacks.refresh_models(
true);
406 if (!model_cache.available_models.empty() ||
407 !model_cache.local_model_names.empty()) {
408 const int provider_count =
409 static_cast<int>(model_cache.available_models.size());
410 const int local_count =
411 static_cast<int>(model_cache.local_model_names.size());
412 if (provider_count > 0 && local_count > 0) {
413 ImGui::TextDisabled(
"Models: %d provider, %d local", provider_count,
415 }
else if (provider_count > 0) {
416 ImGui::TextDisabled(
"Models: %d provider", provider_count);
417 }
else if (local_count > 0) {
418 ImGui::TextDisabled(
"Models: %d local files", local_count);
422 float list_height = std::max(220.0f, ImGui::GetContentRegionAvail().y * 0.6f);
423 gui::StyleColorGuard model_list_bg(ImGuiCol_ChildBg, theme.panel_bg_darker);
424 ImGui::BeginChild(
"UnifiedModelList", ImVec2(0, list_height),
true);
425 std::string filter = absl::AsciiStrToLower(model_cache.search_buffer);
429 std::string provider;
430 std::string param_size;
431 std::string quantization;
433 uint64_t size_bytes = 0;
434 bool is_local =
false;
435 bool is_file =
false;
438 std::vector<ModelRow> rows;
439 if (!model_cache.available_models.empty()) {
440 rows.reserve(model_cache.available_models.size());
441 for (
const auto& info : model_cache.available_models) {
443 row.name = info.name;
444 row.provider = info.provider;
445 row.param_size = info.parameter_size;
446 row.quantization = info.quantization;
447 row.family = info.family;
448 row.size_bytes = info.size_bytes;
449 row.is_local = info.is_local;
450 rows.push_back(std::move(row));
453 rows.reserve(model_cache.model_names.size());
454 for (
const auto& model_name : model_cache.model_names) {
456 row.name = model_name;
457 row.provider = config.ai_provider;
458 rows.push_back(std::move(row));
462 if (!filter_by_provider && !model_cache.local_model_names.empty()) {
463 for (
const auto& model_name : model_cache.local_model_names) {
465 row.name = model_name;
466 row.provider =
"local";
469 rows.push_back(std::move(row));
473 auto get_provider_color = [&theme](
const std::string& provider) -> ImVec4 {
475 return theme.provider_ollama;
477 return theme.provider_gemini;
479 return theme.provider_openai;
481 return theme.provider_openai;
482 return theme.provider_mock;
485 auto matches_filter = [&](
const ModelRow& row) {
486 if (filter.empty()) {
489 std::string lower_name = absl::AsciiStrToLower(row.name);
490 std::string lower_provider = absl::AsciiStrToLower(row.provider);
495 if (!row.param_size.empty() &&
496 ContainsText(absl::AsciiStrToLower(row.param_size), filter)) {
499 if (!row.family.empty() &&
500 ContainsText(absl::AsciiStrToLower(row.family), filter)) {
503 if (!row.quantization.empty() &&
504 ContainsText(absl::AsciiStrToLower(row.quantization), filter)) {
511 ImGui::TextDisabled(
"No cached models. Refresh to discover.");
513 float list_width = ImGui::GetContentRegionAvail().x;
514 bool compact = list_width < 520.0f;
515 int column_count = compact ? 3 : 5;
516 ImGuiTableFlags table_flags = ImGuiTableFlags_RowBg |
517 ImGuiTableFlags_BordersInnerH |
518 ImGuiTableFlags_SizingStretchProp;
519 if (ImGui::BeginTable(
"ModelTable", column_count, table_flags)) {
521 ImGui::TableSetupColumn(
"Provider", ImGuiTableColumnFlags_WidthFixed,
523 ImGui::TableSetupColumn(
"Model", ImGuiTableColumnFlags_WidthStretch);
524 ImGui::TableSetupColumn(
"Actions", ImGuiTableColumnFlags_WidthFixed,
527 ImGui::TableSetupColumn(
"Provider", ImGuiTableColumnFlags_WidthFixed,
529 ImGui::TableSetupColumn(
"Model", ImGuiTableColumnFlags_WidthStretch);
530 ImGui::TableSetupColumn(
"Size", ImGuiTableColumnFlags_WidthFixed,
532 ImGui::TableSetupColumn(
"Meta", ImGuiTableColumnFlags_WidthStretch);
533 ImGui::TableSetupColumn(
"Actions", ImGuiTableColumnFlags_WidthFixed,
536 ImGui::TableHeadersRow();
539 for (
const auto& row : rows) {
540 if (filter_by_provider) {
541 if (row.provider ==
"local") {
544 if (!row.provider.empty() && row.provider != config.ai_provider) {
549 if (!matches_filter(row)) {
553 ImGui::PushID(row_id++);
554 ImGui::TableNextRow();
556 ImGui::TableSetColumnIndex(0);
557 if (row.provider.empty()) {
558 ImGui::TextDisabled(
"-");
559 }
else if (row.provider ==
"local") {
562 ImVec4 provider_color = get_provider_color(row.provider);
564 gui::StyleColorGuard badge_color(ImGuiCol_Button, provider_color);
565 gui::StyleVarGuard badge_var(
566 {{ImGuiStyleVar_FrameRounding, 6.0f},
567 {ImGuiStyleVar_FramePadding, ImVec2(4, 1)}});
568 ImGui::SmallButton(row.provider.c_str());
572 ImGui::TableSetColumnIndex(1);
573 bool is_selected = config.ai_model == row.name;
574 if (ImGui::Selectable(row.name.c_str(), is_selected)) {
575 config.ai_model = row.name;
576 std::snprintf(config.model_buffer,
sizeof(config.model_buffer),
"%s",
578 if (!row.provider.empty() && row.provider !=
"local") {
579 config.ai_provider = row.provider;
580 std::snprintf(config.provider_buffer,
581 sizeof(config.provider_buffer),
"%s",
582 row.provider.c_str());
586 if (row.is_file && ImGui::IsItemHovered()) {
588 "Local file detected. Serve this model via LM Studio/Ollama to "
592 std::string size_label = row.param_size;
593 if (size_label.empty() && row.size_bytes > 0) {
598 ImGui::TableSetColumnIndex(2);
599 ImGui::TextColored(theme.text_secondary_color,
"%s",
602 ImGui::TableSetColumnIndex(3);
603 if (!row.quantization.empty()) {
604 ImGui::TextColored(theme.text_info,
"%s", row.quantization.c_str());
605 if (!row.family.empty()) {
609 if (!row.family.empty()) {
610 ImGui::TextColored(theme.text_secondary_gray,
"%s",
613 if (row.is_local && !row.is_file) {
617 }
else if (ImGui::IsItemHovered() &&
618 (!size_label.empty() || !row.quantization.empty() ||
619 !row.family.empty())) {
621 if (!size_label.empty()) {
624 if (!row.quantization.empty()) {
628 meta += row.quantization;
630 if (!row.family.empty()) {
636 ImGui::SetTooltip(
"%s", meta.c_str());
639 int action_column = compact ? 2 : 4;
640 ImGui::TableSetColumnIndex(action_column);
641 gui::StyleVarGuard action_var(ImGuiStyleVar_FramePadding, ImVec2(4, 1));
642 bool is_favorite = std::find(config.favorite_models.begin(),
643 config.favorite_models.end(),
644 row.name) != config.favorite_models.end();
646 gui::StyleColorGuard star_color(
648 is_favorite ? theme.status_warning : theme.text_secondary_color);
652 config.favorite_models.erase(
653 std::remove(config.favorite_models.begin(),
654 config.favorite_models.end(), row.name),
655 config.favorite_models.end());
656 config.model_chain.erase(
657 std::remove(config.model_chain.begin(),
658 config.model_chain.end(), row.name),
659 config.model_chain.end());
661 config.favorite_models.push_back(row.name);
666 if (ImGui::IsItemHovered()) {
667 ImGui::SetTooltip(is_favorite ?
"Remove from favorites"
671 if (!row.provider.empty() && row.provider !=
"local") {
675 preset.name = row.name;
676 preset.model = row.name;
677 preset.provider = row.provider;
679 preset.host = config.ollama_host;
681 preset.host = config.openai_base_url;
683 preset.tags = {row.provider};
684 preset.last_used = absl::Now();
685 config.model_presets.push_back(std::move(preset));
690 if (ImGui::IsItemHovered()) {
691 ImGui::SetTooltip(
"Capture preset from this model");
702 if (model_cache.last_refresh != absl::InfinitePast()) {
704 absl::ToDoubleSeconds(absl::Now() - model_cache.last_refresh);
705 ImGui::TextDisabled(
"Last refresh %.0fs ago", seconds);
707 ImGui::TextDisabled(
"Models not refreshed yet");
714 if (!config.favorite_models.empty()) {
716 ImGui::TextColored(theme.status_warning,
ICON_MD_STAR " Favorites");
717 for (
size_t i = 0; i < config.favorite_models.size(); ++i) {
718 auto& favorite = config.favorite_models[i];
719 ImGui::PushID(
static_cast<int>(i));
720 bool active = config.ai_model == favorite;
722 std::string provider_name;
723 for (
const auto& info : model_cache.available_models) {
724 if (info.name == favorite) {
725 provider_name = info.provider;
730 if (!provider_name.empty()) {
731 ImVec4 badge_color = theme.provider_mock;
733 badge_color = theme.provider_ollama;
735 badge_color = theme.provider_gemini;
737 badge_color = theme.provider_openai;
739 badge_color = theme.provider_openai;
741 gui::StyleColorGuard fav_badge_color(ImGuiCol_Button, badge_color);
742 gui::StyleVarGuard fav_badge_var(
743 {{ImGuiStyleVar_FrameRounding, 6.0f},
744 {ImGuiStyleVar_FramePadding, ImVec2(3, 1)}});
745 ImGui::SmallButton(provider_name.c_str());
750 if (ImGui::Selectable(favorite.c_str(), active)) {
751 config.ai_model = favorite;
752 std::snprintf(config.model_buffer,
sizeof(config.model_buffer),
"%s",
754 if (!provider_name.empty()) {
755 config.ai_provider = provider_name;
756 std::snprintf(config.provider_buffer,
sizeof(config.provider_buffer),
757 "%s", provider_name.c_str());
762 gui::StyleColorGuard close_color(ImGuiCol_Text, theme.status_error);
764 config.model_chain.erase(
765 std::remove(config.model_chain.begin(), config.model_chain.end(),
767 config.model_chain.end());
768 config.favorite_models.erase(config.favorite_models.begin() + i);
779 const Callbacks& callbacks,
780 ToastManager* toast_manager) {
782 ImGuiStyle& style = ImGui::GetStyle();
783 auto& config = context->agent_config();
784 auto& model_cache = context->model_cache();
786 ImGui::TextDisabled(
"Presets");
787 if (config.model_presets.empty()) {
788 ImGui::TextDisabled(
"Capture a preset to swap models quickly.");
792 style.FramePadding.x * 2.0f;
793 float capture_input_width =
794 ImGui::GetContentRegionAvail().x - capture_width - style.ItemSpacing.x;
795 if (capture_input_width > 120.0f) {
796 ImGui::SetNextItemWidth(capture_input_width);
798 ImGui::InputTextWithHint(
"##new_preset_name",
"Preset name...",
799 model_cache.new_preset_name,
800 IM_ARRAYSIZE(model_cache.new_preset_name));
801 if (capture_input_width > 120.0f) {
806 preset.name = model_cache.new_preset_name[0]
807 ? std::string(model_cache.new_preset_name)
809 preset.model = config.ai_model;
810 preset.provider = config.ai_provider;
812 preset.host = config.ollama_host;
814 preset.host = config.openai_base_url;
816 preset.tags = {config.ai_provider};
817 preset.last_used = absl::Now();
818 config.model_presets.push_back(std::move(preset));
819 model_cache.new_preset_name[0] =
'\0';
825 float deck_height = std::max(90.0f, ImGui::GetContentRegionAvail().y * 0.32f);
826 gui::StyledChild preset_child(
"PresetList", ImVec2(0, deck_height),
827 {.bg = theme.panel_bg_darker},
true);
828 if (config.model_presets.empty()) {
829 ImGui::TextDisabled(
"No presets yet");
831 ImGuiTableFlags table_flags = ImGuiTableFlags_RowBg |
832 ImGuiTableFlags_BordersInnerH |
833 ImGuiTableFlags_SizingStretchProp;
834 if (ImGui::BeginTable(
"PresetTable", 3, table_flags)) {
835 ImGui::TableSetupColumn(
"Preset", ImGuiTableColumnFlags_WidthStretch);
836 ImGui::TableSetupColumn(
"Host/Provider",
837 ImGuiTableColumnFlags_WidthStretch);
838 ImGui::TableSetupColumn(
"Actions", ImGuiTableColumnFlags_WidthFixed,
840 ImGui::TableHeadersRow();
842 for (
int i = 0; i < static_cast<int>(config.model_presets.size()); ++i) {
843 auto& preset = config.model_presets[i];
845 ImGui::TableNextRow();
847 ImGui::TableSetColumnIndex(0);
848 bool selected = model_cache.active_preset_index == i;
849 if (ImGui::Selectable(preset.name.c_str(), selected)) {
850 model_cache.active_preset_index = i;
851 if (callbacks.apply_preset) {
852 callbacks.apply_preset(preset);
855 if (ImGui::IsItemHovered()) {
856 std::string tooltip = absl::StrFormat(
"Model: %s", preset.model);
857 if (!preset.tags.empty()) {
859 absl::StrFormat(
"\nTags: %s", absl::StrJoin(preset.tags,
", "));
861 if (preset.last_used != absl::InfinitePast()) {
862 tooltip += absl::StrFormat(
"\nLast used %s",
865 ImGui::SetTooltip(
"%s", tooltip.c_str());
868 ImGui::TableSetColumnIndex(1);
869 if (!preset.host.empty()) {
870 ImGui::TextDisabled(
"%s", preset.host.c_str());
871 }
else if (!preset.provider.empty()) {
872 ImGui::TextDisabled(
"%s", preset.provider.c_str());
874 ImGui::TextDisabled(
"-");
877 ImGui::TableSetColumnIndex(2);
879 gui::StyleVarGuard preset_action_var(ImGuiStyleVar_FramePadding,
882 model_cache.active_preset_index = i;
883 if (callbacks.apply_preset) {
884 callbacks.apply_preset(preset);
890 preset.pinned = !preset.pinned;
894 config.model_presets.erase(config.model_presets.begin() + i);
895 if (model_cache.active_preset_index == i) {
896 model_cache.active_preset_index = -1;
910 ImGui::SliderFloat(
"Temperature", &config.temperature, 0.0f, 1.5f);
911 ImGui::SliderFloat(
"Top P", &config.top_p, 0.0f, 1.0f);
912 ImGui::SliderInt(
"Max Output Tokens", &config.max_output_tokens, 256, 8192);
913 ImGui::SliderInt(
"Max Tool Iterations", &config.max_tool_iterations, 1, 10);
914 ImGui::SliderInt(
"Max Retry Attempts", &config.max_retry_attempts, 0, 5);
915 ImGui::Checkbox(
"Stream responses", &config.stream_responses);
917 ImGui::Checkbox(
"Show reasoning", &config.show_reasoning);
919 ImGui::Checkbox(
"Verbose logs", &config.verbose);
923 const Callbacks& callbacks) {
924 struct ToolToggleEntry {
929 {
"Resources", &config.tool_config.resources,
"resource-list/search"},
930 {
"Dungeon", &config.tool_config.dungeon,
"Room + sprite inspection"},
931 {
"Overworld", &config.tool_config.overworld,
"Map + entrance analysis"},
932 {
"Dialogue", &config.tool_config.dialogue,
"Dialogue list/search"},
933 {
"Messages", &config.tool_config.messages,
"Message table + ROM text"},
934 {
"GUI Automation", &config.tool_config.gui,
"GUI automation tools"},
935 {
"Music", &config.tool_config.music,
"Music info & tracks"},
936 {
"Sprite", &config.tool_config.sprite,
"Sprite palette/properties"},
937 {
"Emulator", &config.tool_config.emulator,
"Emulator controls"},
938 {
"Memory", &config.tool_config.memory_inspector,
939 "RAM inspection & watch"}};
941 ImGui::TextDisabled(
"Expose tools in the agent sidebar and editor panels.");
942 int columns = ImGui::GetContentRegionAvail().x > 360.0f ? 2 : 1;
943 ImGuiTableFlags table_flags =
944 ImGuiTableFlags_SizingStretchSame | ImGuiTableFlags_RowBg;
945 if (ImGui::BeginTable(
"AgentToolTable", columns, table_flags)) {
946 for (
size_t i = 0; i < std::size(entries); ++i) {
947 ImGui::TableNextColumn();
948 if (ImGui::Checkbox(entries[i].label, entries[i].flag)) {
949 if (callbacks.apply_tool_preferences) {
950 callbacks.apply_tool_preferences();
953 if (ImGui::IsItemHovered() && entries[i].hint) {
954 ImGui::SetTooltip(
"%s", entries[i].hint);
963 ImGui::TextDisabled(
"Chain Mode (Experimental)");
966 if (ImGui::Checkbox(
"Round Robin", &round_robin)) {
970 if (ImGui::IsItemHovered()) {
971 ImGui::SetTooltip(
"Rotate through favorite models for each response");
976 if (ImGui::Checkbox(
"Consensus", &consensus)) {
980 if (ImGui::IsItemHovered()) {
981 ImGui::SetTooltip(
"Ask multiple models and synthesize a response");
void RenderToolingControls(AgentConfigState &config, const Callbacks &callbacks)
void RenderParameterControls(AgentConfigState &config)
void RenderModelConfigControls(AgentUIContext *context, const Callbacks &callbacks, ToastManager *toast_manager)
void RenderModelDeck(AgentUIContext *context, const Callbacks &callbacks, ToastManager *toast_manager)
void RenderChainModeControls(AgentConfigState &config)
void Draw(AgentUIContext *context, const Callbacks &callbacks, ToastManager *toast_manager)
#define ICON_MD_CLOUD_SYNC
#define ICON_MD_PLAY_ARROW
#define ICON_MD_CONSTRUCTION
#define ICON_MD_AUTO_AWESOME
#define ICON_MD_PSYCHOLOGY
#define ICON_MD_STAR_BORDER
#define ICON_MD_SMART_TOY
constexpr char kProviderGemini[]
constexpr char kProviderAnthropic[]
constexpr char kProviderMock[]
constexpr char kProviderOpenAi[]
constexpr char kProviderOllama[]
void HorizontalSpacing(float amount)
const AgentUITheme & GetTheme()
void RenderSectionHeader(const char *icon, const char *label, const ImVec4 &color)
std::string FormatByteSize(uint64_t bytes)
bool IsHalextEndpoint(const std::string &base_url)
bool IsLocalEndpoint(const std::string &base_url)
bool ContainsText(const std::string &haystack, const std::string &needle)
bool StartsWithText(const std::string &text, const std::string &prefix)
std::string FormatRelativeTime(absl::Time timestamp)