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 targeted_turn_control_supported: false,
91 native_subagents: Vec::new(),
92 steering: None,
93 active_prompt_id: None,
94 cancelling_prompt_id: None,
95 capacity_retry: None,
96 quota_recovery: None,
97 id: session.id.clone(),
98 workspace_id: session.workspace_id.clone(),
99 title: session.display_title().to_owned(),
100 subagent_parent_id: subagent.map(|child| child.parent_session_id.clone()),
101 subagent_task_name: subagent.map(|child| child.task_name.clone()),
102 subagent_session_ids,
103 harness_kind: session.harness_kind.id().into(),
104 profile_id: session.last_profile.clone(),
105 bundle_id: session.bundle_id.clone(),
106 target_id: session.target_template_id.clone(),
107 state: match session.state {
108 SessionState::Closing => "suspending",
109 SessionState::Stopped => "suspended",
110 _ => session.state.as_str(),
111 }
112 .into(),
113 created_at: session.created_at.clone(),
114 updated_at: session.updated_at.clone(),
115 has_error: session.last_error.is_some()
116 || session.configuration_issue(config).is_some(),
117 configuration_issue: session.configuration_issue(config),
118 launch_error: matches!(
133 session.state,
134 SessionState::Error | SessionState::Stopped
135 )
136 .then(|| session.last_error.clone())
137 .flatten()
138 .or_else(|| session.public_error().map(str::to_owned)),
139 preview: Vec::new(),
140 queued_prompts: Vec::new(),
141 active_user_shells: Vec::new(),
142 background_tasks: Vec::new(),
143 pending_elicitations: Vec::new(),
144 conversation_available: false,
145 prompt_images_supported: false,
146 incompatible_resume_targets: incompatible.clone(),
147 compatible_resume_targets: config
148 .targets
149 .keys()
150 .filter(|target_id| !incompatible.contains(*target_id))
151 .cloned()
152 .collect(),
153 project_label: source.short,
154 project_key: project_key(&source.key),
155 display_location: project.project_target(config, &session.target_template_id),
156 lifecycle,
157 transitioning: session.state.transition_kind().is_some(),
158 latest_event_ordinal: 0,
159 last_activity_at_ms: None,
160 activity_details: None,
161 activity: String::new(),
162 operation: None,
163 move_recovery: None,
164 chat_phase: ViewerChatPhase::default(),
167 is_idle: false,
168 activity_state: None,
169 config_options: Vec::new(),
170 plan_mode_active: None,
171 turn_review: None,
172 available_commands: Vec::new(),
173 capabilities: ViewerSessionCapabilities {
177 clear_context: false,
178 open: false,
179 prompt: false,
180 run_shell: false,
181 interrupt_turn: false,
182 cancel_operation: false,
183 suspend: lifecycle.is_dashboard_visible(),
184 destroy: true,
185 rename: true,
186 resume: !lifecycle.is_dashboard_visible(),
187 move_session: false,
188 set_config: false,
189 set_plan_mode: false,
190 },
191 }
192 })
193 .collect();
194 let profiles = config
195 .enabled_profiles()
196 .map(|(id, profile)| ViewerProfile {
197 id: id.to_owned(),
198 harness_kind: profile.kind.id().into(),
199 quota: None,
200 })
201 .collect();
202 let targets = config
203 .targets
204 .iter()
205 .map(|(id, target)| ViewerTarget {
206 id: id.clone(),
207 kind: target.kind_name().into(),
208 requires_project_directory: matches!(
209 target,
210 TargetTemplate::LocalBare | TargetTemplate::SshBare { .. }
211 ),
212 recent_project_directories: project_history_host(target)
213 .map(|host| {
214 state
215 .project_directories(host)
216 .iter()
217 .map(|directory| directory.to_string_lossy().into_owned())
218 .collect()
219 })
220 .unwrap_or_default(),
221 })
222 .collect();
223 let bundles = config
224 .bundles
225 .iter()
226 .map(|(id, bundle)| ViewerBundle {
227 id: id.clone(),
228 primary_repository: bundle.primary_repo.clone(),
229 repositories: bundle
230 .repositories
231 .iter()
232 .map(|repository| ViewerRepository {
233 id: repository.id.clone(),
234 github: repository.github.clone(),
235 destination: repository.destination.to_string_lossy().into_owned(),
236 })
237 .collect(),
238 })
239 .collect();
240 Self {
241 revision,
242 generated_at: now_unix().to_string(),
243 server_time_ms: mj_core::clock::epoch_millis(),
244 server_version: env!("CARGO_PKG_VERSION").to_owned(),
245 workspaces: Vec::new(),
246 sessions,
247 profiles,
248 targets,
249 bundles,
250 review_config: ViewerReviewConfig {
251 enabled: config.review.enabled,
252 tier: config.review.tier.label().to_owned(),
253 profile: config.review.profile.clone(),
254 },
255 subagents_enabled: config.subagents.enabled,
256 capacity: Vec::new(),
257 launch_failures: Vec::new(),
258 }
259 }
260}
261
262pub(super) fn project_key(identity: &str) -> String {
269 use sha2::Digest as _;
270 let digest = Sha256::digest(identity.as_bytes());
271 mj_core::hex::lower_hex(&digest[..8])
272}
273
274#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
275#[serde(deny_unknown_fields)]
276pub struct ViewerSession {
277 #[serde(default)]
278 pub targeted_turn_control_supported: bool,
279 #[serde(default, skip_serializing_if = "Vec::is_empty")]
280 pub native_subagents: Vec<mj_core::native_agent::NativeAgent>,
281 #[serde(default, skip_serializing_if = "Option::is_none")]
282 pub steering: Option<mj_core::relay::SteeringOperation>,
283 #[serde(default, skip_serializing_if = "Option::is_none")]
284 pub active_prompt_id: Option<String>,
285 #[serde(default, skip_serializing_if = "Option::is_none")]
286 pub cancelling_prompt_id: Option<String>,
287 #[serde(default, skip_serializing_if = "Option::is_none")]
288 pub capacity_retry: Option<mj_core::relay::CapacityRetry>,
289 #[serde(default, skip_serializing_if = "Option::is_none")]
290 pub quota_recovery: Option<mj_core::continuation::QuotaRecovery>,
291 pub id: String,
292 #[serde(default, skip_serializing_if = "String::is_empty")]
293 pub workspace_id: String,
294 pub title: String,
295 #[serde(default, skip_serializing_if = "Option::is_none")]
297 pub subagent_parent_id: Option<String>,
298 #[serde(default, skip_serializing_if = "Option::is_none")]
300 pub subagent_task_name: Option<String>,
301 #[serde(default, skip_serializing_if = "Vec::is_empty")]
303 pub subagent_session_ids: Vec<String>,
304 pub harness_kind: String,
305 pub profile_id: String,
306 pub bundle_id: String,
307 pub target_id: String,
308 pub state: String,
309 pub created_at: String,
310 pub updated_at: String,
311 pub has_error: bool,
312 #[serde(default, skip_serializing_if = "Option::is_none")]
314 pub configuration_issue: Option<String>,
315 #[serde(default, skip_serializing_if = "Option::is_none")]
320 pub launch_error: Option<String>,
321
322 #[serde(default, skip_serializing_if = "Vec::is_empty")]
323 pub preview: Vec<String>,
324 #[serde(default, skip_serializing_if = "Vec::is_empty")]
325 pub queued_prompts: Vec<ViewerQueuedPrompt>,
326 #[serde(default, skip_serializing_if = "Vec::is_empty")]
327 pub active_user_shells: Vec<ViewerUserShell>,
328 #[serde(default, skip_serializing_if = "Vec::is_empty")]
329 pub background_tasks: Vec<ViewerBackgroundTask>,
330 #[serde(default, skip_serializing_if = "Vec::is_empty")]
334 pub pending_elicitations: Vec<ElicitationRequest>,
335 pub conversation_available: bool,
336 #[serde(default)]
340 pub prompt_images_supported: bool,
341 #[serde(default, skip_serializing_if = "Vec::is_empty")]
348 pub incompatible_resume_targets: Vec<String>,
349 #[serde(default, skip_serializing_if = "Vec::is_empty")]
352 pub compatible_resume_targets: Vec<String>,
353 #[serde(default, skip_serializing_if = "String::is_empty")]
356 pub project_label: String,
357 #[serde(default, skip_serializing_if = "String::is_empty")]
361 pub project_key: String,
362 #[serde(default)]
365 pub display_location: String,
366 pub lifecycle: ViewerLifecycleCategory,
367 #[serde(default)]
371 pub transitioning: bool,
372 #[serde(default)]
376 pub latest_event_ordinal: u64,
377 #[serde(default, skip_serializing_if = "Option::is_none")]
381 pub last_activity_at_ms: Option<i64>,
382 #[serde(default, skip_serializing_if = "Option::is_none")]
385 pub activity_details: Option<ViewerActivityDetails>,
386 #[serde(default, skip_serializing_if = "Option::is_none")]
387 pub operation: Option<ViewerOperation>,
388 #[serde(default, skip_serializing_if = "Option::is_none")]
392 pub move_recovery: Option<ViewerMoveRecovery>,
393 #[serde(default)]
394 pub chat_phase: ViewerChatPhase,
395 #[serde(default)]
398 pub is_idle: bool,
399 #[serde(default, skip_serializing_if = "Option::is_none")]
404 pub activity_state: Option<mj_core::activity::ActivityState>,
405 #[serde(default, skip_serializing_if = "String::is_empty")]
408 pub activity: String,
409 #[serde(default, skip_serializing_if = "Vec::is_empty")]
411 pub config_options: Vec<ViewerConfigOption>,
412 #[serde(default, skip_serializing_if = "Option::is_none")]
414 pub plan_mode_active: Option<bool>,
415 #[serde(default, skip_serializing_if = "Option::is_none")]
418 pub turn_review: Option<ViewerTurnReview>,
419 #[serde(default, skip_serializing_if = "Vec::is_empty")]
423 pub available_commands: Vec<ViewerMjCommand>,
424 pub capabilities: ViewerSessionCapabilities,
425}
426
427#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
428#[serde(deny_unknown_fields)]
429pub struct ViewerMoveRecovery {
430 pub operation_id: String,
431 pub source_profile_id: String,
432 pub source_target_template_id: String,
433 pub destination_profile_id: String,
434 pub destination_target_template_id: String,
435 pub phase: String,
436 pub queue: String,
437 pub clear_resource_allocation: bool,
438 #[serde(default)]
441 pub source_additional_mounts: Vec<AdditionalMount>,
442 #[serde(default)]
443 pub source_resource_allocation: Option<SessionResourceAllocation>,
444 #[serde(default)]
447 pub destination_additional_mounts: Vec<AdditionalMount>,
448 #[serde(default)]
449 pub destination_resource_allocation: Option<SessionResourceAllocation>,
450 pub checkpoint_retained: bool,
451 pub destination_ready: bool,
452 pub queue_admission_started: bool,
453 pub queue_admission_finished: bool,
454}
455
456impl ViewerMoveRecovery {
457 #[must_use]
458 pub fn from_operation(operation: &MoveOperation) -> Option<Self> {
459 if matches!(operation.phase, MovePhase::Completed) {
460 return None;
461 }
462 Some(Self {
463 operation_id: operation.operation_id.clone(),
464 source_profile_id: operation.source_profile_id.clone(),
465 source_target_template_id: operation.source_target_template_id.clone(),
466 destination_profile_id: operation.selection.profile_id.clone().unwrap_or_default(),
467 destination_target_template_id: operation
468 .selection
469 .target_template_id
470 .clone()
471 .unwrap_or_default(),
472 phase: match operation.phase {
473 MovePhase::Preparing => "preparing",
474 MovePhase::ClosingSource => "closing_source",
475 MovePhase::ResumingDestination => "resuming_destination",
476 MovePhase::StartingQueue => "starting_queue",
477 MovePhase::Completed => "completed",
478 MovePhase::Failed => "failed",
479 MovePhase::Cancelled => "cancelled",
480 }
481 .into(),
482 queue: match operation.queue {
483 ResumeQueueDisposition::Start => "start",
484 ResumeQueueDisposition::Discard => "discard",
485 }
486 .into(),
487 clear_resource_allocation: operation.selection.clear_resource_allocation,
488 source_additional_mounts: operation.source_additional_mounts.clone(),
489 source_resource_allocation: operation.source_resource_allocation.clone(),
490 destination_additional_mounts: operation
491 .selection
492 .additional_mounts
493 .clone()
494 .unwrap_or_default(),
495 destination_resource_allocation: operation.selection.resource_allocation.clone(),
496 checkpoint_retained: operation.checkpoint.is_some(),
497 destination_ready: operation.destination_target.is_some()
498 && operation.destination_native_session_id.is_some(),
499 queue_admission_started: operation.queue_admission_started,
500 queue_admission_finished: operation.queue_admission_finished,
501 })
502 }
503}
504
505impl ViewerSession {
506 pub fn set_project_source(&mut self, source: &ProjectSourceIdentity) {
509 self.project_label = source.short.clone();
510 self.project_key = project_key(&source.key);
511 }
512}
513
514pub use crate::database::{
516 ApiActivityDetails as ViewerActivityDetails, ApiActivityKind as ViewerActivityKind,
517};
518
519#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
521#[serde(deny_unknown_fields)]
522pub struct ViewerMjCommand {
523 pub name: String,
524 pub description: String,
525 pub source: ViewerCommandSource,
528 #[serde(default, skip_serializing_if = "Option::is_none")]
530 pub argument: Option<String>,
531}
532
533#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
534#[serde(rename_all = "snake_case")]
535pub enum ViewerCommandSource {
536 Mj,
537 Agent,
538}
539
540#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
542#[serde(deny_unknown_fields)]
543pub struct ViewerReviewConfig {
544 pub enabled: bool,
545 pub tier: String,
546 #[serde(default, skip_serializing_if = "Option::is_none")]
547 pub profile: Option<String>,
548}
549
550#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
552#[serde(deny_unknown_fields)]
553pub struct ViewerTurnReview {
554 pub tier: String,
556 pub status: String,
558 #[serde(default, skip_serializing_if = "Vec::is_empty")]
560 pub roles: Vec<ViewerReviewRole>,
561 #[serde(default, skip_serializing_if = "Option::is_none")]
563 pub verdict: Option<ViewerReviewVerdict>,
564}
565
566#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
567#[serde(deny_unknown_fields)]
568pub struct ViewerReviewRole {
569 pub label: String,
570 pub state: String,
572}
573
574#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
575#[serde(deny_unknown_fields)]
576pub struct ViewerReviewVerdict {
577 pub kind: String,
579 pub text: String,
581 #[serde(default, skip_serializing_if = "Vec::is_empty")]
585 pub allowed: Vec<String>,
586}
587
588impl ViewerTurnReview {
589 #[must_use]
591 pub fn from_runtime(review: &crate::review_host::RuntimeReviewView) -> Self {
592 Self {
593 tier: review.tier.label().to_owned(),
594 status: review.status.clone(),
595 roles: review
596 .roles
597 .iter()
598 .map(|role| ViewerReviewRole {
599 label: role.label.clone(),
600 state: role.state.label().to_owned(),
601 })
602 .collect(),
603 verdict: review.verdict.as_ref().map(|verdict| ViewerReviewVerdict {
604 kind: match verdict.kind {
605 crate::review_host::VerdictKind::Clean => "clean",
606 crate::review_host::VerdictKind::Findings => "findings",
607 crate::review_host::VerdictKind::Failed => "failed",
608 }
609 .to_owned(),
610 text: verdict.text.clone(),
611 allowed: verdict
612 .allowed
613 .iter()
614 .filter_map(resolution_name)
615 .map(str::to_owned)
616 .collect(),
617 }),
618 }
619 }
620}
621
622#[must_use]
625pub fn resolution_name(resolution: &mj_core::review::driver::Resolution) -> Option<&'static str> {
626 match resolution {
627 mj_core::review::driver::Resolution::Forwarded => Some("forward"),
628 mj_core::review::driver::Resolution::Dismissed => Some("dismiss"),
629 mj_core::review::driver::Resolution::Cancelled => Some("cancel"),
630 mj_core::review::driver::Resolution::NothingToReview
632 | mj_core::review::driver::Resolution::CoverageStarted => None,
633 }
634}
635
636#[must_use]
638pub fn resolution_from_name(name: &str) -> Option<mj_core::review::driver::Resolution> {
639 match name {
640 "forward" => Some(mj_core::review::driver::Resolution::Forwarded),
641 "dismiss" => Some(mj_core::review::driver::Resolution::Dismissed),
642 "cancel" => Some(mj_core::review::driver::Resolution::Cancelled),
643 _ => None,
644 }
645}
646
647#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
648#[serde(deny_unknown_fields)]
649pub struct ViewerWorkspace {
650 pub id: String,
651 pub name: String,
652}
653
654#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
655#[serde(deny_unknown_fields)]
656pub struct ViewerQueuedPrompt {
657 pub id: String,
658 pub text: String,
659 pub created_at: String,
660}
661
662#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
663#[serde(deny_unknown_fields)]
664pub struct ViewerUserShell {
665 pub id: String,
666 pub command: String,
667 pub started_at_ms: Option<i64>,
668}
669
670#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
672#[serde(deny_unknown_fields)]
673pub struct ViewerBackgroundTask {
674 pub id: String,
675 pub command: String,
676 pub started_at_ms: i64,
677 pub can_stop: bool,
678}
679
680#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
681#[serde(deny_unknown_fields)]
682pub struct ViewerProfile {
683 pub id: String,
684 pub harness_kind: String,
685 #[serde(default, skip_serializing_if = "Option::is_none")]
686 pub quota: Option<ViewerQuota>,
687}
688
689#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
695#[serde(deny_unknown_fields)]
696pub struct ViewerQuotaWindow {
697 pub label: String,
698 #[serde(default, skip_serializing_if = "Option::is_none")]
699 pub percent_used: Option<u8>,
700 #[serde(default, skip_serializing_if = "Option::is_none")]
701 pub resets_at: Option<String>,
702 pub projects_exhaustion_before_reset: bool,
705}
706
707#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
708#[serde(deny_unknown_fields)]
709pub struct ViewerQuota {
710 pub summary: String,
713 #[serde(default, skip_serializing_if = "Vec::is_empty")]
714 pub windows: Vec<ViewerQuotaWindow>,
715 #[serde(default, skip_serializing_if = "Option::is_none")]
716 pub resets_at: Option<String>,
717 pub stale: bool,
718 #[serde(default)]
721 pub refreshed_at_epoch_seconds: u64,
722 pub has_error: bool,
725}
726
727#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
733#[serde(deny_unknown_fields)]
734pub struct ViewerTargetCapacity {
735 pub id: String,
736 pub label: String,
739 pub target_ids: Vec<String>,
740 #[serde(default, skip_serializing_if = "Option::is_none")]
741 pub cpu_percent: Option<u8>,
742 #[serde(default, skip_serializing_if = "Option::is_none")]
743 pub memory_used_bytes: Option<u64>,
744 #[serde(default, skip_serializing_if = "Option::is_none")]
745 pub memory_total_bytes: Option<u64>,
746 #[serde(default, skip_serializing_if = "Option::is_none")]
747 pub logical_cores: Option<u64>,
748 #[serde(default, skip_serializing_if = "Option::is_none")]
749 pub disk_total_bytes: Option<u64>,
750 #[serde(default, skip_serializing_if = "Option::is_none")]
752 pub virtual_machines: Option<u64>,
753 #[serde(default, skip_serializing_if = "Option::is_none")]
754 pub sampled_at_epoch_seconds: Option<u64>,
755 pub refreshing: bool,
756 pub stale: bool,
757 pub has_error: bool,
760}
761
762#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
763#[serde(deny_unknown_fields)]
764pub struct ViewerTarget {
765 pub id: String,
766 pub kind: String,
767 pub requires_project_directory: bool,
768 #[serde(default)]
772 pub recent_project_directories: Vec<String>,
773}
774
775#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
776#[serde(deny_unknown_fields)]
777pub struct ViewerBundle {
778 pub id: String,
779 pub primary_repository: String,
780 pub repositories: Vec<ViewerRepository>,
781}
782
783#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
784#[serde(deny_unknown_fields)]
785pub struct ViewerRepository {
786 pub id: String,
787 pub github: Option<String>,
788 pub destination: String,
789}
790
791#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
799#[serde(deny_unknown_fields)]
800pub struct ViewerSessionCapabilities {
801 #[serde(default)]
802 pub clear_context: bool,
803 pub open: bool,
804 pub prompt: bool,
805 pub run_shell: bool,
806 pub interrupt_turn: bool,
808 pub cancel_operation: bool,
810 pub suspend: bool,
811 pub destroy: bool,
812 pub rename: bool,
813 pub resume: bool,
814 #[serde(default)]
817 pub move_session: bool,
818 pub set_config: bool,
819 pub set_plan_mode: bool,
820}
821
822#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
828#[serde(rename_all = "kebab-case")]
829pub enum ViewerLifecycleCategory {
830 Live,
831 Starting,
832 Suspending,
833 Suspended,
834 Failed,
835}
836
837impl ViewerLifecycleCategory {
838 pub(super) const fn of(state: SessionState) -> Self {
839 match state {
840 SessionState::Provisioning => Self::Starting,
841 SessionState::Running | SessionState::Disconnected | SessionState::Checkpointing => {
842 Self::Live
843 }
844 SessionState::Closing | SessionState::Destroying => Self::Suspending,
845 SessionState::Stopped => Self::Suspended,
846 SessionState::Lost | SessionState::Error | SessionState::DestroyedWithDataLoss => {
847 Self::Failed
848 }
849 }
850 }
851
852 pub const fn is_dashboard_visible(self) -> bool {
856 matches!(self, Self::Live | Self::Starting | Self::Suspending)
857 }
858}
859
860#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
861#[serde(rename_all = "kebab-case")]
862pub enum ViewerOperationKind {
863 Create,
864 Resume,
865 Move,
866 Suspend,
867 Destroy,
868 Cleanup,
869 Checkpoint,
870}
871
872impl ViewerOperationKind {
873 pub const fn transition_kind(self) -> Option<SessionTransitionKind> {
874 match self {
875 Self::Create => Some(SessionTransitionKind::Starting),
876 Self::Resume => Some(SessionTransitionKind::Resuming),
877 Self::Move => Some(SessionTransitionKind::Moving),
878 Self::Suspend => Some(SessionTransitionKind::Suspending),
879 Self::Destroy | Self::Cleanup => Some(SessionTransitionKind::Destroying),
880 Self::Checkpoint => None,
883 }
884 }
885}
886
887#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
889#[serde(deny_unknown_fields)]
890pub struct ViewerOperationStage {
891 pub label: String,
892 pub started_at_epoch_seconds: u64,
893}
894
895#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
901#[serde(deny_unknown_fields)]
902pub struct ViewerOperation {
903 pub id: String,
904 pub session_id: String,
905 pub kind: ViewerOperationKind,
906 pub started_at_epoch_seconds: u64,
907 #[serde(default, skip_serializing_if = "Vec::is_empty")]
908 pub stages: Vec<ViewerOperationStage>,
909 #[serde(default, skip_serializing_if = "Option::is_none")]
912 pub notice: Option<String>,
913 pub cancellable: bool,
914}
915
916#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
918#[serde(rename_all = "kebab-case")]
919pub enum ViewerChatPhase {
920 #[default]
921 Idle,
922 Running,
923 Closing,
924 Closed,
925}
926
927#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
928#[serde(deny_unknown_fields)]
929pub struct ViewerConfigChoice {
930 pub value: String,
931 pub name: String,
932 #[serde(default, skip_serializing_if = "Option::is_none")]
933 pub description: Option<String>,
934}
935
936#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
942#[serde(deny_unknown_fields)]
943pub struct ViewerConfigOption {
944 pub key: String,
945 pub label: String,
946 #[serde(default, skip_serializing_if = "Option::is_none")]
947 pub current: Option<String>,
948 pub choices: Vec<ViewerConfigChoice>,
949}