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

View File

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

View File

@@ -6,7 +6,7 @@
* Focus defaults to Cancel to prevent accidental data loss. * 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 { AlertTriangle } from 'lucide-react';
import type { Theme } from '../types'; import type { Theme } from '../types';
import { useLayerStack } from '../contexts/LayerStackContext'; import { useLayerStack } from '../contexts/LayerStackContext';
@@ -20,6 +20,8 @@ interface QuitConfirmModalProps {
busyAgentNames: string[]; busyAgentNames: string[];
/** Callback when user confirms quit */ /** Callback when user confirms quit */
onConfirmQuit: () => void; onConfirmQuit: () => void;
/** Callback when user confirms quit and requests working directory deletion */
onConfirmQuitAndDelete: () => void;
/** Callback when user cancels (stays in app) */ /** Callback when user cancels (stays in app) */
onCancel: () => void; onCancel: () => void;
} }
@@ -35,6 +37,7 @@ export function QuitConfirmModal({
busyAgentCount, busyAgentCount,
busyAgentNames, busyAgentNames,
onConfirmQuit, onConfirmQuit,
onConfirmQuitAndDelete,
onCancel, onCancel,
}: QuitConfirmModalProps): JSX.Element { }: QuitConfirmModalProps): JSX.Element {
const { registerLayer, unregisterLayer, updateLayerHandler } = useLayerStack(); const { registerLayer, unregisterLayer, updateLayerHandler } = useLayerStack();
@@ -42,6 +45,12 @@ export function QuitConfirmModal({
const cancelButtonRef = useRef<HTMLButtonElement>(null); const cancelButtonRef = useRef<HTMLButtonElement>(null);
const onCancelRef = useRef(onCancel); const onCancelRef = useRef(onCancel);
onCancelRef.current = 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) // Focus Cancel button on mount (safer default action)
useEffect(() => { useEffect(() => {
@@ -98,7 +107,7 @@ export function QuitConfirmModal({
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
> >
<div <div
className="w-[450px] border rounded-xl shadow-2xl overflow-hidden" className="w-[520px] border rounded-xl shadow-2xl overflow-hidden"
style={{ style={{
backgroundColor: theme.colors.bgSidebar, backgroundColor: theme.colors.bgSidebar,
borderColor: theme.colors.border, borderColor: theme.colors.border,
@@ -170,11 +179,47 @@ export function QuitConfirmModal({
</div> </div>
</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 */} {/* 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 <button
onClick={onConfirmQuit} 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={{ style={{
backgroundColor: theme.colors.error, backgroundColor: theme.colors.error,
color: '#ffffff', color: '#ffffff',
@@ -185,7 +230,7 @@ export function QuitConfirmModal({
<button <button
ref={cancelButtonRef} ref={cancelButtonRef}
onClick={onCancel} 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={{ style={{
backgroundColor: theme.colors.accent, backgroundColor: theme.colors.accent,
color: theme.colors.accentForeground, color: theme.colors.accentForeground,

View File

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