feat(backend): guard de fermeture "travail en cours" (#83)
Expose l'état du guard de sortie applicative (GetAppExitWorkGuardState) : agents busy + tâches d'arrière-plan actives à travers tous les projets ouverts, avec détails compacts pour la popup de confirmation. Le handler CloseRequested d'app-tauri interroge ce guard avant de laisser la fenêtre se fermer, et respecte la confirmation explicite de l'utilisateur (EXIT_GUARD_CONFIRMED) pour ne pas la redemander en boucle. QA vert (backend + frontend). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@ -9,12 +9,12 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use application::{
|
||||
AgentBackgroundTaskState, AgentTicketState, AppError, AttachLiveAgentOutput,
|
||||
BackgroundTaskKindLabel, ConversationPreviewStatus, ConversationTurnWorkPreview,
|
||||
ConversationWorkSummary, CreateProjectInput, CreateProjectOutput, GitGraphOutput, HealthInput,
|
||||
HealthReport, LayoutKind, ListProjectsOutput, LiveSessionKind, LiveSessionSnapshot,
|
||||
OpenProjectOutput, ProjectWorkState, StopLiveAgentOutput, TicketWorkSource, TicketWorkStatus,
|
||||
TurnPage, TurnSource, TurnView,
|
||||
AgentBackgroundTaskState, AgentTicketState, AppError, AppExitWorkGuardDetail,
|
||||
AppExitWorkGuardState, AttachLiveAgentOutput, BackgroundTaskKindLabel,
|
||||
ConversationPreviewStatus, ConversationTurnWorkPreview, ConversationWorkSummary,
|
||||
CreateProjectInput, CreateProjectOutput, GitGraphOutput, HealthInput, HealthReport, LayoutKind,
|
||||
ListProjectsOutput, LiveSessionKind, LiveSessionSnapshot, OpenProjectOutput, ProjectWorkState,
|
||||
StopLiveAgentOutput, TicketWorkSource, TicketWorkStatus, TurnPage, TurnSource, TurnView,
|
||||
};
|
||||
use domain::{AgentBusyState, PageCursor, PageDirection, Project, ProjectId, TurnRole};
|
||||
|
||||
@ -2275,6 +2275,111 @@ impl From<ProjectWorkState> for ProjectWorkStateDto {
|
||||
}
|
||||
}
|
||||
|
||||
/// App-wide shutdown guard read model for the exit confirmation flow.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AppExitWorkGuardStateDto {
|
||||
/// Whether at least one active work item would be interrupted by app exit.
|
||||
pub has_work_in_progress: bool,
|
||||
/// Number of busy agents across all open projects.
|
||||
pub busy_agent_count: usize,
|
||||
/// Number of non-terminal background tasks across all open projects.
|
||||
pub active_background_task_count: usize,
|
||||
/// Total active work items.
|
||||
pub total_work_count: usize,
|
||||
/// Best-effort compact details for the confirmation dialog.
|
||||
pub details: Vec<AppExitWorkGuardDetailDto>,
|
||||
}
|
||||
|
||||
impl From<AppExitWorkGuardState> for AppExitWorkGuardStateDto {
|
||||
fn from(state: AppExitWorkGuardState) -> Self {
|
||||
Self {
|
||||
has_work_in_progress: state.has_work_in_progress,
|
||||
busy_agent_count: state.busy_agent_count,
|
||||
active_background_task_count: state.active_background_task_count,
|
||||
total_work_count: state.busy_agent_count + state.active_background_task_count,
|
||||
details: state
|
||||
.details
|
||||
.into_iter()
|
||||
.map(AppExitWorkGuardDetailDto::from)
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One active work item contributing to [`AppExitWorkGuardStateDto`].
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase", tag = "kind")]
|
||||
pub enum AppExitWorkGuardDetailDto {
|
||||
/// A manifest agent is currently processing a turn.
|
||||
BusyAgent {
|
||||
/// Owning project id.
|
||||
project_id: String,
|
||||
/// Owning project display name.
|
||||
project_name: String,
|
||||
/// Agent id.
|
||||
agent_id: String,
|
||||
/// Agent display name.
|
||||
agent_name: String,
|
||||
/// Busy ticket id, when available.
|
||||
ticket_id: Option<String>,
|
||||
},
|
||||
/// A first-class background task is queued, running or waiting.
|
||||
ActiveBackgroundTask {
|
||||
/// Owning project id.
|
||||
project_id: String,
|
||||
/// Owning project display name.
|
||||
project_name: String,
|
||||
/// Owning agent id.
|
||||
agent_id: String,
|
||||
/// Owning agent display name.
|
||||
agent_name: String,
|
||||
/// Stable task id.
|
||||
task_id: String,
|
||||
/// Lifecycle state.
|
||||
state: String,
|
||||
/// Kind discriminant.
|
||||
task_kind: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<AppExitWorkGuardDetail> for AppExitWorkGuardDetailDto {
|
||||
fn from(detail: AppExitWorkGuardDetail) -> Self {
|
||||
match detail {
|
||||
AppExitWorkGuardDetail::BusyAgent {
|
||||
project_id,
|
||||
project_name,
|
||||
agent_id,
|
||||
agent_name,
|
||||
ticket_id,
|
||||
} => Self::BusyAgent {
|
||||
project_id: project_id.to_string(),
|
||||
project_name,
|
||||
agent_id: agent_id.to_string(),
|
||||
agent_name,
|
||||
ticket_id: ticket_id.map(|id| id.to_string()),
|
||||
},
|
||||
AppExitWorkGuardDetail::ActiveBackgroundTask {
|
||||
project_id,
|
||||
project_name,
|
||||
agent_id,
|
||||
agent_name,
|
||||
task_id,
|
||||
state,
|
||||
kind,
|
||||
} => Self::ActiveBackgroundTask {
|
||||
project_id: project_id.to_string(),
|
||||
project_name,
|
||||
agent_id: agent_id.to_string(),
|
||||
agent_name,
|
||||
task_id: task_id.to_string(),
|
||||
state: background_state_label(state).to_owned(),
|
||||
task_kind: background_kind_label_from_work_state(kind).to_owned(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Request DTO for `attach_live_agent`: bind an already-running agent session to
|
||||
/// a visible layout cell without spawning a new process.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
|
||||
Reference in New Issue
Block a user