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 = "Vec::is_empty")]
13 pub workspaces: Vec<ViewerWorkspace>,
14 pub sessions: Vec<ViewerSession>,
15 pub profiles: Vec<ViewerProfile>,
16 pub targets: Vec<ViewerTarget>,
17 pub bundles: Vec<ViewerBundle>,
18 #[serde(default)]
21 pub review_config: ViewerReviewConfig,
22 #[serde(default)]
25 pub subagents_enabled: bool,
26 #[serde(default, skip_serializing_if = "Vec::is_empty")]
29 pub capacity: Vec<ViewerTargetCapacity>,
30 #[serde(default, skip_serializing_if = "Vec::is_empty")]
32 pub launch_failures: Vec<ViewerLaunchFailure>,
33}
34
35#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
40pub struct ViewerLaunchFailure {
41 pub id: String,
44 pub workspace_id: String,
45 #[serde(default, skip_serializing_if = "Option::is_none")]
48 pub session_id: Option<String>,
49 #[serde(default, skip_serializing_if = "Option::is_none")]
51 pub error: Option<String>,
52}
53
54impl ViewerSnapshot {
55 pub fn from_config_state(config: &Config, state: &AppState, revision: u64) -> Self {
59 let sessions = state
60 .sessions
61 .values()
62 .map(|session| {
63 let incompatible = config
64 .targets
65 .keys()
66 .filter(|target_id| {
67 crate::controller::resume_compatibility(session, config, target_id).is_err()
68 })
69 .cloned()
70 .collect::<Vec<_>>();
71 let lifecycle = ViewerLifecycleCategory::of(session.state);
72 let source = session.project_source(config);
73 let subagent = state.subagents.get(&session.id);
74 let subagent_session_ids = state
75 .subagents
76 .values()
77 .filter(|child| child.parent_session_id == session.id)
78 .map(|child| child.child_session_id.clone())
79 .collect();
80 ViewerSession {
81 capacity_retry: None,
82 id: session.id.clone(),
83 workspace_id: session.workspace_id.clone(),
84 title: session.display_title().to_owned(),
85 subagent_parent_id: subagent.map(|child| child.parent_session_id.clone()),
86 subagent_task_name: subagent.map(|child| child.task_name.clone()),
87 subagent_session_ids,
88 harness_kind: session.harness_kind.id().into(),
89 profile_id: session.last_profile.clone(),
90 bundle_id: session.bundle_id.clone(),
91 target_id: session.target_template_id.clone(),
92 state: session.state.as_str().into(),
93 created_at: session.created_at.clone(),
94 updated_at: session.updated_at.clone(),
95 has_error: session.last_error.is_some()
96 || session.configuration_issue(config).is_some(),
97 configuration_issue: session.configuration_issue(config),
98 launch_error: (session.state == SessionState::Error)
102 .then(|| session.last_error.clone())
103 .flatten(),
104 preview: Vec::new(),
105 queued_prompts: Vec::new(),
106 active_user_shells: Vec::new(),
107 background_tasks: Vec::new(),
108 pending_elicitations: Vec::new(),
109 conversation_available: false,
110 prompt_images_supported: false,
111 incompatible_resume_targets: incompatible.clone(),
112 compatible_resume_targets: config
113 .targets
114 .keys()
115 .filter(|target_id| !incompatible.contains(*target_id))
116 .cloned()
117 .collect(),
118 project_label: source.short,
119 project_key: project_key(&source.key),
120 display_location: session.project_target(config, &session.target_template_id),
121 lifecycle,
122 transitioning: session.state.transition_kind().is_some(),
123 latest_event_ordinal: 0,
124 last_activity_at_ms: None,
125 activity_details: None,
126 activity: String::new(),
127 operation: None,
128 move_recovery: None,
129 chat_phase: ViewerChatPhase::default(),
130 is_idle: false,
131 config_options: Vec::new(),
132 plan_mode_active: None,
133 turn_review: None,
134 available_commands: Vec::new(),
135 capabilities: ViewerSessionCapabilities {
139 open: false,
140 prompt: false,
141 run_shell: false,
142 cancel_turn: false,
143 cancel_operation: false,
144 stop: lifecycle.is_dashboard_visible(),
145 rename: true,
146 resume: !lifecycle.is_dashboard_visible(),
147 move_session: false,
148 set_config: false,
149 set_plan_mode: false,
150 },
151 }
152 })
153 .collect();
154 let profiles = config
155 .enabled_profiles()
156 .map(|(id, profile)| ViewerProfile {
157 id: id.to_owned(),
158 harness_kind: profile.kind.id().into(),
159 quota: None,
160 })
161 .collect();
162 let targets = config
163 .targets
164 .iter()
165 .map(|(id, target)| ViewerTarget {
166 id: id.clone(),
167 kind: target.kind_name().into(),
168 requires_project_directory: matches!(
169 target,
170 TargetTemplate::LocalBare | TargetTemplate::SshBare { .. }
171 ),
172 recent_project_directories: project_history_host(target)
173 .map(|host| {
174 state
175 .project_directories(host)
176 .iter()
177 .map(|directory| directory.to_string_lossy().into_owned())
178 .collect()
179 })
180 .unwrap_or_default(),
181 })
182 .collect();
183 let bundles = config
184 .bundles
185 .iter()
186 .map(|(id, bundle)| ViewerBundle {
187 id: id.clone(),
188 primary_repository: bundle.primary_repo.clone(),
189 repositories: bundle
190 .repositories
191 .iter()
192 .map(|repository| ViewerRepository {
193 id: repository.id.clone(),
194 github: repository.github.clone(),
195 destination: repository.destination.to_string_lossy().into_owned(),
196 })
197 .collect(),
198 })
199 .collect();
200 Self {
201 revision,
202 generated_at: now_unix().to_string(),
203 server_time_ms: mj_core::clock::epoch_millis(),
204 workspaces: Vec::new(),
205 sessions,
206 profiles,
207 targets,
208 bundles,
209 review_config: ViewerReviewConfig {
210 enabled: config.review.enabled,
211 tier: config.review.tier.label().to_owned(),
212 profile: config.review.profile.clone(),
213 },
214 subagents_enabled: config.subagents.enabled,
215 capacity: Vec::new(),
216 launch_failures: Vec::new(),
217 }
218 }
219}
220
221pub(super) fn project_key(identity: &str) -> String {
228 use sha2::Digest as _;
229 let digest = Sha256::digest(identity.as_bytes());
230 mj_core::hex::lower_hex(&digest[..8])
231}
232
233#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
234#[serde(deny_unknown_fields)]
235pub struct ViewerSession {
236 #[serde(default, skip_serializing_if = "Option::is_none")]
237 pub capacity_retry: Option<mj_core::relay::CapacityRetry>,
238 pub id: String,
239 #[serde(default, skip_serializing_if = "String::is_empty")]
240 pub workspace_id: String,
241 pub title: String,
242 #[serde(default, skip_serializing_if = "Option::is_none")]
244 pub subagent_parent_id: Option<String>,
245 #[serde(default, skip_serializing_if = "Option::is_none")]
247 pub subagent_task_name: Option<String>,
248 #[serde(default, skip_serializing_if = "Vec::is_empty")]
250 pub subagent_session_ids: Vec<String>,
251 pub harness_kind: String,
252 pub profile_id: String,
253 pub bundle_id: String,
254 pub target_id: String,
255 pub state: String,
256 pub created_at: String,
257 pub updated_at: String,
258 pub has_error: bool,
259 #[serde(default, skip_serializing_if = "Option::is_none")]
261 pub configuration_issue: Option<String>,
262 #[serde(default, skip_serializing_if = "Option::is_none")]
267 pub launch_error: Option<String>,
268
269 #[serde(default, skip_serializing_if = "Vec::is_empty")]
270 pub preview: Vec<String>,
271 #[serde(default, skip_serializing_if = "Vec::is_empty")]
272 pub queued_prompts: Vec<ViewerQueuedPrompt>,
273 #[serde(default, skip_serializing_if = "Vec::is_empty")]
274 pub active_user_shells: Vec<ViewerUserShell>,
275 #[serde(default, skip_serializing_if = "Vec::is_empty")]
276 pub background_tasks: Vec<ViewerBackgroundTask>,
277 #[serde(default, skip_serializing_if = "Vec::is_empty")]
281 pub pending_elicitations: Vec<ElicitationRequest>,
282 pub conversation_available: bool,
283 #[serde(default)]
287 pub prompt_images_supported: bool,
288 #[serde(default, skip_serializing_if = "Vec::is_empty")]
295 pub incompatible_resume_targets: Vec<String>,
296 #[serde(default, skip_serializing_if = "Vec::is_empty")]
299 pub compatible_resume_targets: Vec<String>,
300 #[serde(default, skip_serializing_if = "String::is_empty")]
303 pub project_label: String,
304 #[serde(default, skip_serializing_if = "String::is_empty")]
308 pub project_key: String,
309 #[serde(default)]
312 pub display_location: String,
313 pub lifecycle: ViewerLifecycleCategory,
314 #[serde(default)]
318 pub transitioning: bool,
319 #[serde(default)]
323 pub latest_event_ordinal: u64,
324 #[serde(default, skip_serializing_if = "Option::is_none")]
328 pub last_activity_at_ms: Option<i64>,
329 #[serde(default, skip_serializing_if = "Option::is_none")]
332 pub activity_details: Option<ViewerActivityDetails>,
333 #[serde(default, skip_serializing_if = "Option::is_none")]
334 pub operation: Option<ViewerOperation>,
335 #[serde(default, skip_serializing_if = "Option::is_none")]
339 pub move_recovery: Option<ViewerMoveRecovery>,
340 #[serde(default)]
341 pub chat_phase: ViewerChatPhase,
342 #[serde(default)]
345 pub is_idle: bool,
346 #[serde(default, skip_serializing_if = "String::is_empty")]
349 pub activity: String,
350 #[serde(default, skip_serializing_if = "Vec::is_empty")]
352 pub config_options: Vec<ViewerConfigOption>,
353 #[serde(default, skip_serializing_if = "Option::is_none")]
355 pub plan_mode_active: Option<bool>,
356 #[serde(default, skip_serializing_if = "Option::is_none")]
359 pub turn_review: Option<ViewerTurnReview>,
360 #[serde(default, skip_serializing_if = "Vec::is_empty")]
364 pub available_commands: Vec<ViewerMjCommand>,
365 pub capabilities: ViewerSessionCapabilities,
366}
367
368#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
369#[serde(deny_unknown_fields)]
370pub struct ViewerMoveRecovery {
371 pub operation_id: String,
372 pub source_profile_id: String,
373 pub source_target_template_id: String,
374 pub destination_profile_id: String,
375 pub destination_target_template_id: String,
376 pub phase: String,
377 pub queue: String,
378 pub clear_resource_allocation: bool,
379 #[serde(default)]
382 pub source_additional_mounts: Vec<AdditionalMount>,
383 #[serde(default)]
384 pub source_resource_allocation: Option<SessionResourceAllocation>,
385 #[serde(default)]
388 pub destination_additional_mounts: Vec<AdditionalMount>,
389 #[serde(default)]
390 pub destination_resource_allocation: Option<SessionResourceAllocation>,
391 pub checkpoint_retained: bool,
392 pub destination_ready: bool,
393 pub queue_admission_started: bool,
394 pub queue_admission_finished: bool,
395}
396
397impl ViewerMoveRecovery {
398 #[must_use]
399 pub fn from_operation(operation: &MoveOperation) -> Option<Self> {
400 if matches!(operation.phase, MovePhase::Completed) {
401 return None;
402 }
403 Some(Self {
404 operation_id: operation.operation_id.clone(),
405 source_profile_id: operation.source_profile_id.clone(),
406 source_target_template_id: operation.source_target_template_id.clone(),
407 destination_profile_id: operation.selection.profile_id.clone().unwrap_or_default(),
408 destination_target_template_id: operation
409 .selection
410 .target_template_id
411 .clone()
412 .unwrap_or_default(),
413 phase: match operation.phase {
414 MovePhase::Preparing => "preparing",
415 MovePhase::ClosingSource => "closing_source",
416 MovePhase::ResumingDestination => "resuming_destination",
417 MovePhase::StartingQueue => "starting_queue",
418 MovePhase::Completed => "completed",
419 MovePhase::Failed => "failed",
420 MovePhase::Cancelled => "cancelled",
421 }
422 .into(),
423 queue: match operation.queue {
424 ResumeQueueDisposition::Start => "start",
425 ResumeQueueDisposition::Discard => "discard",
426 }
427 .into(),
428 clear_resource_allocation: operation.selection.clear_resource_allocation,
429 source_additional_mounts: operation.source_additional_mounts.clone(),
430 source_resource_allocation: operation.source_resource_allocation.clone(),
431 destination_additional_mounts: operation
432 .selection
433 .additional_mounts
434 .clone()
435 .unwrap_or_default(),
436 destination_resource_allocation: operation.selection.resource_allocation.clone(),
437 checkpoint_retained: operation.checkpoint.is_some(),
438 destination_ready: operation.destination_target.is_some()
439 && operation.destination_native_session_id.is_some(),
440 queue_admission_started: operation.queue_admission_started,
441 queue_admission_finished: operation.queue_admission_finished,
442 })
443 }
444}
445
446impl ViewerSession {
447 pub fn set_project_source(&mut self, source: &ProjectSourceIdentity) {
450 self.project_label = source.short.clone();
451 self.project_key = project_key(&source.key);
452 }
453}
454
455pub use crate::database::{
457 ApiActivityDetails as ViewerActivityDetails, ApiActivityKind as ViewerActivityKind,
458};
459
460#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
462#[serde(deny_unknown_fields)]
463pub struct ViewerMjCommand {
464 pub name: String,
465 pub description: String,
466 pub source: ViewerCommandSource,
469 #[serde(default, skip_serializing_if = "Option::is_none")]
471 pub argument: Option<String>,
472}
473
474#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
475#[serde(rename_all = "snake_case")]
476pub enum ViewerCommandSource {
477 Mj,
478 Agent,
479}
480
481#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
483#[serde(deny_unknown_fields)]
484pub struct ViewerReviewConfig {
485 pub enabled: bool,
486 pub tier: String,
487 #[serde(default, skip_serializing_if = "Option::is_none")]
488 pub profile: Option<String>,
489}
490
491#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
493#[serde(deny_unknown_fields)]
494pub struct ViewerTurnReview {
495 pub tier: String,
497 pub status: String,
499 #[serde(default, skip_serializing_if = "Vec::is_empty")]
501 pub roles: Vec<ViewerReviewRole>,
502 #[serde(default, skip_serializing_if = "Option::is_none")]
504 pub verdict: Option<ViewerReviewVerdict>,
505}
506
507#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
508#[serde(deny_unknown_fields)]
509pub struct ViewerReviewRole {
510 pub label: String,
511 pub state: String,
513}
514
515#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
516#[serde(deny_unknown_fields)]
517pub struct ViewerReviewVerdict {
518 pub kind: String,
520 pub text: String,
522 #[serde(default, skip_serializing_if = "Vec::is_empty")]
526 pub allowed: Vec<String>,
527}
528
529impl ViewerTurnReview {
530 #[must_use]
532 pub fn from_runtime(review: &crate::review_host::RuntimeReviewView) -> Self {
533 Self {
534 tier: review.tier.label().to_owned(),
535 status: review.status.clone(),
536 roles: review
537 .roles
538 .iter()
539 .map(|role| ViewerReviewRole {
540 label: role.label.clone(),
541 state: role.state.label().to_owned(),
542 })
543 .collect(),
544 verdict: review.verdict.as_ref().map(|verdict| ViewerReviewVerdict {
545 kind: match verdict.kind {
546 crate::review_host::VerdictKind::Clean => "clean",
547 crate::review_host::VerdictKind::Findings => "findings",
548 crate::review_host::VerdictKind::Failed => "failed",
549 }
550 .to_owned(),
551 text: verdict.text.clone(),
552 allowed: verdict
553 .allowed
554 .iter()
555 .filter_map(resolution_name)
556 .map(str::to_owned)
557 .collect(),
558 }),
559 }
560 }
561}
562
563#[must_use]
566pub fn resolution_name(resolution: &mj_core::review::driver::Resolution) -> Option<&'static str> {
567 match resolution {
568 mj_core::review::driver::Resolution::Forwarded => Some("forward"),
569 mj_core::review::driver::Resolution::Dismissed => Some("dismiss"),
570 mj_core::review::driver::Resolution::Cancelled => Some("cancel"),
571 mj_core::review::driver::Resolution::NothingToReview
573 | mj_core::review::driver::Resolution::CoverageStarted => None,
574 }
575}
576
577#[must_use]
579pub fn resolution_from_name(name: &str) -> Option<mj_core::review::driver::Resolution> {
580 match name {
581 "forward" => Some(mj_core::review::driver::Resolution::Forwarded),
582 "dismiss" => Some(mj_core::review::driver::Resolution::Dismissed),
583 "cancel" => Some(mj_core::review::driver::Resolution::Cancelled),
584 _ => None,
585 }
586}
587
588#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
589#[serde(deny_unknown_fields)]
590pub struct ViewerWorkspace {
591 pub id: String,
592 pub name: String,
593}
594
595#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
596#[serde(deny_unknown_fields)]
597pub struct ViewerQueuedPrompt {
598 pub id: String,
599 pub text: String,
600 pub created_at: String,
601}
602
603#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
604#[serde(deny_unknown_fields)]
605pub struct ViewerUserShell {
606 pub id: String,
607 pub command: String,
608 pub started_at_ms: Option<i64>,
609}
610
611#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
613#[serde(deny_unknown_fields)]
614pub struct ViewerBackgroundTask {
615 pub id: String,
616 pub command: String,
617 pub started_at_ms: i64,
618 pub can_stop: bool,
619}
620
621#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
622#[serde(deny_unknown_fields)]
623pub struct ViewerProfile {
624 pub id: String,
625 pub harness_kind: String,
626 #[serde(default, skip_serializing_if = "Option::is_none")]
627 pub quota: Option<ViewerQuota>,
628}
629
630#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
636#[serde(deny_unknown_fields)]
637pub struct ViewerQuotaWindow {
638 pub label: String,
639 #[serde(default, skip_serializing_if = "Option::is_none")]
640 pub percent_used: Option<u8>,
641 #[serde(default, skip_serializing_if = "Option::is_none")]
642 pub resets_at: Option<String>,
643 pub projects_exhaustion_before_reset: bool,
646}
647
648#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
649#[serde(deny_unknown_fields)]
650pub struct ViewerQuota {
651 pub summary: String,
654 #[serde(default, skip_serializing_if = "Vec::is_empty")]
655 pub windows: Vec<ViewerQuotaWindow>,
656 #[serde(default, skip_serializing_if = "Option::is_none")]
657 pub resets_at: Option<String>,
658 pub stale: bool,
659 #[serde(default)]
662 pub refreshed_at_epoch_seconds: u64,
663 pub has_error: bool,
666}
667
668#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
674#[serde(deny_unknown_fields)]
675pub struct ViewerTargetCapacity {
676 pub id: String,
677 pub label: String,
680 pub target_ids: Vec<String>,
681 #[serde(default, skip_serializing_if = "Option::is_none")]
682 pub cpu_percent: Option<u8>,
683 #[serde(default, skip_serializing_if = "Option::is_none")]
684 pub memory_used_bytes: Option<u64>,
685 #[serde(default, skip_serializing_if = "Option::is_none")]
686 pub memory_total_bytes: Option<u64>,
687 #[serde(default, skip_serializing_if = "Option::is_none")]
688 pub logical_cores: Option<u64>,
689 #[serde(default, skip_serializing_if = "Option::is_none")]
690 pub disk_total_bytes: Option<u64>,
691 #[serde(default, skip_serializing_if = "Option::is_none")]
693 pub virtual_machines: Option<u64>,
694 #[serde(default, skip_serializing_if = "Option::is_none")]
695 pub sampled_at_epoch_seconds: Option<u64>,
696 pub refreshing: bool,
697 pub stale: bool,
698 pub has_error: bool,
701}
702
703#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
704#[serde(deny_unknown_fields)]
705pub struct ViewerTarget {
706 pub id: String,
707 pub kind: String,
708 pub requires_project_directory: bool,
709 #[serde(default)]
713 pub recent_project_directories: Vec<String>,
714}
715
716#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
717#[serde(deny_unknown_fields)]
718pub struct ViewerBundle {
719 pub id: String,
720 pub primary_repository: String,
721 pub repositories: Vec<ViewerRepository>,
722}
723
724#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
725#[serde(deny_unknown_fields)]
726pub struct ViewerRepository {
727 pub id: String,
728 pub github: Option<String>,
729 pub destination: String,
730}
731
732#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
740#[serde(deny_unknown_fields)]
741pub struct ViewerSessionCapabilities {
742 pub open: bool,
743 pub prompt: bool,
744 pub run_shell: bool,
745 pub cancel_turn: bool,
747 pub cancel_operation: bool,
749 pub stop: bool,
750 pub rename: bool,
751 pub resume: bool,
752 #[serde(default)]
755 pub move_session: bool,
756 pub set_config: bool,
757 pub set_plan_mode: bool,
758}
759
760#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
766#[serde(rename_all = "kebab-case")]
767pub enum ViewerLifecycleCategory {
768 Live,
769 Starting,
770 Stopping,
771 Stopped,
772 Failed,
773}
774
775impl ViewerLifecycleCategory {
776 pub(super) const fn of(state: SessionState) -> Self {
777 match state {
778 SessionState::Provisioning => Self::Starting,
779 SessionState::Running | SessionState::Disconnected | SessionState::Checkpointing => {
780 Self::Live
781 }
782 SessionState::Closing | SessionState::Destroying => Self::Stopping,
783 SessionState::Stopped => Self::Stopped,
784 SessionState::Lost | SessionState::Error | SessionState::DestroyedWithDataLoss => {
785 Self::Failed
786 }
787 }
788 }
789
790 pub const fn is_dashboard_visible(self) -> bool {
794 matches!(self, Self::Live | Self::Starting | Self::Stopping)
795 }
796}
797
798#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
799#[serde(rename_all = "kebab-case")]
800pub enum ViewerOperationKind {
801 Create,
802 Resume,
803 Move,
804 Stop,
805 Destroy,
806 Cleanup,
807 Checkpoint,
808}
809
810impl ViewerOperationKind {
811 pub const fn transition_kind(self) -> Option<SessionTransitionKind> {
812 match self {
813 Self::Create => Some(SessionTransitionKind::Starting),
814 Self::Resume => Some(SessionTransitionKind::Resuming),
815 Self::Move => Some(SessionTransitionKind::Moving),
816 Self::Stop => Some(SessionTransitionKind::Stopping),
817 Self::Destroy | Self::Cleanup => Some(SessionTransitionKind::Destroying),
818 Self::Checkpoint => None,
821 }
822 }
823}
824
825#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
827#[serde(deny_unknown_fields)]
828pub struct ViewerOperationStage {
829 pub label: String,
830 pub started_at_epoch_seconds: u64,
831}
832
833#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
839#[serde(deny_unknown_fields)]
840pub struct ViewerOperation {
841 pub id: String,
842 pub session_id: String,
843 pub kind: ViewerOperationKind,
844 pub started_at_epoch_seconds: u64,
845 #[serde(default, skip_serializing_if = "Vec::is_empty")]
846 pub stages: Vec<ViewerOperationStage>,
847 #[serde(default, skip_serializing_if = "Option::is_none")]
850 pub notice: Option<String>,
851 pub cancellable: bool,
852}
853
854#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
856#[serde(rename_all = "kebab-case")]
857pub enum ViewerChatPhase {
858 #[default]
859 Idle,
860 Running,
861 Closing,
862 Closed,
863}
864
865#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
866#[serde(deny_unknown_fields)]
867pub struct ViewerConfigChoice {
868 pub value: String,
869 pub name: String,
870 #[serde(default, skip_serializing_if = "Option::is_none")]
871 pub description: Option<String>,
872}
873
874#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
880#[serde(deny_unknown_fields)]
881pub struct ViewerConfigOption {
882 pub key: String,
883 pub label: String,
884 #[serde(default, skip_serializing_if = "Option::is_none")]
885 pub current: Option<String>,
886 pub choices: Vec<ViewerConfigChoice>,
887}