feat(frontend): popup de confirmation à la fermeture avec travail en cours (#83)
Écoute l'event Tauri `app-exit-work-guard` (émis par le backend quand la fermeture de la fenêtre main est interceptée) et affiche une popup modale "Du travail est encore en cours" avec le résumé pluralisé exact du carnet #83, le détail agents/tâches capé à 5 lignes, et les deux actions Annuler (focus par défaut, no-op local) / Quitter quand même (danger, appelle confirm_app_exit). Pas d'option "ne plus demander". - domain/ports/adapters (Tauri listen+invoke, HTTP desktop-only stub, mock avec helpers de test) : onAppExitWorkGuard/confirmAppExit sur SystemGateway, suivant le patron déjà utilisé pour focused-project et les domain events. - AppExitConfirmDialog : mounted une fois près de la racine (App.tsx), à côté d'AnnouncementsProvider — role="alertdialog", focus trap, Échap = Annuler, pas de fermeture au clic extérieur, ne se referme jamais automatiquement (un event pendant l'ouverture rafraîchit juste le résumé). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@ -21,6 +21,7 @@
|
||||
|
||||
import type {
|
||||
Agent,
|
||||
AppExitWorkGuardState,
|
||||
DomainEvent,
|
||||
HealthReport,
|
||||
ReplyChunk,
|
||||
@ -114,6 +115,19 @@ export class HttpSystemGateway implements SystemGateway {
|
||||
// folder browser would be its own lot (flagged in the F1 report).
|
||||
return unsupportedOnWeb("Native folder picker");
|
||||
}
|
||||
|
||||
onAppExitWorkGuard(
|
||||
_handler: (state: AppExitWorkGuardState) => void,
|
||||
): Promise<Unsubscribe> {
|
||||
// Desktop-only (ticket #83): there is no interceptable native window to
|
||||
// guard on the web client, so this never fires — an inert unsubscribe,
|
||||
// not a rejection, so callers can subscribe unconditionally.
|
||||
return Promise.resolve(() => {});
|
||||
}
|
||||
|
||||
confirmAppExit(): Promise<void> {
|
||||
return unsupportedOnWeb("Confirming an app exit");
|
||||
}
|
||||
}
|
||||
|
||||
export class HttpTerminalGateway implements TerminalGateway {
|
||||
|
||||
@ -7,6 +7,7 @@
|
||||
import type {
|
||||
Agent,
|
||||
AgentDrift,
|
||||
AppExitWorkGuardState,
|
||||
AgentProfile,
|
||||
DiagnosticWarning,
|
||||
DomainEvent,
|
||||
@ -182,6 +183,28 @@ export class MockSystemGateway implements SystemGateway {
|
||||
async pickFolder(): Promise<string | null> {
|
||||
return "/home/user/mock-project";
|
||||
}
|
||||
|
||||
private exitGuardListeners = new Set<(state: AppExitWorkGuardState) => void>();
|
||||
/** Count of `confirmAppExit()` calls, for test assertions. */
|
||||
confirmAppExitCallCount = 0;
|
||||
|
||||
async onAppExitWorkGuard(
|
||||
handler: (state: AppExitWorkGuardState) => void,
|
||||
): Promise<Unsubscribe> {
|
||||
this.exitGuardListeners.add(handler);
|
||||
return () => {
|
||||
this.exitGuardListeners.delete(handler);
|
||||
};
|
||||
}
|
||||
|
||||
/** Test/dev helper to push an app-exit work guard state to all subscribers. */
|
||||
emitAppExitWorkGuard(state: AppExitWorkGuardState): void {
|
||||
for (const l of this.exitGuardListeners) l(state);
|
||||
}
|
||||
|
||||
async confirmAppExit(): Promise<void> {
|
||||
this.confirmAppExitCallCount += 1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -8,12 +8,15 @@ import { invoke } from "@tauri-apps/api/core";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { open } from "@tauri-apps/plugin-dialog";
|
||||
|
||||
import type { DomainEvent, HealthReport, Unsubscribe } from "@/domain";
|
||||
import type { AppExitWorkGuardState, DomainEvent, HealthReport, Unsubscribe } from "@/domain";
|
||||
import type { SystemGateway } from "@/ports";
|
||||
|
||||
/** Tauri event name carrying relayed domain events (mirror of `DOMAIN_EVENT`). */
|
||||
const DOMAIN_EVENT = "domain://event";
|
||||
|
||||
/** Tauri event name carrying the app-exit work guard state (ticket #83). */
|
||||
const APP_EXIT_WORK_GUARD = "app-exit-work-guard";
|
||||
|
||||
export class TauriSystemGateway implements SystemGateway {
|
||||
async health(note?: string): Promise<HealthReport> {
|
||||
// The backend command takes an optional `request: { note }` (camelCase).
|
||||
@ -37,4 +40,17 @@ export class TauriSystemGateway implements SystemGateway {
|
||||
// `open` with `multiple: false` returns a string when a path is chosen.
|
||||
return typeof result === "string" ? result : null;
|
||||
}
|
||||
|
||||
async onAppExitWorkGuard(
|
||||
handler: (state: AppExitWorkGuardState) => void,
|
||||
): Promise<Unsubscribe> {
|
||||
const unlisten = await listen<AppExitWorkGuardState>(APP_EXIT_WORK_GUARD, (e) => {
|
||||
handler(e.payload);
|
||||
});
|
||||
return unlisten;
|
||||
}
|
||||
|
||||
async confirmAppExit(): Promise<void> {
|
||||
await invoke("confirm_app_exit");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user