wisp/session/
session_config_view.rs1use acp_utils::config_meta::{ConfigOptionMeta, SelectOptionMeta};
2use acp_utils::config_option_id::ConfigOptionId;
3use agent_client_protocol::schema::v2::{self as acp, SessionConfigOptionCategory};
4use utils::ReasoningEffort;
5
6#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct LocalConfigOption {
12 pub id: String,
13 pub name: String,
14 pub(crate) category: Option<SessionConfigOptionCategory>,
15 pub(crate) meta: Option<acp::Meta>,
16 pub(crate) kind: LocalConfigKind,
17}
18
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub enum LocalConfigKind {
21 Select { current_value: String, values: Vec<LocalConfigValue>, multi_select: bool },
22 Boolean { current_value: bool },
23}
24
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct LocalConfigValue {
27 pub value: String,
28 pub name: String,
29 pub group: Option<String>,
30 pub description: Option<String>,
31 pub is_disabled: bool,
32 pub meta: SelectOptionMeta,
33 pub raw_meta: Option<acp::Meta>,
34}
35
36impl LocalConfigOption {
37 pub fn from_acp(option: acp::SessionConfigOption) -> Self {
38 let kind = match option.kind {
39 acp::SessionConfigKind::Select(select) => LocalConfigKind::Select {
40 current_value: select.current_value.0.to_string(),
41 values: match select.options {
42 acp::SessionConfigSelectOptions::Ungrouped(options) => {
43 options.into_iter().map(|option| (None, option)).collect()
44 }
45 acp::SessionConfigSelectOptions::Grouped(groups) => groups
46 .into_iter()
47 .flat_map(|group| {
48 let name = group.name;
49 group.options.into_iter().map(move |option| (Some(name.clone()), option))
50 })
51 .collect(),
52 _ => Vec::new(),
53 }
54 .into_iter()
55 .map(|(group, value)| LocalConfigValue {
56 value: value.value.0.to_string(),
57 name: value.name,
58 group,
59 is_disabled: value.description.as_deref().is_some_and(|text| text.starts_with("Unavailable:")),
60 description: value.description,
61 meta: SelectOptionMeta::from_meta(value.meta.as_ref()),
62 raw_meta: value.meta,
63 })
64 .collect(),
65 multi_select: ConfigOptionMeta::from_meta(option.meta.as_ref()).multi_select,
66 },
67 acp::SessionConfigKind::Boolean(boolean) => {
68 LocalConfigKind::Boolean { current_value: boolean.current_value }
69 }
70 _ => LocalConfigKind::Boolean { current_value: false },
71 };
72 Self {
73 id: option.config_id.0.to_string(),
74 name: option.name,
75 category: option.category,
76 meta: option.meta,
77 kind,
78 }
79 }
80
81 pub(crate) fn select(&self) -> Option<LocalConfigSelect<'_>> {
82 match &self.kind {
83 LocalConfigKind::Select { current_value, values, multi_select: _ } => {
84 Some(LocalConfigSelect { current_value, values })
85 }
86 LocalConfigKind::Boolean { .. } => None,
87 }
88 }
89
90 pub fn current_value(&self) -> Option<&str> {
92 match &self.kind {
93 LocalConfigKind::Select { current_value, .. } => Some(current_value),
94 LocalConfigKind::Boolean { .. } => None,
95 }
96 }
97}
98
99pub struct LocalConfigSelect<'a> {
100 pub current_value: &'a str,
101 pub values: &'a [LocalConfigValue],
102}
103
104pub struct LocalConfigView<'a> {
106 options: &'a [LocalConfigOption],
107}
108
109impl<'a> LocalConfigView<'a> {
110 pub fn new(options: &'a [LocalConfigOption]) -> Self {
111 Self { options }
112 }
113
114 pub fn select(&self, id: ConfigOptionId) -> Option<LocalConfigSelect<'a>> {
115 self.options.iter().find(|option| option.id == id.as_str()).and_then(LocalConfigOption::select)
116 }
117
118 pub fn flattened_options(&self, id: ConfigOptionId) -> Vec<&'a LocalConfigValue> {
119 self.select(id).map_or_else(Vec::new, |select| select.values.iter().collect())
120 }
121
122 pub fn current_values(&self, id: ConfigOptionId) -> Vec<&'a str> {
123 self.select(id)
124 .map(|select| select.current_value.split(',').map(str::trim).filter(|value| !value.is_empty()).collect())
125 .unwrap_or_default()
126 }
127
128 pub fn current_display_name(&self, id: ConfigOptionId) -> Option<String> {
129 let values = self.current_values(id);
130 let options = self.flattened_options(id);
131 let names: Vec<_> = values
132 .iter()
133 .filter_map(|value| options.iter().find(|option| option.value == *value).map(|option| option.name.as_str()))
134 .collect();
135 (!names.is_empty()).then(|| names.join(" + "))
136 }
137
138 pub fn next_mode(&self) -> Option<(&'a str, &'a str)> {
139 let option = self
140 .options
141 .iter()
142 .find(|option| option.category == Some(SessionConfigOptionCategory::Mode) && option.select().is_some())?;
143 let select = option.select()?;
144 let current = select.values.iter().position(|value| value.value == select.current_value).unwrap_or(0);
145 let next = select.values.get((current + 1) % select.values.len().max(1))?;
146 Some((option.id.as_str(), next.value.as_str()))
147 }
148
149 pub fn reasoning_levels(&self) -> Vec<ReasoningEffort> {
150 self.flattened_options(ConfigOptionId::ReasoningEffort)
151 .into_iter()
152 .filter_map(|option| option.value.parse().ok())
153 .filter(|effort| *effort != ReasoningEffort::Default)
154 .collect()
155 }
156
157 pub fn reasoning_effort(&self) -> Option<ReasoningEffort> {
158 ReasoningEffort::parse(self.select(ConfigOptionId::ReasoningEffort)?.current_value).unwrap_or(None)
159 }
160
161 pub fn selected_model_metadata(&self) -> Vec<SelectOptionMeta> {
162 let values = self.current_values(ConfigOptionId::Model);
163 let options = self.flattened_options(ConfigOptionId::Model);
164 values
165 .iter()
166 .filter_map(|value| options.iter().find(|option| option.value == *value).map(|option| option.meta.clone()))
167 .collect()
168 }
169}