Skip to main content

wisp/session/
session_model.rs

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