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)]
|
||||
|
||||
@ -21,30 +21,30 @@ use application::{
|
||||
CreateTemplate, DeleteAgent, DeleteEmbedderProfile, DeleteIssue, DeleteLayout, DeleteMemory,
|
||||
DeleteModelServer, DeleteProfile, DeleteSkill, DeleteSprint, DeleteTemplate,
|
||||
DescribeEmbedderEngines, DetectAgentDrift, DetectProfiles, DismissEmbedderSuggestion,
|
||||
EnsureLocalModelServer, FirstRunState, GetLiveStateLean, GetMemory, GetProjectPermissions,
|
||||
GetProjectWorkState, GitBranches, GitCheckout, GitCommit, GitGraph, GitInit, GitLog, GitStage,
|
||||
GitStatus, GitUnstage, HarvestMemoryFromTurn, HealthUseCase, InspectConversation, LaunchAgent,
|
||||
LaunchAgentInput, LinkIssues, ListAgents, ListAgentsInput, ListDevices, ListEmbedderProfiles,
|
||||
ListIssues, ListLayouts, ListMemories, ListModelServers, ListProfiles, ListProjects,
|
||||
ListResumableAgents, ListSkills, ListSprints, ListTemplates, LiveAgentRegistry, LiveSessions,
|
||||
LiveStateLeanProvider, LiveStateProvider, LiveStateReadProvider, LoadLayout, McpRuntime,
|
||||
McpToolPermissionCatalogue, MoveTabToNewWindow, MutateLayout, OnnxModelView, OpenProject,
|
||||
OpenTerminal, OpenTicketAssistant, OrchestratorService, PairAttemptLimiter, PairDevice,
|
||||
PermissionProjectorRegistry, ProposeContext, ReadAgentContext, ReadContext,
|
||||
ReadConversationPage, ReadIssue, ReadIssueCarnet, ReadMcpToolPermissions, ReadMemory,
|
||||
ReadMemoryIndex, ReadProjectContext, ReadSkill, ReadTemplate, RecallMemory, ReconcileLayouts,
|
||||
ReconcileLiveState, ReconcileLiveStateInput, RecordTurn, RecordTurnProvider, ReferenceProfiles,
|
||||
RenameDevice, RenameLayout, RenameSprint, ReorderSprints, ResizeTerminal,
|
||||
ResolveAgentPermissions, ResolveMemoryLinks, RestoreOpenWindows, RetryBackgroundTask,
|
||||
RevokeAllDevices, RevokeDevice, RotateConversationLog, SaveEmbedderProfile, SaveModelServer,
|
||||
SaveProfile, SessionLimitService, SetActiveLayout, SnapshotOpenWindows, SnapshotRunningAgents,
|
||||
SpawnBackgroundCommand, StopLiveAgent, StructuredRoutingMode, StructuredSessions,
|
||||
SuggestedThisSession, SyncAgentWithTemplate, TerminalSessions, TouchDevice,
|
||||
UnassignSkillFromAgent, UnassignTicketFromSprint, UnlinkIssues, UpdateAgentContext,
|
||||
UpdateAgentMcpToolPermissions, UpdateAgentPermissions, UpdateIssue, UpdateIssueCarnet,
|
||||
UpdateLiveState, UpdateMemory, UpdateProjectContext, UpdateProjectMcpToolPermissions,
|
||||
UpdateProjectPermissions, UpdateSkill, UpdateTemplate, WakeSessionProvider, WriteMemory,
|
||||
WriteToTerminal, AGENT_MEMORY_RECALL_BUDGET,
|
||||
EnsureLocalModelServer, FirstRunState, GetAppExitWorkGuardState, GetLiveStateLean, GetMemory,
|
||||
GetProjectPermissions, GetProjectWorkState, GitBranches, GitCheckout, GitCommit, GitGraph,
|
||||
GitInit, GitLog, GitStage, GitStatus, GitUnstage, HarvestMemoryFromTurn, HealthUseCase,
|
||||
InspectConversation, LaunchAgent, LaunchAgentInput, LinkIssues, ListAgents, ListAgentsInput,
|
||||
ListDevices, ListEmbedderProfiles, ListIssues, ListLayouts, ListMemories, ListModelServers,
|
||||
ListProfiles, ListProjects, ListResumableAgents, ListSkills, ListSprints, ListTemplates,
|
||||
LiveAgentRegistry, LiveSessions, LiveStateLeanProvider, LiveStateProvider,
|
||||
LiveStateReadProvider, LoadLayout, McpRuntime, McpToolPermissionCatalogue, MoveTabToNewWindow,
|
||||
MutateLayout, OnnxModelView, OpenProject, OpenTerminal, OpenTicketAssistant,
|
||||
OrchestratorService, PairAttemptLimiter, PairDevice, PermissionProjectorRegistry,
|
||||
ProposeContext, ReadAgentContext, ReadContext, ReadConversationPage, ReadIssue,
|
||||
ReadIssueCarnet, ReadMcpToolPermissions, ReadMemory, ReadMemoryIndex, ReadProjectContext,
|
||||
ReadSkill, ReadTemplate, RecallMemory, ReconcileLayouts, ReconcileLiveState,
|
||||
ReconcileLiveStateInput, RecordTurn, RecordTurnProvider, ReferenceProfiles, RenameDevice,
|
||||
RenameLayout, RenameSprint, ReorderSprints, ResizeTerminal, ResolveAgentPermissions,
|
||||
ResolveMemoryLinks, RestoreOpenWindows, RetryBackgroundTask, RevokeAllDevices, RevokeDevice,
|
||||
RotateConversationLog, SaveEmbedderProfile, SaveModelServer, SaveProfile, SessionLimitService,
|
||||
SetActiveLayout, SnapshotOpenWindows, SnapshotRunningAgents, SpawnBackgroundCommand,
|
||||
StopLiveAgent, StructuredRoutingMode, StructuredSessions, SuggestedThisSession,
|
||||
SyncAgentWithTemplate, TerminalSessions, TouchDevice, UnassignSkillFromAgent,
|
||||
UnassignTicketFromSprint, UnlinkIssues, UpdateAgentContext, UpdateAgentMcpToolPermissions,
|
||||
UpdateAgentPermissions, UpdateIssue, UpdateIssueCarnet, UpdateLiveState, UpdateMemory,
|
||||
UpdateProjectContext, UpdateProjectMcpToolPermissions, UpdateProjectPermissions, UpdateSkill,
|
||||
UpdateTemplate, WakeSessionProvider, WriteMemory, WriteToTerminal, AGENT_MEMORY_RECALL_BUDGET,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use domain::ports::{
|
||||
@ -1020,6 +1020,8 @@ pub struct BackendCore {
|
||||
pub list_resumable_agents: Arc<ListResumableAgents>,
|
||||
/// Read-only live/busy state for the project's manifest agents.
|
||||
pub get_project_work_state: Arc<GetProjectWorkState>,
|
||||
/// App-wide work-in-progress guard used before confirmed application exit.
|
||||
pub get_app_exit_work_guard_state: Arc<GetAppExitWorkGuardState>,
|
||||
/// Human paginated read of a conversation's full transcript (lot LS6).
|
||||
pub read_conversation_page: Arc<ReadConversationPage>,
|
||||
/// Best-effort log rotation, triggered off the hot path at thread resume/open (lot LS6).
|
||||
@ -2233,6 +2235,9 @@ impl BackendCore {
|
||||
)
|
||||
.with_background_tasks(Arc::clone(&background_tasks_port)),
|
||||
);
|
||||
let get_app_exit_work_guard_state = Arc::new(GetAppExitWorkGuardState::new(Arc::clone(
|
||||
&get_project_work_state,
|
||||
)));
|
||||
// Lot LS6 — rotation (hors chemin chaud) + lecture humaine paginée. Tous deux
|
||||
// composent le provider d'archive par root ; la rotation lit aussi le handoff
|
||||
// (plancher `up_to`, INV-LS6). Aucune persistance déclenchée par un `append`.
|
||||
@ -2543,6 +2548,7 @@ impl BackendCore {
|
||||
change_agent_profile,
|
||||
list_resumable_agents,
|
||||
get_project_work_state,
|
||||
get_app_exit_work_guard_state,
|
||||
read_conversation_page,
|
||||
rotate_conversation_log,
|
||||
attach_live_agent,
|
||||
|
||||
Reference in New Issue
Block a user