| 1 | use serde::{Deserialize, Serialize}; |
| 2 | |
| 3 | use crate::common::{BlockId, ServerConversationToken}; |
| 4 | |
| 5 | /// The active base model selection for agent mode. |
| 6 | /// This represents the UI state of which model is selected in the model picker chip. |
| 7 | #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] |
| 8 | pub struct SelectedAgentModel(String); |
| 9 | |
| 10 | impl SelectedAgentModel { |
| 11 | pub fn new(model_id: impl Into<String>) -> Self { |
| 12 | Self(model_id.into()) |
| 13 | } |
| 14 | |
| 15 | pub fn model_id(&self) -> &str { |
| 16 | &self.0 |
| 17 | } |
| 18 | } |
| 19 | |
| 20 | /// The selected conversation for agent mode. |
| 21 | /// When agent view is enabled, this represents the conversation that is currently expanded. |
| 22 | /// When agent view is disabled, this represents the conversation the next query will follow up in. |
| 23 | #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Default)] |
| 24 | pub enum SelectedConversation { |
| 25 | /// An existing conversation identified by a server token |
| 26 | ExistingConversation(ServerConversationToken), |
| 27 | /// The next query will start a new conversation |
| 28 | /// (when agent view is enabled, this looks like an empty expanded view). |
| 29 | #[default] |
| 30 | NewConversation, |
| 31 | /// No conversation selected |
| 32 | /// (when agent view is enabled, this means that no agent view is expanded). |
| 33 | NoConversation, |
| 34 | } |
| 35 | |
| 36 | impl SelectedConversation { |
| 37 | pub fn new(server_token: Option<ServerConversationToken>) -> Self { |
| 38 | match server_token { |
| 39 | Some(token) => Self::ExistingConversation(token), |
| 40 | None => Self::NewConversation, |
| 41 | } |
| 42 | } |
| 43 | |
| 44 | pub fn server_token(&self) -> Option<&ServerConversationToken> { |
| 45 | match self { |
| 46 | Self::ExistingConversation(token) => Some(token), |
| 47 | Self::NewConversation | Self::NoConversation => None, |
| 48 | } |
| 49 | } |
| 50 | |
| 51 | pub fn is_new_conversation(&self) -> bool { |
| 52 | matches!(self, Self::NewConversation) |
| 53 | } |
| 54 | |
| 55 | pub fn is_no_conversation(&self) -> bool { |
| 56 | matches!(self, Self::NoConversation) |
| 57 | } |
| 58 | } |
| 59 | |
| 60 | /// The input type for the universal developer input. |
| 61 | #[derive(Clone, Default, Deserialize, Serialize, PartialEq, Eq, Debug, Copy)] |
| 62 | pub enum InputType { |
| 63 | /// The user input is a shell command. |
| 64 | #[default] |
| 65 | Shell, |
| 66 | |
| 67 | /// The user input is a natural language query to AI. |
| 68 | AI, |
| 69 | } |
| 70 | |
| 71 | /// The input mode for the universal developer input |
| 72 | /// (i.e. the input type and whether the input is locked in said type) |
| 73 | #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Default)] |
| 74 | pub struct InputMode { |
| 75 | pub input_type: InputType, |
| 76 | pub is_locked: bool, |
| 77 | } |
| 78 | |
| 79 | impl InputMode { |
| 80 | pub fn new(input_type: InputType, is_locked: bool) -> Self { |
| 81 | Self { |
| 82 | input_type, |
| 83 | is_locked, |
| 84 | } |
| 85 | } |
| 86 | } |
| 87 | |
| 88 | /// Whether a CLI agent (e.g. Claude Code, Gemini CLI) is active in the |
| 89 | /// terminal. Synced during shared sessions so viewers see the agent footer. |
| 90 | #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Default)] |
| 91 | pub enum CLIAgentSessionState { |
| 92 | /// A CLI agent is running. |
| 93 | Active { |
| 94 | /// Serialized `CLIAgent` enum value (e.g. "Claude", "Gemini", "Codex"). |
| 95 | cli_agent: String, |
| 96 | /// Whether the CLI agent rich input composer is open. |
| 97 | is_rich_input_open: bool, |
| 98 | }, |
| 99 | /// No CLI agent is running (or the previous one ended). |
| 100 | #[default] |
| 101 | Inactive, |
| 102 | } |
| 103 | |
| 104 | /// How the agent is interacting with the current long running command (if at all). |
| 105 | #[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)] |
| 106 | pub enum LongRunningCommandAgentInteractionState { |
| 107 | /// The agent is not interacting with any long running command. |
| 108 | NotInteracting, |
| 109 | /// The user started a long running command and tagged the agent into it. |
| 110 | TaggedIn, |
| 111 | /// The agent started and is controlling a long running command. |
| 112 | InControl, |
| 113 | } |
| 114 | |
| 115 | /// How the agent is interacting with a specific long running command block. |
| 116 | #[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] |
| 117 | pub struct LongRunningCommandAgentInteraction { |
| 118 | pub block_id: BlockId, |
| 119 | pub state: LongRunningCommandAgentInteractionState, |
| 120 | } |
| 121 | |
| 122 | /// The combined state container for universal developer input context. |
| 123 | /// This includes model selection, input mode, and selected conversation. |
| 124 | #[derive(Clone, Debug, Deserialize, Serialize, Default, PartialEq, Eq)] |
| 125 | pub struct UniversalDeveloperInputContext { |
| 126 | /// Which agent model is selected as the primary model. |
| 127 | pub selected_model: Option<SelectedAgentModel>, |
| 128 | |
| 129 | /// The input mode for the universal developer input. |
| 130 | pub input_mode: Option<InputMode>, |
| 131 | |
| 132 | /// The selected conversation (identified by server token) for the next agent query. |
| 133 | pub selected_conversation: Option<SelectedConversation>, |
| 134 | |
| 135 | /// How the agent is interacting with the current long running command (if at all). |
| 136 | /// Deprecated in favor of long_running_command_agent_interaction. |
| 137 | #[serde(default, skip_serializing_if = "Option::is_none")] |
| 138 | pub long_running_command_agent_interaction_state: |
| 139 | Option<LongRunningCommandAgentInteractionState>, |
| 140 | |
| 141 | /// How the agent is interacting with a specific long running command block, if any. |
| 142 | #[serde(default, skip_serializing_if = "Option::is_none")] |
| 143 | pub long_running_command_agent_interaction: Option<LongRunningCommandAgentInteraction>, |
| 144 | |
| 145 | /// Whether auto-approve is enabled for agent actions. |
| 146 | pub auto_approve_agent_actions: Option<bool>, |
| 147 | |
| 148 | /// Whether a CLI agent is active in this terminal. |
| 149 | #[serde(default)] |
| 150 | pub cli_agent_session: CLIAgentSessionState, |
| 151 | } |
| 152 | |
| 153 | /// Update message for universal developer input context - only contains fields that changed. |
| 154 | #[derive(Clone, Debug, Deserialize, Serialize, Default)] |
| 155 | pub struct UniversalDeveloperInputContextUpdate { |
| 156 | #[serde(skip_serializing_if = "Option::is_none")] |
| 157 | pub selected_model: Option<SelectedAgentModel>, |
| 158 | |
| 159 | #[serde(skip_serializing_if = "Option::is_none")] |
| 160 | pub input_mode: Option<InputMode>, |
| 161 | |
| 162 | #[serde(skip_serializing_if = "Option::is_none")] |
| 163 | pub selected_conversation: Option<SelectedConversation>, |
| 164 | |
| 165 | /// How the agent is interacting with the current long running command (if at all). |
| 166 | #[serde(skip_serializing_if = "Option::is_none")] |
| 167 | pub long_running_command_agent_interaction_state: |
| 168 | Option<LongRunningCommandAgentInteractionState>, |
| 169 | |
| 170 | /// How the agent is interacting with a specific long running command block, if any. |
| 171 | #[serde(skip_serializing_if = "Option::is_none")] |
| 172 | pub long_running_command_agent_interaction: Option<LongRunningCommandAgentInteraction>, |
| 173 | |
| 174 | #[serde(skip_serializing_if = "Option::is_none")] |
| 175 | pub auto_approve_agent_actions: Option<bool>, |
| 176 | |
| 177 | /// Whether a CLI agent is active. `None` = no change. |
| 178 | #[serde(skip_serializing_if = "Option::is_none")] |
| 179 | pub cli_agent_session: Option<CLIAgentSessionState>, |
| 180 | } |
| 181 | |
| 182 | impl UniversalDeveloperInputContextUpdate { |
| 183 | /// Returns true if this update would actually change the given cached context. |
| 184 | pub fn changes_cached_context(&self, cached: &UniversalDeveloperInputContext) -> bool { |
| 185 | // We destructure here to ensure that, when new fields are added, we check said fields. |
| 186 | let UniversalDeveloperInputContextUpdate { |
| 187 | selected_model: updated_selected_model, |
| 188 | input_mode: updated_input_mode, |
| 189 | selected_conversation: updated_selected_conversation, |
| 190 | auto_approve_agent_actions: updated_auto_approve_agent_actions, |
| 191 | long_running_command_agent_interaction_state: |
| 192 | updated_long_running_command_agent_interaction_state, |
| 193 | long_running_command_agent_interaction: updated_long_running_command_agent_interaction, |
| 194 | cli_agent_session: updated_cli_agent_session, |
| 195 | } = self; |
| 196 | let UniversalDeveloperInputContext { |
| 197 | selected_model: cached_selected_model, |
| 198 | input_mode: cached_input_mode, |
| 199 | selected_conversation: cached_selected_conversation, |
| 200 | auto_approve_agent_actions: cached_auto_approve_agent_actions, |
| 201 | long_running_command_agent_interaction_state: |
| 202 | cached_long_running_command_agent_interaction_state, |
| 203 | long_running_command_agent_interaction: cached_long_running_command_agent_interaction, |
| 204 | cli_agent_session: cached_cli_agent_session, |
| 205 | } = cached; |
| 206 | |
| 207 | // If any of the fields are present and different from the cached context, return true |
| 208 | // (as the update will change the cached context) |
| 209 | (updated_selected_model.is_some() |
| 210 | && updated_selected_model.as_ref() != cached_selected_model.as_ref()) |
| 211 | || (updated_input_mode.is_some() |
| 212 | && updated_input_mode.as_ref() != cached_input_mode.as_ref()) |
| 213 | || (updated_selected_conversation.is_some() |
| 214 | && updated_selected_conversation != cached_selected_conversation) |
| 215 | || (updated_auto_approve_agent_actions.is_some() |
| 216 | && updated_auto_approve_agent_actions != cached_auto_approve_agent_actions) |
| 217 | || (updated_long_running_command_agent_interaction_state.is_some() |
| 218 | && updated_long_running_command_agent_interaction_state |
| 219 | != cached_long_running_command_agent_interaction_state) |
| 220 | || (updated_long_running_command_agent_interaction.is_some() |
| 221 | && updated_long_running_command_agent_interaction.as_ref() |
| 222 | != cached_long_running_command_agent_interaction.as_ref()) |
| 223 | || (updated_cli_agent_session.is_some() |
| 224 | && updated_cli_agent_session.as_ref() != Some(cached_cli_agent_session)) |
| 225 | } |
| 226 | |
| 227 | /// Merges this update into the current context, returning the new merged state. |
| 228 | pub fn merge_into( |
| 229 | self, |
| 230 | current: UniversalDeveloperInputContext, |
| 231 | ) -> UniversalDeveloperInputContext { |
| 232 | let UniversalDeveloperInputContextUpdate { |
| 233 | selected_model: updated_selected_model, |
| 234 | input_mode: updated_input_mode, |
| 235 | selected_conversation: updated_selected_conversation, |
| 236 | auto_approve_agent_actions: updated_auto_approve_agent_actions, |
| 237 | long_running_command_agent_interaction_state: |
| 238 | updated_long_running_command_agent_interaction_state, |
| 239 | long_running_command_agent_interaction: updated_long_running_command_agent_interaction, |
| 240 | cli_agent_session: updated_cli_agent_session, |
| 241 | } = self; |
| 242 | let UniversalDeveloperInputContext { |
| 243 | selected_model: current_selected_model, |
| 244 | input_mode: current_input_mode, |
| 245 | selected_conversation: current_selected_conversation, |
| 246 | auto_approve_agent_actions: current_auto_approve_agent_actions, |
| 247 | long_running_command_agent_interaction_state: |
| 248 | current_long_running_command_agent_interaction_state, |
| 249 | long_running_command_agent_interaction: current_long_running_command_agent_interaction, |
| 250 | cli_agent_session: current_cli_agent_session, |
| 251 | } = current; |
| 252 | |
| 253 | UniversalDeveloperInputContext { |
| 254 | selected_model: updated_selected_model.or(current_selected_model), |
| 255 | input_mode: updated_input_mode.or(current_input_mode), |
| 256 | selected_conversation: updated_selected_conversation.or(current_selected_conversation), |
| 257 | auto_approve_agent_actions: updated_auto_approve_agent_actions |
| 258 | .or(current_auto_approve_agent_actions), |
| 259 | long_running_command_agent_interaction_state: |
| 260 | updated_long_running_command_agent_interaction_state |
| 261 | .or(current_long_running_command_agent_interaction_state), |
| 262 | long_running_command_agent_interaction: updated_long_running_command_agent_interaction |
| 263 | .or(current_long_running_command_agent_interaction), |
| 264 | cli_agent_session: updated_cli_agent_session.unwrap_or(current_cli_agent_session), |
| 265 | } |
| 266 | } |
| 267 | } |
| 268 | |
| 269 | impl From<UniversalDeveloperInputContext> for UniversalDeveloperInputContextUpdate { |
| 270 | fn from(context: UniversalDeveloperInputContext) -> Self { |
| 271 | Self { |
| 272 | selected_model: context.selected_model, |
| 273 | input_mode: context.input_mode, |
| 274 | selected_conversation: context.selected_conversation, |
| 275 | auto_approve_agent_actions: context.auto_approve_agent_actions, |
| 276 | long_running_command_agent_interaction_state: context |
| 277 | .long_running_command_agent_interaction_state, |
| 278 | long_running_command_agent_interaction: context.long_running_command_agent_interaction, |
| 279 | cli_agent_session: Some(context.cli_agent_session), |
| 280 | } |
| 281 | } |
| 282 | } |