use std::collections::BTreeMap;
use std::path::Path;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ConsoleUiConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(default, skip_serializing_if = "ConsoleBrandingConfig::is_default")]
pub brand: ConsoleBrandingConfig,
#[serde(default, skip_serializing_if = "ConsoleAppearanceConfig::is_default")]
pub appearance: ConsoleAppearanceConfig,
#[serde(default, skip_serializing_if = "ConsoleEnvironmentConfig::is_default")]
pub environment: ConsoleEnvironmentConfig,
#[serde(default, skip_serializing_if = "ConsoleLayoutConfig::is_default")]
pub layout: ConsoleLayoutConfig,
#[serde(default, skip_serializing_if = "ConsoleRailUiConfig::is_default")]
pub rail: ConsoleRailUiConfig,
#[serde(default, skip_serializing_if = "ConsoleSidebarUiConfig::is_default")]
pub sidebar: ConsoleSidebarUiConfig,
#[serde(default, skip_serializing_if = "ConsoleAgentListConfig::is_default")]
pub agent_list: ConsoleAgentListConfig,
#[serde(default, skip_serializing_if = "ConsoleActionsUiConfig::is_default")]
pub actions: ConsoleActionsUiConfig,
}
impl ConsoleUiConfig {
pub fn is_default(value: &Self) -> bool {
value == &Self::default()
}
pub fn normalized(mut self) -> Self {
self.title = normalize_optional_string(self.title);
self.brand = self.brand.normalized();
self.appearance = self.appearance.normalized();
self.environment = self.environment.normalized();
self.layout = self.layout.normalized();
self.rail = self.rail.normalized();
self.sidebar = self.sidebar.normalized();
self.agent_list = self.agent_list.normalized();
self.actions = self.actions.normalized();
self
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ConsoleBrandingConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub logo_url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub logo_alt: Option<String>,
}
impl ConsoleBrandingConfig {
pub fn is_default(value: &Self) -> bool {
value == &Self::default()
}
fn normalized(mut self) -> Self {
self.label = normalize_optional_string(self.label);
self.logo_url = normalize_optional_string(self.logo_url);
self.logo_alt = normalize_optional_string(self.logo_alt);
self
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ConsoleAppearanceConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_theme: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub default_variant: Option<String>,
}
impl ConsoleAppearanceConfig {
pub fn is_default(value: &Self) -> bool {
value == &Self::default()
}
fn normalized(mut self) -> Self {
self.default_theme = normalize_optional_string(self.default_theme);
self.default_variant = normalize_optional_string(self.default_variant);
self
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ConsoleEnvironmentConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
}
impl ConsoleEnvironmentConfig {
pub fn is_default(value: &Self) -> bool {
value == &Self::default()
}
fn normalized(mut self) -> Self {
self.label = normalize_optional_string(self.label);
self
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ConsoleLayoutConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub initial_preset: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub initial_control: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub initial_agent: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub sidebar_collapsed: Option<bool>,
}
impl ConsoleLayoutConfig {
pub fn is_default(value: &Self) -> bool {
value == &Self::default()
}
fn normalized(mut self) -> Self {
self.initial_preset = normalize_optional_string(self.initial_preset);
self.initial_control = normalize_optional_string(self.initial_control);
self.initial_agent = normalize_optional_string(self.initial_agent);
self
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ConsoleRailUiConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub visible: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub collapsed: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub active_preset_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub empty_text: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub filter_presets: Vec<ConsoleRailFilterPresetConfig>,
}
impl ConsoleRailUiConfig {
pub fn is_default(value: &Self) -> bool {
value == &Self::default()
}
fn normalized(mut self) -> Self {
self.active_preset_id = normalize_optional_string(self.active_preset_id);
self.empty_text = normalize_optional_string(self.empty_text);
self.filter_presets = self
.filter_presets
.into_iter()
.map(ConsoleRailFilterPresetConfig::normalized)
.filter(|preset| !preset.id.is_empty() && !preset.label.is_empty())
.collect();
self
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ConsoleRailFilterPresetConfig {
pub id: String,
pub label: String,
#[serde(
default,
rename = "watchedOnly",
alias = "watched_only",
skip_serializing_if = "Option::is_none"
)]
pub watched_only: Option<bool>,
#[serde(
default,
rename = "alertLevels",
alias = "alert_levels",
skip_serializing_if = "Vec::is_empty"
)]
pub alert_levels: Vec<String>,
}
impl ConsoleRailFilterPresetConfig {
fn normalized(mut self) -> Self {
self.id = self.id.trim().to_string();
self.label = self.label.trim().to_string();
self.alert_levels = normalize_string_vec(self.alert_levels);
self
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ConsoleSidebarUiConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub visible_controls: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub hidden_controls: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub buttons: Vec<ConsoleSidebarButtonConfig>,
}
impl ConsoleSidebarUiConfig {
pub fn is_default(value: &Self) -> bool {
value == &Self::default()
}
fn normalized(mut self) -> Self {
self.visible_controls = self.visible_controls.map(normalize_string_vec);
self.hidden_controls = normalize_string_vec(self.hidden_controls);
self.buttons = self
.buttons
.into_iter()
.map(ConsoleSidebarButtonConfig::normalized)
.filter(ConsoleSidebarButtonConfig::is_valid)
.collect();
self
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ConsoleSidebarButtonConfig {
pub id: String,
pub label: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub control: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub href: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub target: Option<String>,
#[serde(default, alias = "iconName", skip_serializing_if = "Option::is_none")]
pub icon_name: Option<String>,
}
impl ConsoleSidebarButtonConfig {
fn normalized(mut self) -> Self {
self.id = self.id.trim().to_string();
self.label = self.label.trim().to_string();
self.control = normalize_optional_string(self.control);
self.href = normalize_optional_string(self.href);
self.target = normalize_optional_string(self.target);
self.icon_name = normalize_optional_string(self.icon_name);
self
}
fn is_valid(&self) -> bool {
!self.id.is_empty()
&& !self.label.is_empty()
&& (self.control.as_ref().is_some_and(|value| !value.is_empty())
|| self.href.as_ref().is_some_and(|value| !value.is_empty()))
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ConsoleAgentListConfig {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub group_by: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub subgroup_by: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub section_order: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fallback_group: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fallback_subgroup: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub collapse_single_subgroup: Option<bool>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub badges: Vec<ConsoleAgentBadgeConfig>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub sections: Vec<ConsoleAgentSectionConfig>,
}
impl ConsoleAgentListConfig {
pub fn is_default(value: &Self) -> bool {
value == &Self::default()
}
fn normalized(mut self) -> Self {
self.group_by = normalize_string_vec(self.group_by);
self.subgroup_by = normalize_string_vec(self.subgroup_by);
self.section_order = normalize_string_vec(self.section_order);
self.fallback_group = normalize_optional_string(self.fallback_group);
self.fallback_subgroup = normalize_optional_string(self.fallback_subgroup);
self.badges = self
.badges
.into_iter()
.map(ConsoleAgentBadgeConfig::normalized)
.filter(ConsoleAgentBadgeConfig::is_valid)
.collect();
self.sections = self
.sections
.into_iter()
.map(ConsoleAgentSectionConfig::normalized)
.filter(|section| !section.name.is_empty())
.collect();
self
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ConsoleAgentBadgeConfig {
pub id: String,
pub label: String,
pub field: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tone: Option<String>,
}
impl ConsoleAgentBadgeConfig {
fn normalized(mut self) -> Self {
self.id = self.id.trim().to_string();
self.label = self.label.trim().to_string();
self.field = self.field.trim().to_string();
self.tone = normalize_optional_string(self.tone);
self
}
fn is_valid(&self) -> bool {
!self.id.is_empty() && !self.label.is_empty() && !self.field.is_empty()
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ConsoleAgentSectionConfig {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub collapsed: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub empty_title: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub empty_text: Option<String>,
}
impl ConsoleAgentSectionConfig {
fn normalized(mut self) -> Self {
self.name = self.name.trim().to_string();
self.empty_title = normalize_optional_string(self.empty_title);
self.empty_text = normalize_optional_string(self.empty_text);
self
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ConsoleActionsUiConfig {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub inspect_label: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub chat_label: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub send_label: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub respawn_label: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub retire_label: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reset_label: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub show_inspect: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub show_chat: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub show_respawn: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub show_retire: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub show_reset: Option<bool>,
}
impl ConsoleActionsUiConfig {
pub fn is_default(value: &Self) -> bool {
value == &Self::default()
}
fn normalized(mut self) -> Self {
self.inspect_label = normalize_optional_string(self.inspect_label);
self.chat_label = normalize_optional_string(self.chat_label);
self.send_label = normalize_optional_string(self.send_label);
self.respawn_label = normalize_optional_string(self.respawn_label);
self.retire_label = normalize_optional_string(self.retire_label);
self.reset_label = normalize_optional_string(self.reset_label);
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ConsoleConfigError {
Io(String),
TomlParse(String),
}
impl std::fmt::Display for ConsoleConfigError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Io(message) => write!(f, "I/O error: {message}"),
Self::TomlParse(message) => write!(f, "TOML parse error: {message}"),
}
}
}
impl std::error::Error for ConsoleConfigError {}
#[derive(Debug, Clone, Default, Deserialize)]
struct ConsoleUiConfigPatch {
#[serde(default)]
title: Option<String>,
#[serde(default)]
brand: Option<ConsoleBrandingConfigPatch>,
#[serde(default)]
appearance: Option<ConsoleAppearanceConfigPatch>,
#[serde(default)]
environment: Option<ConsoleEnvironmentConfigPatch>,
#[serde(default)]
layout: Option<ConsoleLayoutConfigPatch>,
#[serde(default)]
rail: Option<ConsoleRailUiConfigPatch>,
#[serde(default)]
sidebar: Option<ConsoleSidebarUiConfigPatch>,
#[serde(default)]
agent_list: Option<ConsoleAgentListConfigPatch>,
#[serde(default)]
actions: Option<ConsoleActionsUiConfigPatch>,
#[serde(default)]
realms: BTreeMap<String, ConsoleUiConfigPatch>,
}
impl ConsoleUiConfigPatch {
fn apply_to(&self, config: &mut ConsoleUiConfig) {
if let Some(title) = &self.title {
config.title = normalize_optional_string(Some(title.clone()));
}
if let Some(brand) = &self.brand {
brand.apply_to(&mut config.brand);
}
if let Some(appearance) = &self.appearance {
appearance.apply_to(&mut config.appearance);
}
if let Some(environment) = &self.environment {
environment.apply_to(&mut config.environment);
}
if let Some(layout) = &self.layout {
layout.apply_to(&mut config.layout);
}
if let Some(rail) = &self.rail {
rail.apply_to(&mut config.rail);
}
if let Some(sidebar) = &self.sidebar {
sidebar.apply_to(&mut config.sidebar);
}
if let Some(agent_list) = &self.agent_list {
agent_list.apply_to(&mut config.agent_list);
}
if let Some(actions) = &self.actions {
actions.apply_to(&mut config.actions);
}
}
}
#[derive(Debug, Clone, Default, Deserialize)]
struct ConsoleBrandingConfigPatch {
#[serde(default)]
label: Option<String>,
#[serde(default)]
logo_url: Option<String>,
#[serde(default)]
logo_alt: Option<String>,
}
impl ConsoleBrandingConfigPatch {
fn apply_to(&self, config: &mut ConsoleBrandingConfig) {
if let Some(label) = &self.label {
config.label = normalize_optional_string(Some(label.clone()));
}
if let Some(logo_url) = &self.logo_url {
config.logo_url = normalize_optional_string(Some(logo_url.clone()));
}
if let Some(logo_alt) = &self.logo_alt {
config.logo_alt = normalize_optional_string(Some(logo_alt.clone()));
}
}
}
#[derive(Debug, Clone, Default, Deserialize)]
struct ConsoleAppearanceConfigPatch {
#[serde(default)]
default_theme: Option<String>,
#[serde(default)]
default_variant: Option<String>,
}
impl ConsoleAppearanceConfigPatch {
fn apply_to(&self, config: &mut ConsoleAppearanceConfig) {
if let Some(default_theme) = &self.default_theme {
config.default_theme = normalize_optional_string(Some(default_theme.clone()));
}
if let Some(default_variant) = &self.default_variant {
config.default_variant = normalize_optional_string(Some(default_variant.clone()));
}
}
}
#[derive(Debug, Clone, Default, Deserialize)]
struct ConsoleEnvironmentConfigPatch {
#[serde(default)]
label: Option<String>,
}
impl ConsoleEnvironmentConfigPatch {
fn apply_to(&self, config: &mut ConsoleEnvironmentConfig) {
if let Some(label) = &self.label {
config.label = normalize_optional_string(Some(label.clone()));
}
}
}
#[derive(Debug, Clone, Default, Deserialize)]
struct ConsoleLayoutConfigPatch {
#[serde(default)]
initial_preset: Option<String>,
#[serde(default)]
initial_control: Option<String>,
#[serde(default)]
initial_agent: Option<String>,
#[serde(default)]
sidebar_collapsed: Option<bool>,
}
impl ConsoleLayoutConfigPatch {
fn apply_to(&self, config: &mut ConsoleLayoutConfig) {
if let Some(initial_preset) = &self.initial_preset {
config.initial_preset = normalize_optional_string(Some(initial_preset.clone()));
}
if let Some(initial_control) = &self.initial_control {
config.initial_control = normalize_optional_string(Some(initial_control.clone()));
}
if let Some(initial_agent) = &self.initial_agent {
config.initial_agent = normalize_optional_string(Some(initial_agent.clone()));
}
if let Some(sidebar_collapsed) = self.sidebar_collapsed {
config.sidebar_collapsed = Some(sidebar_collapsed);
}
}
}
#[derive(Debug, Clone, Default, Deserialize)]
struct ConsoleRailUiConfigPatch {
#[serde(default)]
visible: Option<bool>,
#[serde(default)]
collapsed: Option<bool>,
#[serde(default)]
active_preset_id: Option<String>,
#[serde(default)]
empty_text: Option<String>,
#[serde(default)]
filter_presets: Option<Vec<ConsoleRailFilterPresetConfig>>,
}
impl ConsoleRailUiConfigPatch {
fn apply_to(&self, config: &mut ConsoleRailUiConfig) {
if let Some(visible) = self.visible {
config.visible = Some(visible);
}
if let Some(collapsed) = self.collapsed {
config.collapsed = Some(collapsed);
}
if let Some(active_preset_id) = &self.active_preset_id {
config.active_preset_id = normalize_optional_string(Some(active_preset_id.clone()));
}
if let Some(empty_text) = &self.empty_text {
config.empty_text = normalize_optional_string(Some(empty_text.clone()));
}
if let Some(filter_presets) = &self.filter_presets {
config.filter_presets = filter_presets
.iter()
.cloned()
.map(ConsoleRailFilterPresetConfig::normalized)
.filter(|preset| !preset.id.is_empty() && !preset.label.is_empty())
.collect();
}
}
}
#[derive(Debug, Clone, Default, Deserialize)]
struct ConsoleSidebarUiConfigPatch {
#[serde(default)]
visible_controls: Option<Vec<String>>,
#[serde(default)]
hidden_controls: Option<Vec<String>>,
#[serde(default)]
buttons: Option<Vec<ConsoleSidebarButtonConfig>>,
}
impl ConsoleSidebarUiConfigPatch {
fn apply_to(&self, config: &mut ConsoleSidebarUiConfig) {
if let Some(visible_controls) = &self.visible_controls {
config.visible_controls = Some(normalize_string_vec(visible_controls.clone()));
}
if let Some(hidden_controls) = &self.hidden_controls {
config.hidden_controls = normalize_string_vec(hidden_controls.clone());
}
if let Some(buttons) = &self.buttons {
config.buttons = buttons
.iter()
.cloned()
.map(ConsoleSidebarButtonConfig::normalized)
.filter(ConsoleSidebarButtonConfig::is_valid)
.collect();
}
}
}
#[derive(Debug, Clone, Default, Deserialize)]
struct ConsoleAgentListConfigPatch {
#[serde(default)]
group_by: Option<Vec<String>>,
#[serde(default)]
subgroup_by: Option<Vec<String>>,
#[serde(default)]
section_order: Option<Vec<String>>,
#[serde(default)]
fallback_group: Option<String>,
#[serde(default)]
fallback_subgroup: Option<String>,
#[serde(default)]
collapse_single_subgroup: Option<bool>,
#[serde(default)]
badges: Option<Vec<ConsoleAgentBadgeConfig>>,
#[serde(default)]
sections: Option<Vec<ConsoleAgentSectionConfig>>,
}
impl ConsoleAgentListConfigPatch {
fn apply_to(&self, config: &mut ConsoleAgentListConfig) {
if let Some(group_by) = &self.group_by {
config.group_by = normalize_string_vec(group_by.clone());
}
if let Some(subgroup_by) = &self.subgroup_by {
config.subgroup_by = normalize_string_vec(subgroup_by.clone());
}
if let Some(section_order) = &self.section_order {
config.section_order = normalize_string_vec(section_order.clone());
}
if let Some(fallback_group) = &self.fallback_group {
config.fallback_group = normalize_optional_string(Some(fallback_group.clone()));
}
if let Some(fallback_subgroup) = &self.fallback_subgroup {
config.fallback_subgroup = normalize_optional_string(Some(fallback_subgroup.clone()));
}
if let Some(collapse_single_subgroup) = self.collapse_single_subgroup {
config.collapse_single_subgroup = Some(collapse_single_subgroup);
}
if let Some(badges) = &self.badges {
config.badges = badges
.iter()
.cloned()
.map(ConsoleAgentBadgeConfig::normalized)
.filter(ConsoleAgentBadgeConfig::is_valid)
.collect();
}
if let Some(sections) = &self.sections {
config.sections = sections
.iter()
.cloned()
.map(ConsoleAgentSectionConfig::normalized)
.filter(|section| !section.name.is_empty())
.collect();
}
}
}
#[derive(Debug, Clone, Default, Deserialize)]
struct ConsoleActionsUiConfigPatch {
#[serde(default)]
inspect_label: Option<String>,
#[serde(default)]
chat_label: Option<String>,
#[serde(default)]
send_label: Option<String>,
#[serde(default)]
respawn_label: Option<String>,
#[serde(default)]
retire_label: Option<String>,
#[serde(default)]
reset_label: Option<String>,
#[serde(default)]
show_inspect: Option<bool>,
#[serde(default)]
show_chat: Option<bool>,
#[serde(default)]
show_respawn: Option<bool>,
#[serde(default)]
show_retire: Option<bool>,
#[serde(default)]
show_reset: Option<bool>,
}
impl ConsoleActionsUiConfigPatch {
fn apply_to(&self, config: &mut ConsoleActionsUiConfig) {
if let Some(inspect_label) = &self.inspect_label {
config.inspect_label = normalize_optional_string(Some(inspect_label.clone()));
}
if let Some(chat_label) = &self.chat_label {
config.chat_label = normalize_optional_string(Some(chat_label.clone()));
}
if let Some(send_label) = &self.send_label {
config.send_label = normalize_optional_string(Some(send_label.clone()));
}
if let Some(respawn_label) = &self.respawn_label {
config.respawn_label = normalize_optional_string(Some(respawn_label.clone()));
}
if let Some(retire_label) = &self.retire_label {
config.retire_label = normalize_optional_string(Some(retire_label.clone()));
}
if let Some(reset_label) = &self.reset_label {
config.reset_label = normalize_optional_string(Some(reset_label.clone()));
}
if let Some(show_inspect) = self.show_inspect {
config.show_inspect = Some(show_inspect);
}
if let Some(show_chat) = self.show_chat {
config.show_chat = Some(show_chat);
}
if let Some(show_respawn) = self.show_respawn {
config.show_respawn = Some(show_respawn);
}
if let Some(show_retire) = self.show_retire {
config.show_retire = Some(show_retire);
}
if let Some(show_reset) = self.show_reset {
config.show_reset = Some(show_reset);
}
}
}
pub fn load_console_ui_config_from_toml(
toml_text: &str,
) -> Result<ConsoleUiConfig, ConsoleConfigError> {
load_console_ui_config_from_toml_for_realm(toml_text, None)
}
pub fn load_console_ui_config_from_toml_for_realm(
toml_text: &str,
realm: Option<&str>,
) -> Result<ConsoleUiConfig, ConsoleConfigError> {
let patch: ConsoleUiConfigPatch =
toml::from_str(toml_text).map_err(|err| ConsoleConfigError::TomlParse(err.to_string()))?;
let mut config = ConsoleUiConfig::default();
patch.apply_to(&mut config);
if let Some(realm) = realm.map(str::trim).filter(|value| !value.is_empty())
&& let Some(overlay) = patch.realms.get(realm)
{
overlay.apply_to(&mut config);
}
Ok(config.normalized())
}
pub fn load_console_ui_config_from_path_for_realm(
path: impl AsRef<Path>,
realm: Option<&str>,
) -> Result<ConsoleUiConfig, ConsoleConfigError> {
let path = path.as_ref();
let text = std::fs::read_to_string(path).map_err(|err| {
ConsoleConfigError::Io(format!("failed to read {}: {err}", path.display()))
})?;
load_console_ui_config_from_toml_for_realm(&text, realm)
}
fn normalize_string_vec(values: Vec<String>) -> Vec<String> {
values
.into_iter()
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())
.collect()
}
fn normalize_optional_string(value: Option<String>) -> Option<String> {
value
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn loads_console_toml_with_sidebar_buttons_and_agent_selectors()
-> Result<(), ConsoleConfigError> {
let config = load_console_ui_config_from_toml(
r#"
title = "OB3"
[brand]
label = "Open Brain"
logo_url = "/assets/ob3.svg"
logo_alt = "OB3"
[appearance]
default_theme = "dark"
default_variant = "graphite"
[environment]
label = "prod"
[layout]
initial_preset = "two_columns"
initial_control = "roster"
sidebar_collapsed = false
[rail]
visible = true
collapsed = false
active_preset_id = "critical"
empty_text = "No signals."
[[rail.filter_presets]]
id = "critical"
label = "Critical"
alert_levels = ["critical"]
[sidebar]
visible_controls = ["topology", "roster", "logs"]
[[sidebar.buttons]]
id = "ob3"
label = "OB3"
href = "https://example.test/ob3"
target = "_blank"
[agent_list]
group_by = ["labels.console_group", "labels.group", "role"]
subgroup_by = ["labels.org"]
section_order = ["Personal", "Initiatives", "Internal"]
fallback_group = "Other"
[[agent_list.badges]]
id = "org"
label = "Org"
field = "labels.org"
tone = "info"
[[agent_list.sections]]
name = "Initiatives"
empty_title = "No initiatives"
empty_text = "Create one in Linear."
[actions]
inspect_label = "Profile"
chat_label = "Talk"
send_label = "Send to agent"
show_reset = false
"#,
)?;
assert_eq!(config.title.as_deref(), Some("OB3"));
assert_eq!(config.brand.label.as_deref(), Some("Open Brain"));
assert_eq!(config.brand.logo_url.as_deref(), Some("/assets/ob3.svg"));
assert_eq!(config.brand.logo_alt.as_deref(), Some("OB3"));
assert_eq!(config.appearance.default_theme.as_deref(), Some("dark"));
assert_eq!(config.environment.label.as_deref(), Some("prod"));
assert_eq!(config.layout.initial_preset.as_deref(), Some("two_columns"));
assert_eq!(config.rail.filter_presets[0].alert_levels, vec!["critical"]);
assert_eq!(
config.sidebar.visible_controls,
Some(vec![
"topology".to_string(),
"roster".to_string(),
"logs".to_string()
])
);
assert_eq!(config.sidebar.buttons.len(), 1);
assert_eq!(config.agent_list.subgroup_by, vec!["labels.org"]);
assert_eq!(config.agent_list.badges[0].field, "labels.org");
assert_eq!(config.agent_list.sections[0].name, "Initiatives");
assert_eq!(config.actions.inspect_label.as_deref(), Some("Profile"));
assert_eq!(config.actions.show_reset, Some(false));
Ok(())
}
#[test]
fn realm_overlay_replaces_only_configured_fields() -> Result<(), ConsoleConfigError> {
let config = load_console_ui_config_from_toml_for_realm(
r#"
title = "Default"
[sidebar]
visible_controls = ["topology", "roster"]
[agent_list]
group_by = ["labels.group"]
[realms.ob3]
title = "OB3"
[realms.ob3.brand]
label = "OB3"
[realms.ob3.agent_list]
subgroup_by = ["labels.org"]
[realms.ob3.layout]
initial_control = "logs"
"#,
Some("ob3"),
)?;
assert_eq!(config.title.as_deref(), Some("OB3"));
assert_eq!(
config.sidebar.visible_controls,
Some(vec!["topology".to_string(), "roster".to_string()])
);
assert_eq!(config.brand.label.as_deref(), Some("OB3"));
assert_eq!(config.layout.initial_control.as_deref(), Some("logs"));
assert_eq!(config.agent_list.group_by, vec!["labels.group"]);
assert_eq!(config.agent_list.subgroup_by, vec!["labels.org"]);
Ok(())
}
}