feat: add working directory deletion option to quit confirmation modal

Add agent name input with confirmation pattern to enable a "Quit & Delete
Working Dirs" button. Widen modal, reduce button size/font, prevent wrapping.
Guard against window.maestro timing issues in production.
This commit is contained in:
Pedram Amini
2026-02-05 21:09:08 -06:00
parent 0502f2c7a4
commit 4b7ef9c213
4 changed files with 71 additions and 5 deletions

View File

@@ -851,6 +851,12 @@ function MaestroConsoleInner() {
window.maestro.app.confirmQuit();
}, []);
const handleConfirmQuitAndDelete = useCallback(() => {
setQuitConfirmModalOpen(false);
// TODO: Implement working directory deletion before quit
window.maestro.app.confirmQuit();
}, []);
const handleCancelQuit = useCallback(() => {
setQuitConfirmModalOpen(false);
window.maestro.app.cancelQuit();
@@ -4135,6 +4141,10 @@ function MaestroConsoleInner() {
// Quit confirmation handler - shows modal when trying to quit with busy agents
useEffect(() => {
// Guard against window.maestro not being defined yet (production timing)
if (!window.maestro?.app?.onQuitConfirmationRequest) {
return;
}
const unsubscribe = window.maestro.app.onQuitConfirmationRequest(() => {
// Get all busy AI sessions (agents that are actively thinking)
const busyAgents = sessions.filter(
@@ -13518,6 +13528,7 @@ You are taking over this conversation. Based on the context above, provide a bri
onCloseConfirmModal={handleCloseConfirmModal}
quitConfirmModalOpen={quitConfirmModalOpen}
onConfirmQuit={handleConfirmQuit}
onConfirmQuitAndDelete={handleConfirmQuitAndDelete}
onCancelQuit={handleCancelQuit}
// AppSessionModals props
newInstanceModalOpen={newInstanceModalOpen}

View File

@@ -291,6 +291,7 @@ export interface AppConfirmModalsProps {
// Quit Confirm Modal
quitConfirmModalOpen: boolean;
onConfirmQuit: () => void;
onConfirmQuitAndDelete: () => void;
onCancelQuit: () => void;
}
@@ -312,6 +313,7 @@ export function AppConfirmModals({
// Quit Confirm Modal
quitConfirmModalOpen,
onConfirmQuit,
onConfirmQuitAndDelete,
onCancelQuit,
}: AppConfirmModalsProps) {
// Compute busy agents for QuitConfirmModal
@@ -338,6 +340,7 @@ export function AppConfirmModals({
busyAgentCount={busyAgents.length}
busyAgentNames={busyAgents.map((s) => s.name)}
onConfirmQuit={onConfirmQuit}
onConfirmQuitAndDelete={onConfirmQuitAndDelete}
onCancel={onCancelQuit}
/>
)}
@@ -1742,6 +1745,7 @@ export interface AppModalsProps {
onCloseConfirmModal: () => void;
quitConfirmModalOpen: boolean;
onConfirmQuit: () => void;
onConfirmQuitAndDelete: () => void;
onCancelQuit: () => void;
// --- AppSessionModals props ---
@@ -2097,6 +2101,7 @@ export function AppModals(props: AppModalsProps) {
onCloseConfirmModal,
quitConfirmModalOpen,
onConfirmQuit,
onConfirmQuitAndDelete,
onCancelQuit,
// Session modals
newInstanceModalOpen,
@@ -2369,6 +2374,7 @@ export function AppModals(props: AppModalsProps) {
onCloseConfirmModal={onCloseConfirmModal}
quitConfirmModalOpen={quitConfirmModalOpen}
onConfirmQuit={onConfirmQuit}
onConfirmQuitAndDelete={onConfirmQuitAndDelete}
onCancelQuit={onCancelQuit}
/>

View File

@@ -6,7 +6,7 @@
* Focus defaults to Cancel to prevent accidental data loss.
*/
import { useEffect, useRef } from 'react';
import { useEffect, useRef, useState } from 'react';
import { AlertTriangle } from 'lucide-react';
import type { Theme } from '../types';
import { useLayerStack } from '../contexts/LayerStackContext';
@@ -20,6 +20,8 @@ interface QuitConfirmModalProps {
busyAgentNames: string[];
/** Callback when user confirms quit */
onConfirmQuit: () => void;
/** Callback when user confirms quit and requests working directory deletion */
onConfirmQuitAndDelete: () => void;
/** Callback when user cancels (stays in app) */
onCancel: () => void;
}
@@ -35,6 +37,7 @@ export function QuitConfirmModal({
busyAgentCount,
busyAgentNames,
onConfirmQuit,
onConfirmQuitAndDelete,
onCancel,
}: QuitConfirmModalProps): JSX.Element {
const { registerLayer, unregisterLayer, updateLayerHandler } = useLayerStack();
@@ -42,6 +45,12 @@ export function QuitConfirmModal({
const cancelButtonRef = useRef<HTMLButtonElement>(null);
const onCancelRef = useRef(onCancel);
onCancelRef.current = onCancel;
const [confirmName, setConfirmName] = useState('');
// Check if typed name matches any busy agent name (case-insensitive)
const deleteEnabled = busyAgentNames.some(
(name) => name.toLowerCase() === confirmName.trim().toLowerCase()
);
// Focus Cancel button on mount (safer default action)
useEffect(() => {
@@ -98,7 +107,7 @@ export function QuitConfirmModal({
onKeyDown={handleKeyDown}
>
<div
className="w-[450px] border rounded-xl shadow-2xl overflow-hidden"
className="w-[520px] border rounded-xl shadow-2xl overflow-hidden"
style={{
backgroundColor: theme.colors.bgSidebar,
borderColor: theme.colors.border,
@@ -170,11 +179,47 @@ export function QuitConfirmModal({
</div>
</div>
{/* Agent name confirmation input for working directory deletion */}
<div className="mt-4">
<label
className="block text-xs mb-1.5"
style={{ color: theme.colors.textDim }}
htmlFor="quit-confirm-agent-name"
>
Enter agent name below to enable working directory deletion
</label>
<input
id="quit-confirm-agent-name"
type="text"
value={confirmName}
onChange={(e) => setConfirmName(e.target.value)}
placeholder=""
className="w-full px-3 py-1.5 rounded-lg border text-xs outline-none focus:ring-1"
style={{
backgroundColor: theme.colors.bgMain,
borderColor: theme.colors.border,
color: theme.colors.textMain,
}}
/>
</div>
{/* Actions */}
<div className="mt-6 flex justify-end gap-3">
<div className="mt-5 flex items-center justify-end gap-2 flex-nowrap">
<button
onClick={onConfirmQuitAndDelete}
disabled={!deleteEnabled}
className="px-3 py-1.5 rounded-lg text-xs font-medium transition-colors whitespace-nowrap"
style={{
backgroundColor: deleteEnabled ? theme.colors.error : `${theme.colors.error}40`,
color: deleteEnabled ? '#ffffff' : `#ffffff80`,
cursor: deleteEnabled ? 'pointer' : 'not-allowed',
}}
>
Quit & Delete Working Dirs
</button>
<button
onClick={onConfirmQuit}
className="px-4 py-2 rounded-lg text-sm font-medium transition-colors hover:opacity-90"
className="px-3 py-1.5 rounded-lg text-xs font-medium transition-colors hover:opacity-90 whitespace-nowrap"
style={{
backgroundColor: theme.colors.error,
color: '#ffffff',
@@ -185,7 +230,7 @@ export function QuitConfirmModal({
<button
ref={cancelButtonRef}
onClick={onCancel}
className="px-4 py-2 rounded-lg text-sm font-medium outline-none focus:ring-2 focus:ring-offset-1 transition-colors"
className="px-3 py-1.5 rounded-lg text-xs font-medium outline-none focus:ring-2 focus:ring-offset-1 transition-colors whitespace-nowrap"
style={{
backgroundColor: theme.colors.accent,
color: theme.colors.accentForeground,

View File

@@ -1815,6 +1815,10 @@ export function useSettings(): UseSettingsReturn {
// Reload settings when system resumes from sleep/suspend
// This ensures settings like maxOutputLines aren't reset to defaults
useEffect(() => {
// Guard against window.maestro not being defined yet (production timing)
if (!window.maestro?.app?.onSystemResume) {
return;
}
const cleanup = window.maestro.app.onSystemResume(() => {
console.log('[Settings] System resumed from sleep, reloading settings');
loadSettings();