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, skip_serializing_if = "Vec::is_empty")]
30 pub capacity: Vec<ViewerTargetCapacity>,
31 #[serde(default, skip_serializing_if = "Vec::is_empty")]
33 pub launch_failures: Vec<ViewerLaunchFailure>,
34}
35
36#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
41pub struct ViewerLaunchFailure {
42 pub id: String,
45 pub workspace_id: String,
46 #[serde(default, skip_serializing_if = "Option::is_none")]
49 pub session_id: Option<String>,
50 #[serde(default, skip_serializing_if = "Option::is_none")]
52 pub error: Option<String>,
53}
54
55impl ViewerSnapshot {
56 pub fn from_config_state(config: &Config, state: &AppState, revision: u64) -> Self {
60 let sessions = state
61 .sessions
62 .values()
63 .map(|session| {
64 let incompatible = config
65 .targets
66 .keys()
67 .filter(|target_id| {
68 crate::controller::resume_compatibility(session, config, target_id).is_err()
69 })
70 .cloned()
71 .collect::<Vec<_>>();
72 let lifecycle = ViewerLifecycleCategory::of(session.state);
73 let project = state.project_identity_session(session);
77 let source = project.project_source(config);
78 let subagent = state.subagents.get(&session.id);
79 let subagent_session_ids = state
80 .subagents
81 .values()
82 .filter(|child| child.parent_session_id == session.id)
83 .map(|child| child.child_session_id.clone())
84 .collect();
85 ViewerSession {
86 targeted_turn_control_supported: false,
87 native_subagents: Vec::new(),
88 steering: None,
89 active_prompt_id: None,
90 cancelling_prompt_id: None,
91 capacity_retry: None,
92 retry_assessment_pending: false,
93 quota_recovery: None,
94 id: session.id.clone(),
95 publication_state: session.publication_state(),
96 managed_checkout_kind: session
97 .managed_worktree
98 .as_ref()
99 .map(|owned| owned.kind),
100 workspace_id: session.workspace_id.clone(),
101 title: public_title(session),
102 subagent_parent_id: subagent.map(|child| child.parent_session_id.clone()),
103 subagent_task_name: subagent.map(|child| child.task_name.clone()),
104 subagent_session_ids,
105 harness_kind: session.harness_kind.id().into(),
106 profile_id: session.last_profile.clone(),
107 bundle_id: session.bundle_id.clone(),
108 target_id: session.target_template_id.clone(),
109 state: match session.state {
110 SessionState::Closing => "suspending",
111 SessionState::Stopped => "suspended",
112 _ => session.state.as_str(),
113 }
114 .into(),
115 created_at: session.created_at.clone(),
116 updated_at: session.updated_at.clone(),
117 has_error: session.last_error.is_some()
118 || session.configuration_issue(config).is_some(),
119 has_checkpoint: session.checkpoint.is_some(),
120 configuration_issue: session.configuration_issue(config),
121 launch_error: matches!(
136 session.state,
137 SessionState::Error | SessionState::Stopped
138 )
139 .then(|| session.last_error.clone())
140 .flatten()
141 .or_else(|| session.public_error().map(str::to_owned)),
142 preview: Vec::new(),
143 queued_prompts: Vec::new(),
144 active_user_shells: Vec::new(),
145 background_tasks: Vec::new(),
146 pending_elicitations: Vec::new(),
147 conversation_available: false,
148 prompt_images_supported: false,
149 incompatible_resume_targets: incompatible.clone(),
150 compatible_resume_targets: config
151 .targets
152 .keys()
153 .filter(|target_id| !incompatible.contains(*target_id))
154 .cloned()
155 .collect(),
156 project_label: source.short,
157 project_key: project_key(&source.key),
158 display_location: project.project_target(config, &session.target_template_id),
159 lifecycle,
160 transitioning: session.state.transition_kind().is_some(),
161 latest_event_ordinal: 0,
162 last_activity_at_ms: None,
163 activity_details: None,
164 activity: String::new(),
165 operation: None,
166 move_recovery: None,
167 chat_phase: ViewerChatPhase::default(),
170 is_idle: false,
171 activity_state: None,
172 config_options: Vec::new(),
173 plan_mode_active: None,
174 turn_review: None,
175 available_commands: Vec::new(),
176 capabilities: ViewerSessionCapabilities {
180 clear_context: false,
181 open: false,
182 prompt: false,
183 run_shell: false,
184 interrupt_turn: false,
185 cancel_operation: false,
186 suspend: lifecycle.is_dashboard_visible(),
187 destroy: true,
188 rename: true,
189 resume: !lifecycle.is_dashboard_visible(),
190 move_session: false,
191 set_config: false,
192 set_plan_mode: false,
193 },
194 }
195 })
196 .collect();
197 let profiles = config
198 .enabled_profiles()
199 .map(|(id, profile)| ViewerProfile {
200 id: id.to_owned(),
201 harness_kind: profile.kind.id().into(),
202 quota: None,
203 })
204 .collect();
205 let targets = config
206 .targets
207 .iter()
208 .map(|(id, target)| ViewerTarget {
209 id: id.clone(),
210 kind: target.kind_name().into(),
211 requires_project_directory: matches!(
212 target,
213 TargetTemplate::LocalBare | TargetTemplate::SshBare { .. }
214 ),
215 recent_project_directories: project_history_host(target)
216 .map(|host| {
217 state
218 .project_directories(host)
219 .iter()
220 .map(|directory| directory.to_string_lossy().into_owned())
221 .collect()
222 })
223 .unwrap_or_default(),
224 })
225 .collect();
226 let bundles = config
227 .bundles
228 .iter()
229 .map(|(id, bundle)| ViewerBundle {
230 id: id.clone(),
231 primary_repository: bundle.primary_repo.clone(),
232 repositories: bundle
233 .repositories
234 .iter()
235 .map(|repository| ViewerRepository {
236 id: repository.id.clone(),
237 github: repository.github.clone(),
238 destination: repository.destination.to_string_lossy().into_owned(),
239 })
240 .collect(),
241 })
242 .collect();
243 Self {
244 revision,
245 generated_at: now_unix().to_string(),
246 server_time_ms: mj_core::clock::epoch_millis(),
247 server_version: env!("CARGO_PKG_VERSION").to_owned(),
248 workspaces: Vec::new(),
249 sessions,
250 profiles,
251 targets,
252 bundles,
253 review_config: ViewerReviewConfig {
254 enabled: config.review.enabled,
255 tier: config.review.tier.label().to_owned(),
256 profile: config.review.profile.clone(),
257 },
258 capacity: Vec::new(),
259 launch_failures: Vec::new(),
260 }
261 }
262}
263
264pub(super) fn project_key(identity: &str) -> String {
271 use sha2::Digest as _;
272 let digest = Sha256::digest(identity.as_bytes());
273 mj_core::hex::lower_hex(&digest[..8])
274}
275
276#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
277#[serde(deny_unknown_fields)]
278pub struct ViewerSession {
279 #[serde(default)]
280 pub targeted_turn_control_supported: bool,
281 #[serde(default, skip_serializing_if = "Vec::is_empty")]
282 pub native_subagents: Vec<mj_core::native_agent::NativeAgent>,
283 #[serde(default, skip_serializing_if = "Option::is_none")]
284 pub steering: Option<mj_core::relay::SteeringOperation>,
285 #[serde(default, skip_serializing_if = "Option::is_none")]
286 pub active_prompt_id: Option<String>,
287 #[serde(default, skip_serializing_if = "Option::is_none")]
288 pub cancelling_prompt_id: Option<String>,
289 #[serde(default, skip_serializing_if = "Option::is_none")]
290 pub capacity_retry: Option<mj_core::relay::CapacityRetry>,
291 #[serde(default)]
292 pub retry_assessment_pending: bool,
293 #[serde(default, skip_serializing_if = "Option::is_none")]
294 pub quota_recovery: Option<mj_core::continuation::QuotaRecovery>,
295 pub id: String,
296 #[serde(default, skip_serializing_if = "Option::is_none")]
297 pub publication_state: Option<mj_core::state::PublicationState>,
298 #[serde(default, skip_serializing_if = "Option::is_none")]
299 pub managed_checkout_kind: Option<mj_core::state::ManagedCheckoutKind>,
300 #[serde(default, skip_serializing_if = "String::is_empty")]
301 pub workspace_id: String,
302 pub title: String,
303 #[serde(default, skip_serializing_if = "Option::is_none")]
305 pub subagent_parent_id: Option<String>,
306 #[serde(default, skip_serializing_if = "Option::is_none")]
308 pub subagent_task_name: Option<String>,
309 #[serde(default, skip_serializing_if = "Vec::is_empty")]
311 pub subagent_session_ids: Vec<String>,
312 pub harness_kind: String,
313 pub profile_id: String,
314 pub bundle_id: String,
315 pub target_id: String,
316 pub state: String,
317 pub created_at: String,
318 pub updated_at: String,
319 pub has_error: bool,
320 #[serde(default)]
326 pub has_checkpoint: bool,
327 #[serde(default, skip_serializing_if = "Option::is_none")]
329 pub configuration_issue: Option<String>,
330 #[serde(default, skip_serializing_if = "Option::is_none")]
335 pub launch_error: Option<String>,
336
337 #[serde(default, skip_serializing_if = "Vec::is_empty")]
338 pub preview: Vec<String>,
339 #[serde(default, skip_serializing_if = "Vec::is_empty")]
340 pub queued_prompts: Vec<ViewerQueuedPrompt>,
341 #[serde(default, skip_serializing_if = "Vec::is_empty")]
342 pub active_user_shells: Vec<ViewerUserShell>,
343 #[serde(default, skip_serializing_if = "Vec::is_empty")]
344 pub background_tasks: Vec<ViewerBackgroundTask>,
345 #[serde(default, skip_serializing_if = "Vec::is_empty")]
349 pub pending_elicitations: Vec<ElicitationRequest>,
350 pub conversation_available: bool,
351 #[serde(default)]
355 pub prompt_images_supported: bool,
356 #[serde(default, skip_serializing_if = "Vec::is_empty")]
363 pub incompatible_resume_targets: Vec<String>,
364 #[serde(default, skip_serializing_if = "Vec::is_empty")]
367 pub compatible_resume_targets: Vec<String>,
368 #[serde(default, skip_serializing_if = "String::is_empty")]
371 pub project_label: String,
372 #[serde(default, skip_serializing_if = "String::is_empty")]
376 pub project_key: String,
377 #[serde(default)]
380 pub display_location: String,
381 pub lifecycle: ViewerLifecycleCategory,
382 #[serde(default)]
386 pub transitioning: bool,
387 #[serde(default)]
391 pub latest_event_ordinal: u64,
392 #[serde(default, skip_serializing_if = "Option::is_none")]
396 pub last_activity_at_ms: Option<i64>,
397 #[serde(default, skip_serializing_if = "Option::is_none")]
400 pub activity_details: Option<ViewerActivityDetails>,
401 #[serde(default, skip_serializing_if = "Option::is_none")]
402 pub operation: Option<ViewerOperation>,
403 #[serde(default, skip_serializing_if = "Option::is_none")]
407 pub move_recovery: Option<ViewerMoveRecovery>,
408 #[serde(default)]
409 pub chat_phase: ViewerChatPhase,
410 #[serde(default)]
413 pub is_idle: bool,
414 #[serde(default, skip_serializing_if = "Option::is_none")]
419 pub activity_state: Option<mj_core::activity::ActivityState>,
420 #[serde(default, skip_serializing_if = "String::is_empty")]
423 pub activity: String,
424 #[serde(default, skip_serializing_if = "Vec::is_empty")]
426 pub config_options: Vec<ViewerConfigOption>,
427 #[serde(default, skip_serializing_if = "Option::is_none")]
429 pub plan_mode_active: Option<bool>,
430 #[serde(default, skip_serializing_if = "Option::is_none")]
433 pub turn_review: Option<ViewerTurnReview>,
434 #[serde(default, skip_serializing_if = "Vec::is_empty")]
438 pub available_commands: Vec<ViewerMjCommand>,
439 pub capabilities: ViewerSessionCapabilities,
440}
441
442#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
443#[serde(deny_unknown_fields)]
444pub struct ViewerMoveRecovery {
445 pub operation_id: String,
446 pub source_profile_id: String,
447 pub source_target_template_id: String,
448 pub destination_profile_id: String,
449 pub destination_target_template_id: String,
450 pub phase: String,
451 pub queue: String,
452 pub clear_resource_allocation: bool,
453 #[serde(default)]
456 pub source_additional_mounts: Vec<AdditionalMount>,
457 #[serde(default)]
458 pub source_resource_allocation: Option<SessionResourceAllocation>,
459 #[serde(default)]
462 pub destination_additional_mounts: Vec<AdditionalMount>,
463 #[serde(default)]
464 pub destination_resource_allocation: Option<SessionResourceAllocation>,
465 pub checkpoint_retained: bool,
466 pub destination_ready: bool,
467 pub queue_admission_started: bool,
468 pub queue_admission_finished: bool,
469}
470
471impl ViewerMoveRecovery {
472 #[must_use]
473 pub fn from_operation(operation: &MoveOperation) -> Option<Self> {
474 if matches!(operation.phase, MovePhase::Completed) {
475 return None;
476 }
477 Some(Self {
478 operation_id: operation.operation_id.clone(),
479 source_profile_id: operation.source_profile_id.clone(),
480 source_target_template_id: operation.source_target_template_id.clone(),
481 destination_profile_id: operation.selection.profile_id.clone().unwrap_or_default(),
482 destination_target_template_id: operation
483 .selection
484 .target_template_id
485 .clone()
486 .unwrap_or_default(),
487 phase: match operation.phase {
488 MovePhase::Preparing => "preparing",
489 MovePhase::ClosingSource => "closing_source",
490 MovePhase::ResumingDestination => "resuming_destination",
491 MovePhase::StartingQueue => "starting_queue",
492 MovePhase::Completed => "completed",
493 MovePhase::Failed => "failed",
494 MovePhase::Cancelled => "cancelled",
495 }
496 .into(),
497 queue: match operation.queue {
498 ResumeQueueDisposition::Start => "start",
499 ResumeQueueDisposition::Discard => "discard",
500 }
501 .into(),
502 clear_resource_allocation: operation.selection.clear_resource_allocation,
503 source_additional_mounts: operation.source_additional_mounts.clone(),
504 source_resource_allocation: operation.source_resource_allocation.clone(),
505 destination_additional_mounts: operation
506 .selection
507 .additional_mounts
508 .clone()
509 .unwrap_or_default(),
510 destination_resource_allocation: operation.selection.resource_allocation.clone(),
511 checkpoint_retained: operation.checkpoint.is_some(),
512 destination_ready: operation.destination_target.is_some()
513 && operation.destination_native_session_id.is_some(),
514 queue_admission_started: operation.queue_admission_started,
515 queue_admission_finished: operation.queue_admission_finished,
516 })
517 }
518}
519
520impl ViewerSession {
521 pub fn set_project_source(&mut self, source: &ProjectSourceIdentity) {
524 self.project_label = source.short.clone();
525 self.project_key = project_key(&source.key);
526 }
527}
528
529pub use crate::database::{
531 ApiActivityDetails as ViewerActivityDetails, ApiActivityKind as ViewerActivityKind,
532};
533
534#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
536#[serde(deny_unknown_fields)]
537pub struct ViewerMjCommand {
538 pub name: String,
539 pub description: String,
540 pub source: ViewerCommandSource,
543 #[serde(default, skip_serializing_if = "Option::is_none")]
545 pub argument: Option<String>,
546}
547
548#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
549#[serde(rename_all = "snake_case")]
550pub enum ViewerCommandSource {
551 Mj,
552 Agent,
553}
554
555#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
557#[serde(deny_unknown_fields)]
558pub struct ViewerReviewConfig {
559 pub enabled: bool,
560 pub tier: String,
561 #[serde(default, skip_serializing_if = "Option::is_none")]
562 pub profile: Option<String>,
563}
564
565#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
567#[serde(deny_unknown_fields)]
568pub struct ViewerTurnReview {
569 pub tier: String,
571 pub status: String,
573 #[serde(default, skip_serializing_if = "Vec::is_empty")]
575 pub roles: Vec<ViewerReviewRole>,
576 #[serde(default, skip_serializing_if = "Option::is_none")]
578 pub verdict: Option<ViewerReviewVerdict>,
579}
580
581#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
582#[serde(deny_unknown_fields)]
583pub struct ViewerReviewRole {
584 pub label: String,
585 pub state: String,
587}
588
589#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
590#[serde(deny_unknown_fields)]
591pub struct ViewerReviewVerdict {
592 pub kind: String,
594 pub text: String,
596 #[serde(default, skip_serializing_if = "Vec::is_empty")]
600 pub allowed: Vec<String>,
601}
602
603impl ViewerTurnReview {
604 #[must_use]
606 pub fn from_runtime(review: &crate::review_host::RuntimeReviewView) -> Self {
607 Self {
608 tier: review.tier.label().to_owned(),
609 status: review.status.clone(),
610 roles: review
611 .roles
612 .iter()
613 .map(|role| ViewerReviewRole {
614 label: role.label.clone(),
615 state: role.state.label().to_owned(),
616 })
617 .collect(),
618 verdict: review.verdict.as_ref().map(|verdict| ViewerReviewVerdict {
619 kind: match verdict.kind {
620 crate::review_host::VerdictKind::Clean => "clean",
621 crate::review_host::VerdictKind::Findings => "findings",
622 crate::review_host::VerdictKind::Failed => "failed",
623 }
624 .to_owned(),
625 text: verdict.text.clone(),
626 allowed: verdict
627 .allowed
628 .iter()
629 .filter_map(resolution_name)
630 .map(str::to_owned)
631 .collect(),
632 }),
633 }
634 }
635}
636
637#[must_use]
640pub fn resolution_name(resolution: &mj_core::review::driver::Resolution) -> Option<&'static str> {
641 match resolution {
642 mj_core::review::driver::Resolution::Forwarded => Some("forward"),
643 mj_core::review::driver::Resolution::Dismissed => Some("dismiss"),
644 mj_core::review::driver::Resolution::Cancelled => Some("cancel"),
645 mj_core::review::driver::Resolution::NothingToReview
647 | mj_core::review::driver::Resolution::CoverageStarted => None,
648 }
649}
650
651#[must_use]
653pub fn resolution_from_name(name: &str) -> Option<mj_core::review::driver::Resolution> {
654 match name {
655 "forward" => Some(mj_core::review::driver::Resolution::Forwarded),
656 "dismiss" => Some(mj_core::review::driver::Resolution::Dismissed),
657 "cancel" => Some(mj_core::review::driver::Resolution::Cancelled),
658 _ => None,
659 }
660}
661
662#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
663#[serde(deny_unknown_fields)]
664pub struct ViewerWorkspace {
665 pub id: String,
666 pub name: String,
667}
668
669#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
670#[serde(deny_unknown_fields)]
671pub struct ViewerQueuedPrompt {
672 pub id: String,
673 pub text: String,
674 pub created_at: String,
675}
676
677#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
678#[serde(deny_unknown_fields)]
679pub struct ViewerUserShell {
680 pub id: String,
681 pub command: String,
682 pub started_at_ms: Option<i64>,
683}
684
685#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
687#[serde(deny_unknown_fields)]
688pub struct ViewerBackgroundTask {
689 pub id: String,
690 pub command: String,
691 pub started_at_ms: i64,
692 pub can_stop: bool,
693}
694
695#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
696#[serde(deny_unknown_fields)]
697pub struct ViewerProfile {
698 pub id: String,
699 pub harness_kind: String,
700 #[serde(default, skip_serializing_if = "Option::is_none")]
701 pub quota: Option<ViewerQuota>,
702}
703
704#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
710#[serde(deny_unknown_fields)]
711pub struct ViewerQuotaWindow {
712 pub label: String,
713 #[serde(default, skip_serializing_if = "Option::is_none")]
714 pub percent_used: Option<u8>,
715 #[serde(default, skip_serializing_if = "Option::is_none")]
716 pub resets_at: Option<String>,
717 pub projects_exhaustion_before_reset: bool,
720}
721
722#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
723#[serde(deny_unknown_fields)]
724pub struct ViewerQuota {
725 pub summary: String,
728 #[serde(default, skip_serializing_if = "Vec::is_empty")]
729 pub windows: Vec<ViewerQuotaWindow>,
730 #[serde(default, skip_serializing_if = "Option::is_none")]
731 pub resets_at: Option<String>,
732 pub stale: bool,
733 #[serde(default)]
736 pub refreshed_at_epoch_seconds: u64,
737 pub has_error: bool,
740}
741
742#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
748#[serde(deny_unknown_fields)]
749pub struct ViewerTargetCapacity {
750 pub id: String,
751 pub label: String,
754 pub target_ids: Vec<String>,
755 #[serde(default, skip_serializing_if = "Option::is_none")]
756 pub cpu_percent: Option<u8>,
757 #[serde(default, skip_serializing_if = "Option::is_none")]
758 pub memory_used_bytes: Option<u64>,
759 #[serde(default, skip_serializing_if = "Option::is_none")]
760 pub memory_total_bytes: Option<u64>,
761 #[serde(default, skip_serializing_if = "Option::is_none")]
762 pub logical_cores: Option<u64>,
763 #[serde(default, skip_serializing_if = "Option::is_none")]
764 pub disk_total_bytes: Option<u64>,
765 #[serde(default, skip_serializing_if = "Option::is_none")]
767 pub virtual_machines: Option<u64>,
768 #[serde(default, skip_serializing_if = "Option::is_none")]
769 pub sampled_at_epoch_seconds: Option<u64>,
770 pub refreshing: bool,
771 pub stale: bool,
772 pub has_error: bool,
775}
776
777#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
778#[serde(deny_unknown_fields)]
779pub struct ViewerTarget {
780 pub id: String,
781 pub kind: String,
782 pub requires_project_directory: bool,
783 #[serde(default)]
787 pub recent_project_directories: Vec<String>,
788}
789
790#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
791#[serde(deny_unknown_fields)]
792pub struct ViewerBundle {
793 pub id: String,
794 pub primary_repository: String,
795 pub repositories: Vec<ViewerRepository>,
796}
797
798#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
799#[serde(deny_unknown_fields)]
800pub struct ViewerRepository {
801 pub id: String,
802 pub github: Option<String>,
803 pub destination: String,
804}
805
806#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
814#[serde(deny_unknown_fields)]
815pub struct ViewerSessionCapabilities {
816 #[serde(default)]
817 pub clear_context: bool,
818 pub open: bool,
819 pub prompt: bool,
820 pub run_shell: bool,
821 pub interrupt_turn: bool,
823 pub cancel_operation: bool,
825 pub suspend: bool,
826 pub destroy: bool,
827 pub rename: bool,
828 pub resume: bool,
829 #[serde(default)]
832 pub move_session: bool,
833 pub set_config: bool,
834 pub set_plan_mode: bool,
835}
836
837#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
843#[serde(rename_all = "kebab-case")]
844pub enum ViewerLifecycleCategory {
845 Live,
846 Starting,
847 Suspending,
848 Suspended,
849 Failed,
850}
851
852pub const LAUNCHING_STATE: &str = "launching";
856
857fn public_title(session: &mj_core::state::SessionRecord) -> String {
862 session.listed_title().to_owned()
863}
864
865impl ViewerLifecycleCategory {
866 pub(super) const fn of(state: SessionState) -> Self {
867 match state {
868 SessionState::Provisioning => Self::Starting,
869 SessionState::Running
871 | SessionState::Disconnected
872 | SessionState::Checkpointing
873 | SessionState::Parked => Self::Live,
874 SessionState::Closing | SessionState::Destroying => Self::Suspending,
875 SessionState::Stopped => Self::Suspended,
876 SessionState::Lost | SessionState::Error | SessionState::DestroyedWithDataLoss => {
877 Self::Failed
878 }
879 }
880 }
881
882 pub const fn is_dashboard_visible(self) -> bool {
886 matches!(self, Self::Live | Self::Starting | Self::Suspending)
887 }
888}
889
890#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
891#[serde(rename_all = "kebab-case")]
892pub enum ViewerOperationKind {
893 Create,
894 Resume,
895 Move,
896 Suspend,
897 Destroy,
898 Stop,
900 Cleanup,
901 Checkpoint,
902}
903
904impl ViewerOperationKind {
905 pub const fn transition_kind(self) -> Option<SessionTransitionKind> {
906 match self {
907 Self::Create => Some(SessionTransitionKind::Starting),
908 Self::Resume => Some(SessionTransitionKind::Resuming),
909 Self::Move => Some(SessionTransitionKind::Moving),
910 Self::Suspend => Some(SessionTransitionKind::Suspending),
911 Self::Destroy | Self::Cleanup => Some(SessionTransitionKind::Destroying),
912 Self::Stop => Some(SessionTransitionKind::Stopping),
913 Self::Checkpoint => None,
916 }
917 }
918}
919
920#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
922#[serde(deny_unknown_fields)]
923pub struct ViewerOperationStage {
924 pub label: String,
925 pub started_at_epoch_seconds: u64,
926}
927
928#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
934#[serde(deny_unknown_fields)]
935pub struct ViewerOperation {
936 pub id: String,
937 pub session_id: String,
938 pub kind: ViewerOperationKind,
939 pub started_at_epoch_seconds: u64,
940 #[serde(default, skip_serializing_if = "Vec::is_empty")]
941 pub stages: Vec<ViewerOperationStage>,
942 #[serde(default, skip_serializing_if = "Option::is_none")]
945 pub notice: Option<String>,
946 pub cancellable: bool,
947}
948
949#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
951#[serde(rename_all = "kebab-case")]
952pub enum ViewerChatPhase {
953 #[default]
954 Idle,
955 Running,
956 Closing,
957 Closed,
958}
959
960#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
961#[serde(deny_unknown_fields)]
962pub struct ViewerConfigChoice {
963 pub value: String,
964 pub name: String,
965 #[serde(default, skip_serializing_if = "Option::is_none")]
966 pub description: Option<String>,
967}
968
969#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
975#[serde(deny_unknown_fields)]
976pub struct ViewerConfigOption {
977 pub key: String,
978 pub label: String,
979 #[serde(default, skip_serializing_if = "Option::is_none")]
980 pub current: Option<String>,
981 pub choices: Vec<ViewerConfigChoice>,
982}