Skip to main content

wisp/session/
session_model.rs

1use crate::session::session_config_view::{LocalConfigKind, LocalConfigOption};
2use crate::session::workspace_status::WorkspaceStatus;
3use acp_utils::notifications::{AetherCapabilities, McpServerStatus, McpServerStatusEntry};
4use agent_client_protocol::schema::v1::{self as acp, SessionId};
5use std::path::{Path, PathBuf};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum WorkspaceMoveState {
9    Idle,
10    Listing,
11    Picking,
12    Moving,
13    LoadingSession,
14}
15
16impl WorkspaceMoveState {
17    pub fn is_idle(self) -> bool {
18        matches!(self, Self::Idle)
19    }
20}
21
22pub struct SessionModel {
23    session_id: SessionId,
24    agent_name: String,
25    working_dir: PathBuf,
26    workspace_status: WorkspaceStatus,
27    prompt_capabilities: acp::PromptCapabilities,
28    capabilities: AetherCapabilities,
29    config_options: Vec<LocalConfigOption>,
30    auth_methods: Vec<acp::AuthMethod>,
31    workspace_move_state: WorkspaceMoveState,
32    server_statuses: Vec<McpServerStatusEntry>,
33}
34
35impl SessionModel {
36    pub fn from_config(config: crate::app::AppConfig, capabilities: AetherCapabilities) -> Self {
37        let crate::app::AppConfig {
38            session_id,
39            agent_name,
40            working_dir,
41            workspace_status,
42            prompt_capabilities,
43            config_options,
44            auth_methods,
45            ..
46        } = config;
47        Self {
48            session_id,
49            agent_name,
50            working_dir,
51            workspace_status,
52            prompt_capabilities,
53            capabilities,
54            config_options: config_options.into_iter().map(LocalConfigOption::from_acp).collect(),
55            auth_methods,
56            workspace_move_state: WorkspaceMoveState::Idle,
57            server_statuses: Vec::new(),
58        }
59    }
60
61    pub fn session_id(&self) -> &SessionId {
62        &self.session_id
63    }
64
65    pub fn agent_name(&self) -> &str {
66        &self.agent_name
67    }
68
69    pub fn working_dir(&self) -> &Path {
70        &self.working_dir
71    }
72
73    pub fn prompt_capabilities(&self) -> &acp::PromptCapabilities {
74        &self.prompt_capabilities
75    }
76
77    pub fn capabilities(&self) -> &AetherCapabilities {
78        &self.capabilities
79    }
80
81    pub fn config_options(&self) -> &[LocalConfigOption] {
82        &self.config_options
83    }
84
85    pub fn auth_methods(&self) -> &[acp::AuthMethod] {
86        &self.auth_methods
87    }
88
89    pub fn workspace_status(&self) -> &WorkspaceStatus {
90        &self.workspace_status
91    }
92
93    pub fn workspace_move_state(&self) -> WorkspaceMoveState {
94        self.workspace_move_state
95    }
96
97    pub fn begin_workspace_listing(&mut self) {
98        self.workspace_move_state = WorkspaceMoveState::Listing;
99    }
100
101    pub fn begin_workspace_picking(&mut self) {
102        self.workspace_move_state = WorkspaceMoveState::Picking;
103    }
104
105    pub fn begin_workspace_move(&mut self) {
106        self.workspace_move_state = WorkspaceMoveState::Moving;
107    }
108
109    /// The move landed; the session is being reloaded in the new workspace.
110    pub fn begin_workspace_load(&mut self) {
111        self.workspace_move_state = WorkspaceMoveState::LoadingSession;
112    }
113
114    /// Ends the move flow, wherever it was: a finished load, a failure, or the
115    /// user backing out.
116    pub fn end_workspace_move(&mut self) {
117        self.workspace_move_state = WorkspaceMoveState::Idle;
118    }
119
120    /// Leaves picking mode when the picker closes. Later phases survive an
121    /// overlay close, because the move is already in flight.
122    pub fn cancel_workspace_picking(&mut self) {
123        if self.workspace_move_state == WorkspaceMoveState::Picking {
124            self.workspace_move_state = WorkspaceMoveState::Idle;
125        }
126    }
127
128    pub fn server_statuses(&self) -> &[McpServerStatusEntry] {
129        &self.server_statuses
130    }
131
132    pub fn unhealthy_server_count(&self) -> usize {
133        self.server_statuses.iter().filter(|server| !matches!(server.status, McpServerStatus::Connected { .. })).count()
134    }
135
136    pub fn update_server_statuses(&mut self, statuses: &[McpServerStatusEntry]) {
137        self.server_statuses = statuses.to_vec();
138    }
139
140    pub fn set_auth_methods(&mut self, auth_methods: &[acp::AuthMethod]) {
141        self.auth_methods = auth_methods.to_vec();
142    }
143
144    /// Reconciliation policy: the agent's update replaces local state
145    /// wholesale. Optimistic edits made through `update_config_option_value`
146    /// are either confirmed or corrected by the next update.
147    pub fn update_config_options(&mut self, config_options: Vec<acp::SessionConfigOption>) {
148        let next = config_options.into_iter().map(LocalConfigOption::from_acp).collect::<Vec<_>>();
149        self.config_options = next;
150    }
151
152    pub fn update_config_option_value(&mut self, config_id: &str, value: &str) {
153        if let Some(option) = self.config_options.iter_mut().find(|option| option.id == config_id)
154            && let LocalConfigKind::Select { current_value, .. } = &mut option.kind
155        {
156            current_value.clear();
157            current_value.push_str(value);
158        }
159    }
160
161
162    pub fn set_session(&mut self, session_id: SessionId, config_options: Vec<acp::SessionConfigOption>) {
163        self.session_id = session_id;
164        self.config_options = config_options.into_iter().map(LocalConfigOption::from_acp).collect();
165    }
166
167    pub fn set_working_dir(&mut self, working_dir: PathBuf) {
168        self.working_dir = working_dir;
169    }
170
171    pub fn set_workspace_status(&mut self, workspace_status: WorkspaceStatus) {
172        self.workspace_status = workspace_status;
173    }
174}