1use super::*;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
6#[serde(rename_all = "kebab-case")]
7pub enum ResumeQueueDisposition {
8 Start,
9 Discard,
10}
11
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(deny_unknown_fields)]
14pub struct MoveSelection {
15 #[serde(default)]
16 pub clear_resource_allocation: bool,
17 pub session_id: String,
18 pub profile_id: Option<String>,
19 pub target_template_id: Option<String>,
20 pub additional_mounts: Option<Vec<AdditionalMount>>,
21 pub resource_allocation: Option<SessionResourceAllocation>,
22}
23
24#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
31#[serde(deny_unknown_fields)]
32pub struct RawConversionPreview {
33 pub checkout: PathBuf,
36 pub destination: PathBuf,
38 pub branch: Option<String>,
40 pub fetch_url: String,
41 pub push_urls: Vec<String>,
42 pub default_branch: String,
45 pub unpushed_commits: u64,
48 pub staged_files: u64,
49 pub unstaged_files: u64,
50 pub untracked_files: u64,
51 pub untracked_bytes: u64,
52 pub host_checkout_retained: bool,
55}
56
57impl RawConversionPreview {
58 pub fn summary_line(&self) -> String {
61 let push = if self.push_urls.is_empty() {
62 self.fetch_url.clone()
63 } else {
64 self.push_urls.join(", ")
65 };
66 format!(
67 "Clone {} (default branch {}) into {} on branch {}; push to {push}.",
68 self.fetch_url,
69 self.default_branch,
70 self.destination.display(),
71 self.branch.as_deref().unwrap_or("a detached head"),
72 )
73 }
74
75 pub fn warning_lines(&self) -> Vec<String> {
81 let mut lines = Vec::new();
82 let dirty = self.staged_files + self.unstaged_files + self.untracked_files;
83 if dirty > 0 {
84 lines.push(format!(
85 "{} staged, {} unstaged, and {} untracked {} ({}) will be copied into the container. \
86 Ignored files such as build output, .env, and node_modules will not.",
87 self.staged_files,
88 self.unstaged_files,
89 self.untracked_files,
90 if dirty == 1 { "file" } else { "files" },
91 format_conversion_bytes(self.untracked_bytes),
92 ));
93 }
94 if self.unpushed_commits > 0 {
95 let mut line = format!(
96 "{} {} not on {} {} in the checkpoint.",
97 self.unpushed_commits,
98 if self.unpushed_commits == 1 {
99 "commit"
100 } else {
101 "commits"
102 },
103 self.fetch_url,
104 if self.unpushed_commits == 1 {
105 "travels"
106 } else {
107 "travel"
108 },
109 );
110 if self.unpushed_commits > 200 {
111 line.push_str(" That is a large history; consider pushing first.");
112 }
113 lines.push(line);
114 }
115 if self.host_checkout_retained {
116 lines.push(format!(
117 "{} stays on this machine and will no longer track this session. \
118 Edits made in the container do not come back automatically; \
119 push the branch or move the session back.",
120 self.checkout.display(),
121 ));
122 }
123 lines
124 }
125}
126
127fn format_conversion_bytes(bytes: u64) -> String {
131 const KB: f64 = 1024.0;
132 const MB: f64 = KB * 1024.0;
133 let bytes = bytes as f64;
134 if bytes < MB {
135 format!("{:.1} KB", bytes / KB)
136 } else {
137 format!("{:.1} MB", bytes / MB)
138 }
139}
140
141#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
142#[serde(deny_unknown_fields)]
143pub struct MovePreparation {
144 #[serde(default)]
145 pub source_unavailable: bool,
146 #[serde(default)]
149 pub in_place: bool,
150 #[serde(default, skip_serializing_if = "Option::is_none")]
154 pub conversion: Option<Box<RawConversionPreview>>,
155 pub selection: MoveSelection,
156 pub source_profile_id: String,
157 pub source_target_template_id: String,
158 pub cross_harness: bool,
159 pub active: bool,
160 pub queued_commands: Vec<MaterializedQueuedPrompt>,
161 pub fingerprint: String,
162 pub operation_id: String,
163}
164
165#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
166#[serde(deny_unknown_fields)]
167pub struct MoveSessionRequest {
168 pub preparation: MovePreparation,
169 pub queue: Option<ResumeQueueDisposition>,
170 pub acknowledge_interruption: bool,
171}
172
173#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
174#[serde(deny_unknown_fields)]
175pub struct MoveOutcome {
176 pub operation_id: String,
177 pub session_id: String,
178 pub profile_id: String,
179 pub target_template_id: String,
180 pub outcome: String,
181 pub error: Option<String>,
182 pub recovery: Option<String>,
183}
184
185#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
186#[serde(rename_all = "snake_case")]
187pub enum MovePhase {
188 Preparing,
189 ClosingSource,
190 ResumingDestination,
191 StartingQueue,
192 Completed,
193 Failed,
194 Cancelled,
195}
196
197#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
198#[serde(deny_unknown_fields)]
199pub struct MoveOperation {
200 #[serde(default)]
202 pub source_checkpoint_only: bool,
203 #[serde(default)]
206 pub in_place: bool,
207 pub operation_id: String,
208 pub selection: MoveSelection,
209 pub source_profile_id: String,
210 pub source_target_template_id: String,
211 pub source_target: Option<TargetLocator>,
212 pub source_native_session_id: Option<String>,
213 pub source_additional_mounts: Vec<AdditionalMount>,
214 pub source_resource_allocation: Option<SessionResourceAllocation>,
215 pub destination_target: Option<TargetLocator>,
216 pub destination_native_session_id: Option<String>,
217 pub destination_store_id: Option<String>,
218 pub configuration_fingerprint: String,
219 pub checkpoint: Option<CheckpointMetadata>,
220 pub recovery_session: Option<SessionRecord>,
222 pub queue: ResumeQueueDisposition,
223 pub phase: MovePhase,
224 pub queue_admission_started: bool,
226 pub queue_admission_finished: bool,
227 pub cancellation_requested: bool,
228 pub created_at: String,
229 pub updated_at: String,
230 pub error: Option<String>,
231}
232
233impl MoveOperation {
234 pub fn retains_checkpoint(&self) -> bool {
235 !matches!(self.phase, MovePhase::Completed | MovePhase::Cancelled)
236 || (self.queue_admission_started && !self.queue_admission_finished)
237 }
238
239 pub fn is_active(&self) -> bool {
240 matches!(
241 self.phase,
242 MovePhase::Preparing
243 | MovePhase::ClosingSource
244 | MovePhase::ResumingDestination
245 | MovePhase::StartingQueue
246 )
247 }
248}