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 checkout: session.checkout.clone(),
87 expected_runtime_identity: session.expected_runtime_identity.clone(),
88 targeted_turn_control_supported: false,
89 native_subagents: Vec::new(),
90 steering: None,
91 active_prompt_id: None,
92 cancelling_prompt_id: None,
93 capacity_retry: None,
94 retry_assessment_pending: false,
95 quota_recovery: None,
96 id: session.id.clone(),
97 publication_state: session.publication_state(),
98 managed_checkout_kind: session
99 .managed_worktree
100 .as_ref()
101 .map(|owned| owned.kind),
102 workspace_id: session.workspace_id.clone(),
103 title: public_title(session),
104 subagent_parent_id: subagent.map(|child| child.parent_session_id.clone()),
105 subagent_task_name: subagent.map(|child| child.task_name.clone()),
106 subagent_session_ids,
107 harness_kind: session.harness_kind.id().into(),
108 profile_id: session.last_profile.clone(),
109 bundle_id: session.bundle_id.clone(),
110 target_id: session.target_template_id.clone(),
111 state: match session.state {
112 SessionState::Closing => "suspending",
113 SessionState::Stopped => "suspended",
114 _ => session.state.as_str(),
115 }
116 .into(),
117 created_at: session.created_at.clone(),
118 updated_at: session.updated_at.clone(),
119 has_error: session.last_error.is_some()
120 || session.configuration_issue(config).is_some(),
121 has_checkpoint: session.checkpoint.is_some(),
122 configuration_issue: session.configuration_issue(config),
123 launch_error: matches!(
138 session.state,
139 SessionState::Error | SessionState::Stopped
140 )
141 .then(|| session.last_error.clone())
142 .flatten()
143 .or_else(|| session.public_error().map(str::to_owned)),
144 preview: Vec::new(),
145 queued_prompts: Vec::new(),
146 active_user_shells: Vec::new(),
147 background_tasks: Vec::new(),
148 pending_elicitations: Vec::new(),
149 conversation_available: false,
150 prompt_images_supported: false,
151 incompatible_resume_targets: incompatible.clone(),
152 compatible_resume_targets: config
153 .targets
154 .keys()
155 .filter(|target_id| !incompatible.contains(*target_id))
156 .cloned()
157 .collect(),
158 project_label: source.short,
159 project_key: project_key(&source.key),
160 display_location: project.project_target(config, &session.target_template_id),
161 lifecycle,
162 transitioning: session.state.transition_kind().is_some(),
163 latest_event_ordinal: 0,
164 last_activity_at_ms: None,
165 activity_details: None,
166 activity: String::new(),
167 operation: None,
168 move_recovery: None,
169 chat_phase: ViewerChatPhase::default(),
172 is_idle: false,
173 activity_state: None,
174 config_options: Vec::new(),
175 plan_mode_active: None,
176 turn_review: None,
177 available_commands: Vec::new(),
178 capabilities: ViewerSessionCapabilities {
182 clear_context: false,
183 open: false,
184 prompt: false,
185 run_shell: false,
186 interrupt_turn: false,
187 cancel_operation: false,
188 suspend: lifecycle.is_dashboard_visible(),
189 destroy: true,
190 rename: true,
191 resume: !lifecycle.is_dashboard_visible(),
192 move_session: false,
193 set_config: false,
194 set_plan_mode: false,
195 },
196 }
197 })
198 .collect();
199 let profiles = config
200 .enabled_profiles()
201 .map(|(id, profile)| ViewerProfile {
202 id: id.to_owned(),
203 harness_kind: profile.kind.id().into(),
204 quota: None,
205 })
206 .collect();
207 let targets = config
208 .targets
209 .iter()
210 .map(|(id, target)| ViewerTarget {
211 id: id.clone(),
212 kind: target.kind_name().into(),
213 requires_project_directory: matches!(
214 target,
215 TargetTemplate::LocalBare | TargetTemplate::SshBare { .. }
216 ),
217 recent_project_directories: project_history_host(target)
218 .map(|host| {
219 state
220 .project_directories(host)
221 .iter()
222 .map(|directory| directory.to_string_lossy().into_owned())
223 .collect()
224 })
225 .unwrap_or_default(),
226 })
227 .collect();
228 let bundles = config
229 .bundles
230 .iter()
231 .map(|(id, bundle)| ViewerBundle {
232 id: id.clone(),
233 primary_repository: bundle.primary_repo.clone(),
234 repositories: bundle
235 .repositories
236 .iter()
237 .map(|repository| ViewerRepository {
238 id: repository.id.clone(),
239 github: repository.github.clone(),
240 destination: repository.destination.to_string_lossy().into_owned(),
241 })
242 .collect(),
243 })
244 .collect();
245 Self {
246 revision,
247 generated_at: now_unix().to_string(),
248 server_time_ms: mj_core::clock::epoch_millis(),
249 server_version: env!("CARGO_PKG_VERSION").to_owned(),
250 workspaces: Vec::new(),
251 sessions,
252 profiles,
253 targets,
254 bundles,
255 review_config: ViewerReviewConfig {
256 enabled: config.review.enabled,
257 tier: config.review.tier.label().to_owned(),
258 profile: config.review.profile.clone(),
259 },
260 capacity: Vec::new(),
261 launch_failures: Vec::new(),
262 }
263 }
264}
265
266pub(super) fn project_key(identity: &str) -> String {
273 use sha2::Digest as _;
274 let digest = Sha256::digest(identity.as_bytes());
275 mj_core::hex::lower_hex(&digest[..8])
276}
277
278#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
279#[serde(deny_unknown_fields)]
280pub struct ViewerSession {
281 #[serde(default, skip_serializing_if = "Option::is_none")]
282 pub expected_runtime_identity: Option<String>,
283 #[serde(default, skip_serializing_if = "Option::is_none")]
284 pub checkout: Option<mj_core::remote_git::ExactCheckout>,
285 #[serde(default)]
286 pub targeted_turn_control_supported: bool,
287 #[serde(default, skip_serializing_if = "Vec::is_empty")]
288 pub native_subagents: Vec<mj_core::native_agent::NativeAgent>,
289 #[serde(default, skip_serializing_if = "Option::is_none")]
290 pub steering: Option<mj_core::relay::SteeringOperation>,
291 #[serde(default, skip_serializing_if = "Option::is_none")]
292 pub active_prompt_id: Option<String>,
293 #[serde(default, skip_serializing_if = "Option::is_none")]
294 pub cancelling_prompt_id: Option<String>,
295 #[serde(default, skip_serializing_if = "Option::is_none")]
296 pub capacity_retry: Option<mj_core::relay::CapacityRetry>,
297 #[serde(default)]
298 pub retry_assessment_pending: bool,
299 #[serde(default, skip_serializing_if = "Option::is_none")]
300 pub quota_recovery: Option<mj_core::continuation::QuotaRecovery>,
301 pub id: String,
302 #[serde(default, skip_serializing_if = "Option::is_none")]
303 pub publication_state: Option<mj_core::state::PublicationState>,
304 #[serde(default, skip_serializing_if = "Option::is_none")]
305 pub managed_checkout_kind: Option<mj_core::state::ManagedCheckoutKind>,
306 #[serde(default, skip_serializing_if = "String::is_empty")]
307 pub workspace_id: String,
308 pub title: String,
309 #[serde(default, skip_serializing_if = "Option::is_none")]
311 pub subagent_parent_id: Option<String>,
312 #[serde(default, skip_serializing_if = "Option::is_none")]
314 pub subagent_task_name: Option<String>,
315 #[serde(default, skip_serializing_if = "Vec::is_empty")]
317 pub subagent_session_ids: Vec<String>,
318 pub harness_kind: String,
319 pub profile_id: String,
320 pub bundle_id: String,
321 pub target_id: String,
322 pub state: String,
323 pub created_at: String,
324 pub updated_at: String,
325 pub has_error: bool,
326 #[serde(default)]
332 pub has_checkpoint: bool,
333 #[serde(default, skip_serializing_if = "Option::is_none")]
335 pub configuration_issue: Option<String>,
336 #[serde(default, skip_serializing_if = "Option::is_none")]
341 pub launch_error: Option<String>,
342
343 #[serde(default, skip_serializing_if = "Vec::is_empty")]
344 pub preview: Vec<String>,
345 #[serde(default, skip_serializing_if = "Vec::is_empty")]
346 pub queued_prompts: Vec<ViewerQueuedPrompt>,
347 #[serde(default, skip_serializing_if = "Vec::is_empty")]
348 pub active_user_shells: Vec<ViewerUserShell>,
349 #[serde(default, skip_serializing_if = "Vec::is_empty")]
350 pub background_tasks: Vec<ViewerBackgroundTask>,
351 #[serde(default, skip_serializing_if = "Vec::is_empty")]
355 pub pending_elicitations: Vec<ElicitationRequest>,
356 pub conversation_available: bool,
357 #[serde(default)]
361 pub prompt_images_supported: bool,
362 #[serde(default, skip_serializing_if = "Vec::is_empty")]
369 pub incompatible_resume_targets: Vec<String>,
370 #[serde(default, skip_serializing_if = "Vec::is_empty")]
373 pub compatible_resume_targets: Vec<String>,
374 #[serde(default, skip_serializing_if = "String::is_empty")]
377 pub project_label: String,
378 #[serde(default, skip_serializing_if = "String::is_empty")]
382 pub project_key: String,
383 #[serde(default)]
386 pub display_location: String,
387 pub lifecycle: ViewerLifecycleCategory,
388 #[serde(default)]
392 pub transitioning: bool,
393 #[serde(default)]
397 pub latest_event_ordinal: u64,
398 #[serde(default, skip_serializing_if = "Option::is_none")]
402 pub last_activity_at_ms: Option<i64>,
403 #[serde(default, skip_serializing_if = "Option::is_none")]
406 pub activity_details: Option<ViewerActivityDetails>,
407 #[serde(default, skip_serializing_if = "Option::is_none")]
408 pub operation: Option<ViewerOperation>,
409 #[serde(default, skip_serializing_if = "Option::is_none")]
413 pub move_recovery: Option<ViewerMoveRecovery>,
414 #[serde(default)]
415 pub chat_phase: ViewerChatPhase,
416 #[serde(default)]
419 pub is_idle: bool,
420 #[serde(default, skip_serializing_if = "Option::is_none")]
425 pub activity_state: Option<mj_core::activity::ActivityState>,
426 #[serde(default, skip_serializing_if = "String::is_empty")]
429 pub activity: String,
430 #[serde(default, skip_serializing_if = "Vec::is_empty")]
432 pub config_options: Vec<ViewerConfigOption>,
433 #[serde(default, skip_serializing_if = "Option::is_none")]
435 pub plan_mode_active: Option<bool>,
436 #[serde(default, skip_serializing_if = "Option::is_none")]
439 pub turn_review: Option<ViewerTurnReview>,
440 #[serde(default, skip_serializing_if = "Vec::is_empty")]
444 pub available_commands: Vec<ViewerMjCommand>,
445 pub capabilities: ViewerSessionCapabilities,
446}
447
448#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
449#[serde(deny_unknown_fields)]
450pub struct ViewerMoveRecovery {
451 pub operation_id: String,
452 pub source_profile_id: String,
453 pub source_target_template_id: String,
454 pub destination_profile_id: String,
455 pub destination_target_template_id: String,
456 pub phase: String,
457 pub queue: String,
458 pub clear_resource_allocation: bool,
459 #[serde(default)]
462 pub source_additional_mounts: Vec<AdditionalMount>,
463 #[serde(default)]
464 pub source_resource_allocation: Option<SessionResourceAllocation>,
465 #[serde(default)]
468 pub destination_additional_mounts: Vec<AdditionalMount>,
469 #[serde(default)]
470 pub destination_resource_allocation: Option<SessionResourceAllocation>,
471 pub checkpoint_retained: bool,
472 pub destination_ready: bool,
473 pub queue_admission_started: bool,
474 pub queue_admission_finished: bool,
475}
476
477impl ViewerMoveRecovery {
478 #[must_use]
479 pub fn from_operation(operation: &MoveOperation) -> Option<Self> {
480 if matches!(operation.phase, MovePhase::Completed) {
481 return None;
482 }
483 Some(Self {
484 operation_id: operation.operation_id.clone(),
485 source_profile_id: operation.source_profile_id.clone(),
486 source_target_template_id: operation.source_target_template_id.clone(),
487 destination_profile_id: operation.selection.profile_id.clone().unwrap_or_default(),
488 destination_target_template_id: operation
489 .selection
490 .target_template_id
491 .clone()
492 .unwrap_or_default(),
493 phase: match operation.phase {
494 MovePhase::Preparing => "preparing",
495 MovePhase::ClosingSource => "closing_source",
496 MovePhase::ResumingDestination => "resuming_destination",
497 MovePhase::StartingQueue => "starting_queue",
498 MovePhase::Completed => "completed",
499 MovePhase::Failed => "failed",
500 MovePhase::Cancelled => "cancelled",
501 }
502 .into(),
503 queue: match operation.queue {
504 ResumeQueueDisposition::Start => "start",
505 ResumeQueueDisposition::Discard => "discard",
506 }
507 .into(),
508 clear_resource_allocation: operation.selection.clear_resource_allocation,
509 source_additional_mounts: operation.source_additional_mounts.clone(),
510 source_resource_allocation: operation.source_resource_allocation.clone(),
511 destination_additional_mounts: operation
512 .selection
513 .additional_mounts
514 .clone()
515 .unwrap_or_default(),
516 destination_resource_allocation: operation.selection.resource_allocation.clone(),
517 checkpoint_retained: operation.checkpoint.is_some(),
518 destination_ready: operation.destination_target.is_some()
519 && operation.destination_native_session_id.is_some(),
520 queue_admission_started: operation.queue_admission_started,
521 queue_admission_finished: operation.queue_admission_finished,
522 })
523 }
524}
525
526impl ViewerSession {
527 pub fn set_project_source(&mut self, source: &ProjectSourceIdentity) {
530 self.project_label = source.short.clone();
531 self.project_key = project_key(&source.key);
532 }
533}
534
535pub use crate::database::{
537 ApiActivityDetails as ViewerActivityDetails, ApiActivityKind as ViewerActivityKind,
538};
539
540#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
542#[serde(deny_unknown_fields)]
543pub struct ViewerMjCommand {
544 pub name: String,
545 pub description: String,
546 pub source: ViewerCommandSource,
549 #[serde(default, skip_serializing_if = "Option::is_none")]
551 pub argument: Option<String>,
552}
553
554#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
555#[serde(rename_all = "snake_case")]
556pub enum ViewerCommandSource {
557 Mj,
558 Agent,
559}
560
561#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
563#[serde(deny_unknown_fields)]
564pub struct ViewerReviewConfig {
565 pub enabled: bool,
566 pub tier: String,
567 #[serde(default, skip_serializing_if = "Option::is_none")]
568 pub profile: Option<String>,
569}
570
571#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
573#[serde(deny_unknown_fields)]
574pub struct ViewerTurnReview {
575 pub tier: String,
577 pub status: String,
579 #[serde(default, skip_serializing_if = "Vec::is_empty")]
581 pub roles: Vec<ViewerReviewRole>,
582 #[serde(default, skip_serializing_if = "Option::is_none")]
584 pub verdict: Option<ViewerReviewVerdict>,
585}
586
587#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
588#[serde(deny_unknown_fields)]
589pub struct ViewerReviewRole {
590 pub label: String,
591 pub state: String,
593}
594
595#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
596#[serde(deny_unknown_fields)]
597pub struct ViewerReviewVerdict {
598 pub kind: String,
600 pub text: String,
602 #[serde(default, skip_serializing_if = "Vec::is_empty")]
606 pub allowed: Vec<String>,
607}
608
609impl ViewerTurnReview {
610 #[must_use]
612 pub fn from_runtime(review: &crate::review_host::RuntimeReviewView) -> Self {
613 Self {
614 tier: review.tier.label().to_owned(),
615 status: review.status.clone(),
616 roles: review
617 .roles
618 .iter()
619 .map(|role| ViewerReviewRole {
620 label: role.label.clone(),
621 state: role.state.label().to_owned(),
622 })
623 .collect(),
624 verdict: review.verdict.as_ref().map(|verdict| ViewerReviewVerdict {
625 kind: match verdict.kind {
626 crate::review_host::VerdictKind::Clean => "clean",
627 crate::review_host::VerdictKind::Findings => "findings",
628 crate::review_host::VerdictKind::Failed => "failed",
629 }
630 .to_owned(),
631 text: verdict.text.clone(),
632 allowed: verdict
633 .allowed
634 .iter()
635 .filter_map(resolution_name)
636 .map(str::to_owned)
637 .collect(),
638 }),
639 }
640 }
641}
642
643#[must_use]
646pub fn resolution_name(resolution: &mj_core::review::driver::Resolution) -> Option<&'static str> {
647 match resolution {
648 mj_core::review::driver::Resolution::Forwarded => Some("forward"),
649 mj_core::review::driver::Resolution::Dismissed => Some("dismiss"),
650 mj_core::review::driver::Resolution::Cancelled => Some("cancel"),
651 mj_core::review::driver::Resolution::NothingToReview
653 | mj_core::review::driver::Resolution::CoverageStarted => None,
654 }
655}
656
657#[must_use]
659pub fn resolution_from_name(name: &str) -> Option<mj_core::review::driver::Resolution> {
660 match name {
661 "forward" => Some(mj_core::review::driver::Resolution::Forwarded),
662 "dismiss" => Some(mj_core::review::driver::Resolution::Dismissed),
663 "cancel" => Some(mj_core::review::driver::Resolution::Cancelled),
664 _ => None,
665 }
666}
667
668#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
669#[serde(deny_unknown_fields)]
670pub struct ViewerWorkspace {
671 pub id: String,
672 pub name: String,
673}
674
675#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
676#[serde(deny_unknown_fields)]
677pub struct ViewerQueuedPrompt {
678 pub id: String,
679 pub text: String,
680 pub created_at: String,
681}
682
683#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
684#[serde(deny_unknown_fields)]
685pub struct ViewerUserShell {
686 pub id: String,
687 pub command: String,
688 pub started_at_ms: Option<i64>,
689}
690
691#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
693#[serde(deny_unknown_fields)]
694pub struct ViewerBackgroundTask {
695 pub id: String,
696 pub command: String,
697 pub started_at_ms: i64,
698 pub can_stop: bool,
699}
700
701#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
702#[serde(deny_unknown_fields)]
703pub struct ViewerProfile {
704 pub id: String,
705 pub harness_kind: String,
706 #[serde(default, skip_serializing_if = "Option::is_none")]
707 pub quota: Option<ViewerQuota>,
708}
709
710#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
716#[serde(deny_unknown_fields)]
717pub struct ViewerQuotaWindow {
718 pub label: String,
719 #[serde(default, skip_serializing_if = "Option::is_none")]
720 pub percent_used: Option<u8>,
721 #[serde(default, skip_serializing_if = "Option::is_none")]
722 pub resets_at: Option<String>,
723 pub projects_exhaustion_before_reset: bool,
726}
727
728#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
729#[serde(deny_unknown_fields)]
730pub struct ViewerQuota {
731 pub summary: String,
734 #[serde(default, skip_serializing_if = "Vec::is_empty")]
735 pub windows: Vec<ViewerQuotaWindow>,
736 #[serde(default, skip_serializing_if = "Option::is_none")]
737 pub resets_at: Option<String>,
738 pub stale: bool,
739 #[serde(default)]
742 pub refreshed_at_epoch_seconds: u64,
743 pub has_error: bool,
746}
747
748#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
754#[serde(deny_unknown_fields)]
755pub struct ViewerTargetCapacity {
756 pub id: String,
757 pub label: String,
760 pub target_ids: Vec<String>,
761 #[serde(default, skip_serializing_if = "Option::is_none")]
762 pub cpu_percent: Option<u8>,
763 #[serde(default, skip_serializing_if = "Option::is_none")]
764 pub memory_used_bytes: Option<u64>,
765 #[serde(default, skip_serializing_if = "Option::is_none")]
766 pub memory_total_bytes: Option<u64>,
767 #[serde(default, skip_serializing_if = "Option::is_none")]
768 pub logical_cores: Option<u64>,
769 #[serde(default, skip_serializing_if = "Option::is_none")]
770 pub disk_total_bytes: Option<u64>,
771 #[serde(default, skip_serializing_if = "Option::is_none")]
773 pub virtual_machines: Option<u64>,
774 #[serde(default, skip_serializing_if = "Option::is_none")]
775 pub sampled_at_epoch_seconds: Option<u64>,
776 pub refreshing: bool,
777 pub stale: bool,
778 pub has_error: bool,
781}
782
783#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
784#[serde(deny_unknown_fields)]
785pub struct ViewerTarget {
786 pub id: String,
787 pub kind: String,
788 pub requires_project_directory: bool,
789 #[serde(default)]
793 pub recent_project_directories: Vec<String>,
794}
795
796#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
797#[serde(deny_unknown_fields)]
798pub struct ViewerBundle {
799 pub id: String,
800 pub primary_repository: String,
801 pub repositories: Vec<ViewerRepository>,
802}
803
804#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
805#[serde(deny_unknown_fields)]
806pub struct ViewerRepository {
807 pub id: String,
808 pub github: Option<String>,
809 pub destination: String,
810}
811
812#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
820#[serde(deny_unknown_fields)]
821pub struct ViewerSessionCapabilities {
822 #[serde(default)]
823 pub clear_context: bool,
824 pub open: bool,
825 pub prompt: bool,
826 pub run_shell: bool,
827 pub interrupt_turn: bool,
829 pub cancel_operation: bool,
831 pub suspend: bool,
832 pub destroy: bool,
833 pub rename: bool,
834 pub resume: bool,
835 #[serde(default)]
838 pub move_session: bool,
839 pub set_config: bool,
840 pub set_plan_mode: bool,
841}
842
843#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
849#[serde(rename_all = "kebab-case")]
850pub enum ViewerLifecycleCategory {
851 Live,
852 Starting,
853 Suspending,
854 Suspended,
855 Failed,
856}
857
858pub const LAUNCHING_STATE: &str = "launching";
862
863fn public_title(session: &mj_core::state::SessionRecord) -> String {
868 session.listed_title().to_owned()
869}
870
871impl ViewerLifecycleCategory {
872 pub(super) const fn of(state: SessionState) -> Self {
873 match state {
874 SessionState::Provisioning => Self::Starting,
875 SessionState::Running
877 | SessionState::Disconnected
878 | SessionState::Checkpointing
879 | SessionState::Parked => Self::Live,
880 SessionState::Closing | SessionState::Destroying => Self::Suspending,
881 SessionState::Stopped => Self::Suspended,
882 SessionState::Lost | SessionState::Error | SessionState::DestroyedWithDataLoss => {
883 Self::Failed
884 }
885 }
886 }
887
888 pub const fn is_dashboard_visible(self) -> bool {
892 matches!(self, Self::Live | Self::Starting | Self::Suspending)
893 }
894}
895
896#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
897#[serde(rename_all = "kebab-case")]
898pub enum ViewerOperationKind {
899 Create,
900 Resume,
901 Move,
902 Suspend,
903 Destroy,
904 Stop,
906 Cleanup,
907 Checkpoint,
908}
909
910impl ViewerOperationKind {
911 pub const fn transition_kind(self) -> Option<SessionTransitionKind> {
912 match self {
913 Self::Create => Some(SessionTransitionKind::Starting),
914 Self::Resume => Some(SessionTransitionKind::Resuming),
915 Self::Move => Some(SessionTransitionKind::Moving),
916 Self::Suspend => Some(SessionTransitionKind::Suspending),
917 Self::Destroy | Self::Cleanup => Some(SessionTransitionKind::Destroying),
918 Self::Stop => Some(SessionTransitionKind::Stopping),
919 Self::Checkpoint => None,
922 }
923 }
924}
925
926#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
928#[serde(deny_unknown_fields)]
929pub struct ViewerOperationStage {
930 pub label: String,
931 pub started_at_epoch_seconds: u64,
932}
933
934#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
940#[serde(deny_unknown_fields)]
941pub struct ViewerOperation {
942 pub id: String,
943 pub session_id: String,
944 pub kind: ViewerOperationKind,
945 pub started_at_epoch_seconds: u64,
946 #[serde(default, skip_serializing_if = "Vec::is_empty")]
947 pub stages: Vec<ViewerOperationStage>,
948 #[serde(default, skip_serializing_if = "Option::is_none")]
951 pub notice: Option<String>,
952 pub cancellable: bool,
953}
954
955#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
957#[serde(rename_all = "kebab-case")]
958pub enum ViewerChatPhase {
959 #[default]
960 Idle,
961 Running,
962 Closing,
963 Closed,
964}
965
966#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
967#[serde(deny_unknown_fields)]
968pub struct ViewerConfigChoice {
969 pub value: String,
970 pub name: String,
971 #[serde(default, skip_serializing_if = "Option::is_none")]
972 pub description: Option<String>,
973}
974
975#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
981#[serde(deny_unknown_fields)]
982pub struct ViewerConfigOption {
983 pub key: String,
984 pub label: String,
985 #[serde(default, skip_serializing_if = "Option::is_none")]
986 pub current: Option<String>,
987 pub choices: Vec<ViewerConfigChoice>,
988}