1use super::*;
2
3#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
4#[serde(deny_unknown_fields)]
5pub struct ViewerSnapshot {
6 pub revision: u64,
7 pub generated_at: String,
8 #[serde(default)]
11 pub server_time_ms: i64,
12 #[serde(default, skip_serializing_if = "String::is_empty")]
16 pub server_version: String,
17 #[serde(default, skip_serializing_if = "Vec::is_empty")]
18 pub workspaces: Vec<ViewerWorkspace>,
19 pub sessions: Vec<ViewerSession>,
20 pub profiles: Vec<ViewerProfile>,
21 pub targets: Vec<ViewerTarget>,
22 pub bundles: Vec<ViewerBundle>,
23 #[serde(default)]
26 pub review_config: ViewerReviewConfig,
27 #[serde(default)]
30 pub subagents_enabled: bool,
31 #[serde(default, skip_serializing_if = "Vec::is_empty")]
34 pub capacity: Vec<ViewerTargetCapacity>,
35 #[serde(default, skip_serializing_if = "Vec::is_empty")]
37 pub launch_failures: Vec<ViewerLaunchFailure>,
38}
39
40#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
45pub struct ViewerLaunchFailure {
46 pub id: String,
49 pub workspace_id: String,
50 #[serde(default, skip_serializing_if = "Option::is_none")]
53 pub session_id: Option<String>,
54 #[serde(default, skip_serializing_if = "Option::is_none")]
56 pub error: Option<String>,
57}
58
59impl ViewerSnapshot {
60 pub fn from_config_state(config: &Config, state: &AppState, revision: u64) -> Self {
64 let sessions = state
65 .sessions
66 .values()
67 .map(|session| {
68 let incompatible = config
69 .targets
70 .keys()
71 .filter(|target_id| {
72 crate::controller::resume_compatibility(session, config, target_id).is_err()
73 })
74 .cloned()
75 .collect::<Vec<_>>();
76 let lifecycle = ViewerLifecycleCategory::of(session.state);
77 let project = state.project_identity_session(session);
81 let source = project.project_source(config);
82 let subagent = state.subagents.get(&session.id);
83 let subagent_session_ids = state
84 .subagents
85 .values()
86 .filter(|child| child.parent_session_id == session.id)
87 .map(|child| child.child_session_id.clone())
88 .collect();
89 ViewerSession {
90 capacity_retry: None,
91 id: session.id.clone(),
92 workspace_id: session.workspace_id.clone(),
93 title: session.display_title().to_owned(),
94 subagent_parent_id: subagent.map(|child| child.parent_session_id.clone()),
95 subagent_task_name: subagent.map(|child| child.task_name.clone()),
96 subagent_session_ids,
97 harness_kind: session.harness_kind.id().into(),
98 profile_id: session.last_profile.clone(),
99 bundle_id: session.bundle_id.clone(),
100 target_id: session.target_template_id.clone(),
101 state: session.state.as_str().into(),
102 created_at: session.created_at.clone(),
103 updated_at: session.updated_at.clone(),
104 has_error: session.last_error.is_some()
105 || session.configuration_issue(config).is_some(),
106 configuration_issue: session.configuration_issue(config),
107 launch_error: matches!(
122 session.state,
123 SessionState::Error | SessionState::Stopped
124 )
125 .then(|| session.last_error.clone())
126 .flatten()
127 .or_else(|| session.public_error().map(str::to_owned)),
128 preview: Vec::new(),
129 queued_prompts: Vec::new(),
130 active_user_shells: Vec::new(),
131 background_tasks: Vec::new(),
132 pending_elicitations: Vec::new(),
133 conversation_available: false,
134 prompt_images_supported: false,
135 incompatible_resume_targets: incompatible.clone(),
136 compatible_resume_targets: config
137 .targets
138 .keys()
139 .filter(|target_id| !incompatible.contains(*target_id))
140 .cloned()
141 .collect(),
142 project_label: source.short,
143 project_key: project_key(&source.key),
144 display_location: project.project_target(config, &session.target_template_id),
145 lifecycle,
146 transitioning: session.state.transition_kind().is_some(),
147 latest_event_ordinal: 0,
148 last_activity_at_ms: None,
149 activity_details: None,
150 activity: String::new(),
151 operation: None,
152 move_recovery: None,
153 chat_phase: ViewerChatPhase::default(),
156 is_idle: false,
157 activity_state: None,
158 config_options: Vec::new(),
159 plan_mode_active: None,
160 turn_review: None,
161 available_commands: Vec::new(),
162 capabilities: ViewerSessionCapabilities {
166 open: false,
167 prompt: false,
168 run_shell: false,
169 cancel_turn: false,
170 cancel_operation: false,
171 stop: lifecycle.is_dashboard_visible(),
172 rename: true,
173 resume: !lifecycle.is_dashboard_visible(),
174 move_session: false,
175 set_config: false,
176 set_plan_mode: false,
177 },
178 }
179 })
180 .collect();
181 let profiles = config
182 .enabled_profiles()
183 .map(|(id, profile)| ViewerProfile {
184 id: id.to_owned(),
185 harness_kind: profile.kind.id().into(),
186 quota: None,
187 })
188 .collect();
189 let targets = config
190 .targets
191 .iter()
192 .map(|(id, target)| ViewerTarget {
193 id: id.clone(),
194 kind: target.kind_name().into(),
195 requires_project_directory: matches!(
196 target,
197 TargetTemplate::LocalBare | TargetTemplate::SshBare { .. }
198 ),
199 recent_project_directories: project_history_host(target)
200 .map(|host| {
201 state
202 .project_directories(host)
203 .iter()
204 .map(|directory| directory.to_string_lossy().into_owned())
205 .collect()
206 })
207 .unwrap_or_default(),
208 })
209 .collect();
210 let bundles = config
211 .bundles
212 .iter()
213 .map(|(id, bundle)| ViewerBundle {
214 id: id.clone(),
215 primary_repository: bundle.primary_repo.clone(),
216 repositories: bundle
217 .repositories
218 .iter()
219 .map(|repository| ViewerRepository {
220 id: repository.id.clone(),
221 github: repository.github.clone(),
222 destination: repository.destination.to_string_lossy().into_owned(),
223 })
224 .collect(),
225 })
226 .collect();
227 Self {
228 revision,
229 generated_at: now_unix().to_string(),
230 server_time_ms: mj_core::clock::epoch_millis(),
231 server_version: env!("CARGO_PKG_VERSION").to_owned(),
232 workspaces: Vec::new(),
233 sessions,
234 profiles,
235 targets,
236 bundles,
237 review_config: ViewerReviewConfig {
238 enabled: config.review.enabled,
239 tier: config.review.tier.label().to_owned(),
240 profile: config.review.profile.clone(),
241 },
242 subagents_enabled: config.subagents.enabled,
243 capacity: Vec::new(),
244 launch_failures: Vec::new(),
245 }
246 }
247}
248
249pub(super) fn project_key(identity: &str) -> String {
256 use sha2::Digest as _;
257 let digest = Sha256::digest(identity.as_bytes());
258 mj_core::hex::lower_hex(&digest[..8])
259}
260
261#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
262#[serde(deny_unknown_fields)]
263pub struct ViewerSession {
264 #[serde(default, skip_serializing_if = "Option::is_none")]
265 pub capacity_retry: Option<mj_core::relay::CapacityRetry>,
266 pub id: String,
267 #[serde(default, skip_serializing_if = "String::is_empty")]
268 pub workspace_id: String,
269 pub title: String,
270 #[serde(default, skip_serializing_if = "Option::is_none")]
272 pub subagent_parent_id: Option<String>,
273 #[serde(default, skip_serializing_if = "Option::is_none")]
275 pub subagent_task_name: Option<String>,
276 #[serde(default, skip_serializing_if = "Vec::is_empty")]
278 pub subagent_session_ids: Vec<String>,
279 pub harness_kind: String,
280 pub profile_id: String,
281 pub bundle_id: String,
282 pub target_id: String,
283 pub state: String,
284 pub created_at: String,
285 pub updated_at: String,
286 pub has_error: bool,
287 #[serde(default, skip_serializing_if = "Option::is_none")]
289 pub configuration_issue: Option<String>,
290 #[serde(default, skip_serializing_if = "Option::is_none")]
295 pub launch_error: Option<String>,
296
297 #[serde(default, skip_serializing_if = "Vec::is_empty")]
298 pub preview: Vec<String>,
299 #[serde(default, skip_serializing_if = "Vec::is_empty")]
300 pub queued_prompts: Vec<ViewerQueuedPrompt>,
301 #[serde(default, skip_serializing_if = "Vec::is_empty")]
302 pub active_user_shells: Vec<ViewerUserShell>,
303 #[serde(default, skip_serializing_if = "Vec::is_empty")]
304 pub background_tasks: Vec<ViewerBackgroundTask>,
305 #[serde(default, skip_serializing_if = "Vec::is_empty")]
309 pub pending_elicitations: Vec<ElicitationRequest>,
310 pub conversation_available: bool,
311 #[serde(default)]
315 pub prompt_images_supported: bool,
316 #[serde(default, skip_serializing_if = "Vec::is_empty")]
323 pub incompatible_resume_targets: Vec<String>,
324 #[serde(default, skip_serializing_if = "Vec::is_empty")]
327 pub compatible_resume_targets: Vec<String>,
328 #[serde(default, skip_serializing_if = "String::is_empty")]
331 pub project_label: String,
332 #[serde(default, skip_serializing_if = "String::is_empty")]
336 pub project_key: String,
337 #[serde(default)]
340 pub display_location: String,
341 pub lifecycle: ViewerLifecycleCategory,
342 #[serde(default)]
346 pub transitioning: bool,
347 #[serde(default)]
351 pub latest_event_ordinal: u64,
352 #[serde(default, skip_serializing_if = "Option::is_none")]
356 pub last_activity_at_ms: Option<i64>,
357 #[serde(default, skip_serializing_if = "Option::is_none")]
360 pub activity_details: Option<ViewerActivityDetails>,
361 #[serde(default, skip_serializing_if = "Option::is_none")]
362 pub operation: Option<ViewerOperation>,
363 #[serde(default, skip_serializing_if = "Option::is_none")]
367 pub move_recovery: Option<ViewerMoveRecovery>,
368 #[serde(default)]
369 pub chat_phase: ViewerChatPhase,
370 #[serde(default)]
373 pub is_idle: bool,
374 #[serde(default, skip_serializing_if = "Option::is_none")]
379 pub activity_state: Option<mj_core::activity::ActivityState>,
380 #[serde(default, skip_serializing_if = "String::is_empty")]
383 pub activity: String,
384 #[serde(default, skip_serializing_if = "Vec::is_empty")]
386 pub config_options: Vec<ViewerConfigOption>,
387 #[serde(default, skip_serializing_if = "Option::is_none")]
389 pub plan_mode_active: Option<bool>,
390 #[serde(default, skip_serializing_if = "Option::is_none")]
393 pub turn_review: Option<ViewerTurnReview>,
394 #[serde(default, skip_serializing_if = "Vec::is_empty")]
398 pub available_commands: Vec<ViewerMjCommand>,
399 pub capabilities: ViewerSessionCapabilities,
400}
401
402#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
403#[serde(deny_unknown_fields)]
404pub struct ViewerMoveRecovery {
405 pub operation_id: String,
406 pub source_profile_id: String,
407 pub source_target_template_id: String,
408 pub destination_profile_id: String,
409 pub destination_target_template_id: String,
410 pub phase: String,
411 pub queue: String,
412 pub clear_resource_allocation: bool,
413 #[serde(default)]
416 pub source_additional_mounts: Vec<AdditionalMount>,
417 #[serde(default)]
418 pub source_resource_allocation: Option<SessionResourceAllocation>,
419 #[serde(default)]
422 pub destination_additional_mounts: Vec<AdditionalMount>,
423 #[serde(default)]
424 pub destination_resource_allocation: Option<SessionResourceAllocation>,
425 pub checkpoint_retained: bool,
426 pub destination_ready: bool,
427 pub queue_admission_started: bool,
428 pub queue_admission_finished: bool,
429}
430
431impl ViewerMoveRecovery {
432 #[must_use]
433 pub fn from_operation(operation: &MoveOperation) -> Option<Self> {
434 if matches!(operation.phase, MovePhase::Completed) {
435 return None;
436 }
437 Some(Self {
438 operation_id: operation.operation_id.clone(),
439 source_profile_id: operation.source_profile_id.clone(),
440 source_target_template_id: operation.source_target_template_id.clone(),
441 destination_profile_id: operation.selection.profile_id.clone().unwrap_or_default(),
442 destination_target_template_id: operation
443 .selection
444 .target_template_id
445 .clone()
446 .unwrap_or_default(),
447 phase: match operation.phase {
448 MovePhase::Preparing => "preparing",
449 MovePhase::ClosingSource => "closing_source",
450 MovePhase::ResumingDestination => "resuming_destination",
451 MovePhase::StartingQueue => "starting_queue",
452 MovePhase::Completed => "completed",
453 MovePhase::Failed => "failed",
454 MovePhase::Cancelled => "cancelled",
455 }
456 .into(),
457 queue: match operation.queue {
458 ResumeQueueDisposition::Start => "start",
459 ResumeQueueDisposition::Discard => "discard",
460 }
461 .into(),
462 clear_resource_allocation: operation.selection.clear_resource_allocation,
463 source_additional_mounts: operation.source_additional_mounts.clone(),
464 source_resource_allocation: operation.source_resource_allocation.clone(),
465 destination_additional_mounts: operation
466 .selection
467 .additional_mounts
468 .clone()
469 .unwrap_or_default(),
470 destination_resource_allocation: operation.selection.resource_allocation.clone(),
471 checkpoint_retained: operation.checkpoint.is_some(),
472 destination_ready: operation.destination_target.is_some()
473 && operation.destination_native_session_id.is_some(),
474 queue_admission_started: operation.queue_admission_started,
475 queue_admission_finished: operation.queue_admission_finished,
476 })
477 }
478}
479
480impl ViewerSession {
481 pub fn set_project_source(&mut self, source: &ProjectSourceIdentity) {
484 self.project_label = source.short.clone();
485 self.project_key = project_key(&source.key);
486 }
487}
488
489pub use crate::database::{
491 ApiActivityDetails as ViewerActivityDetails, ApiActivityKind as ViewerActivityKind,
492};
493
494#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
496#[serde(deny_unknown_fields)]
497pub struct ViewerMjCommand {
498 pub name: String,
499 pub description: String,
500 pub source: ViewerCommandSource,
503 #[serde(default, skip_serializing_if = "Option::is_none")]
505 pub argument: Option<String>,
506}
507
508#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
509#[serde(rename_all = "snake_case")]
510pub enum ViewerCommandSource {
511 Mj,
512 Agent,
513}
514
515#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
517#[serde(deny_unknown_fields)]
518pub struct ViewerReviewConfig {
519 pub enabled: bool,
520 pub tier: String,
521 #[serde(default, skip_serializing_if = "Option::is_none")]
522 pub profile: Option<String>,
523}
524
525#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
527#[serde(deny_unknown_fields)]
528pub struct ViewerTurnReview {
529 pub tier: String,
531 pub status: String,
533 #[serde(default, skip_serializing_if = "Vec::is_empty")]
535 pub roles: Vec<ViewerReviewRole>,
536 #[serde(default, skip_serializing_if = "Option::is_none")]
538 pub verdict: Option<ViewerReviewVerdict>,
539}
540
541#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
542#[serde(deny_unknown_fields)]
543pub struct ViewerReviewRole {
544 pub label: String,
545 pub state: String,
547}
548
549#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
550#[serde(deny_unknown_fields)]
551pub struct ViewerReviewVerdict {
552 pub kind: String,
554 pub text: String,
556 #[serde(default, skip_serializing_if = "Vec::is_empty")]
560 pub allowed: Vec<String>,
561}
562
563impl ViewerTurnReview {
564 #[must_use]
566 pub fn from_runtime(review: &crate::review_host::RuntimeReviewView) -> Self {
567 Self {
568 tier: review.tier.label().to_owned(),
569 status: review.status.clone(),
570 roles: review
571 .roles
572 .iter()
573 .map(|role| ViewerReviewRole {
574 label: role.label.clone(),
575 state: role.state.label().to_owned(),
576 })
577 .collect(),
578 verdict: review.verdict.as_ref().map(|verdict| ViewerReviewVerdict {
579 kind: match verdict.kind {
580 crate::review_host::VerdictKind::Clean => "clean",
581 crate::review_host::VerdictKind::Findings => "findings",
582 crate::review_host::VerdictKind::Failed => "failed",
583 }
584 .to_owned(),
585 text: verdict.text.clone(),
586 allowed: verdict
587 .allowed
588 .iter()
589 .filter_map(resolution_name)
590 .map(str::to_owned)
591 .collect(),
592 }),
593 }
594 }
595}
596
597#[must_use]
600pub fn resolution_name(resolution: &mj_core::review::driver::Resolution) -> Option<&'static str> {
601 match resolution {
602 mj_core::review::driver::Resolution::Forwarded => Some("forward"),
603 mj_core::review::driver::Resolution::Dismissed => Some("dismiss"),
604 mj_core::review::driver::Resolution::Cancelled => Some("cancel"),
605 mj_core::review::driver::Resolution::NothingToReview
607 | mj_core::review::driver::Resolution::CoverageStarted => None,
608 }
609}
610
611#[must_use]
613pub fn resolution_from_name(name: &str) -> Option<mj_core::review::driver::Resolution> {
614 match name {
615 "forward" => Some(mj_core::review::driver::Resolution::Forwarded),
616 "dismiss" => Some(mj_core::review::driver::Resolution::Dismissed),
617 "cancel" => Some(mj_core::review::driver::Resolution::Cancelled),
618 _ => None,
619 }
620}
621
622#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
623#[serde(deny_unknown_fields)]
624pub struct ViewerWorkspace {
625 pub id: String,
626 pub name: String,
627}
628
629#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
630#[serde(deny_unknown_fields)]
631pub struct ViewerQueuedPrompt {
632 pub id: String,
633 pub text: String,
634 pub created_at: String,
635}
636
637#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
638#[serde(deny_unknown_fields)]
639pub struct ViewerUserShell {
640 pub id: String,
641 pub command: String,
642 pub started_at_ms: Option<i64>,
643}
644
645#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
647#[serde(deny_unknown_fields)]
648pub struct ViewerBackgroundTask {
649 pub id: String,
650 pub command: String,
651 pub started_at_ms: i64,
652 pub can_stop: bool,
653}
654
655#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
656#[serde(deny_unknown_fields)]
657pub struct ViewerProfile {
658 pub id: String,
659 pub harness_kind: String,
660 #[serde(default, skip_serializing_if = "Option::is_none")]
661 pub quota: Option<ViewerQuota>,
662}
663
664#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
670#[serde(deny_unknown_fields)]
671pub struct ViewerQuotaWindow {
672 pub label: String,
673 #[serde(default, skip_serializing_if = "Option::is_none")]
674 pub percent_used: Option<u8>,
675 #[serde(default, skip_serializing_if = "Option::is_none")]
676 pub resets_at: Option<String>,
677 pub projects_exhaustion_before_reset: bool,
680}
681
682#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
683#[serde(deny_unknown_fields)]
684pub struct ViewerQuota {
685 pub summary: String,
688 #[serde(default, skip_serializing_if = "Vec::is_empty")]
689 pub windows: Vec<ViewerQuotaWindow>,
690 #[serde(default, skip_serializing_if = "Option::is_none")]
691 pub resets_at: Option<String>,
692 pub stale: bool,
693 #[serde(default)]
696 pub refreshed_at_epoch_seconds: u64,
697 pub has_error: bool,
700}
701
702#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
708#[serde(deny_unknown_fields)]
709pub struct ViewerTargetCapacity {
710 pub id: String,
711 pub label: String,
714 pub target_ids: Vec<String>,
715 #[serde(default, skip_serializing_if = "Option::is_none")]
716 pub cpu_percent: Option<u8>,
717 #[serde(default, skip_serializing_if = "Option::is_none")]
718 pub memory_used_bytes: Option<u64>,
719 #[serde(default, skip_serializing_if = "Option::is_none")]
720 pub memory_total_bytes: Option<u64>,
721 #[serde(default, skip_serializing_if = "Option::is_none")]
722 pub logical_cores: Option<u64>,
723 #[serde(default, skip_serializing_if = "Option::is_none")]
724 pub disk_total_bytes: Option<u64>,
725 #[serde(default, skip_serializing_if = "Option::is_none")]
727 pub virtual_machines: Option<u64>,
728 #[serde(default, skip_serializing_if = "Option::is_none")]
729 pub sampled_at_epoch_seconds: Option<u64>,
730 pub refreshing: bool,
731 pub stale: bool,
732 pub has_error: bool,
735}
736
737#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
738#[serde(deny_unknown_fields)]
739pub struct ViewerTarget {
740 pub id: String,
741 pub kind: String,
742 pub requires_project_directory: bool,
743 #[serde(default)]
747 pub recent_project_directories: Vec<String>,
748}
749
750#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
751#[serde(deny_unknown_fields)]
752pub struct ViewerBundle {
753 pub id: String,
754 pub primary_repository: String,
755 pub repositories: Vec<ViewerRepository>,
756}
757
758#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
759#[serde(deny_unknown_fields)]
760pub struct ViewerRepository {
761 pub id: String,
762 pub github: Option<String>,
763 pub destination: String,
764}
765
766#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
774#[serde(deny_unknown_fields)]
775pub struct ViewerSessionCapabilities {
776 pub open: bool,
777 pub prompt: bool,
778 pub run_shell: bool,
779 pub cancel_turn: bool,
781 pub cancel_operation: bool,
783 pub stop: bool,
784 pub rename: bool,
785 pub resume: bool,
786 #[serde(default)]
789 pub move_session: bool,
790 pub set_config: bool,
791 pub set_plan_mode: bool,
792}
793
794#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
800#[serde(rename_all = "kebab-case")]
801pub enum ViewerLifecycleCategory {
802 Live,
803 Starting,
804 Stopping,
805 Stopped,
806 Failed,
807}
808
809impl ViewerLifecycleCategory {
810 pub(super) const fn of(state: SessionState) -> Self {
811 match state {
812 SessionState::Provisioning => Self::Starting,
813 SessionState::Running | SessionState::Disconnected | SessionState::Checkpointing => {
814 Self::Live
815 }
816 SessionState::Closing | SessionState::Destroying => Self::Stopping,
817 SessionState::Stopped => Self::Stopped,
818 SessionState::Lost | SessionState::Error | SessionState::DestroyedWithDataLoss => {
819 Self::Failed
820 }
821 }
822 }
823
824 pub const fn is_dashboard_visible(self) -> bool {
828 matches!(self, Self::Live | Self::Starting | Self::Stopping)
829 }
830}
831
832#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
833#[serde(rename_all = "kebab-case")]
834pub enum ViewerOperationKind {
835 Create,
836 Resume,
837 Move,
838 Stop,
839 Destroy,
840 Cleanup,
841 Checkpoint,
842}
843
844impl ViewerOperationKind {
845 pub const fn transition_kind(self) -> Option<SessionTransitionKind> {
846 match self {
847 Self::Create => Some(SessionTransitionKind::Starting),
848 Self::Resume => Some(SessionTransitionKind::Resuming),
849 Self::Move => Some(SessionTransitionKind::Moving),
850 Self::Stop => Some(SessionTransitionKind::Stopping),
851 Self::Destroy | Self::Cleanup => Some(SessionTransitionKind::Destroying),
852 Self::Checkpoint => None,
855 }
856 }
857}
858
859#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
861#[serde(deny_unknown_fields)]
862pub struct ViewerOperationStage {
863 pub label: String,
864 pub started_at_epoch_seconds: u64,
865}
866
867#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
873#[serde(deny_unknown_fields)]
874pub struct ViewerOperation {
875 pub id: String,
876 pub session_id: String,
877 pub kind: ViewerOperationKind,
878 pub started_at_epoch_seconds: u64,
879 #[serde(default, skip_serializing_if = "Vec::is_empty")]
880 pub stages: Vec<ViewerOperationStage>,
881 #[serde(default, skip_serializing_if = "Option::is_none")]
884 pub notice: Option<String>,
885 pub cancellable: bool,
886}
887
888#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
890#[serde(rename_all = "kebab-case")]
891pub enum ViewerChatPhase {
892 #[default]
893 Idle,
894 Running,
895 Closing,
896 Closed,
897}
898
899#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
900#[serde(deny_unknown_fields)]
901pub struct ViewerConfigChoice {
902 pub value: String,
903 pub name: String,
904 #[serde(default, skip_serializing_if = "Option::is_none")]
905 pub description: Option<String>,
906}
907
908#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
914#[serde(deny_unknown_fields)]
915pub struct ViewerConfigOption {
916 pub key: String,
917 pub label: String,
918 #[serde(default, skip_serializing_if = "Option::is_none")]
919 pub current: Option<String>,
920 pub choices: Vec<ViewerConfigChoice>,
921}