generated from nhcarrigan/template
feat: add close confirmation modal with minimize to tray option
Implemented a confirmation modal when users try to close the application: - Modal always shows with three options: Cancel, Minimize to Tray, Close Application - Detects if Claude is actively running and shows appropriate warning message - Removed minimize_to_tray config setting (no longer needed) - Added core:window:allow-hide permission for window hiding - Created CloseAppConfirmModal component with keyboard shortcuts (Escape to cancel) - Added close_application command to properly exit the app - Backend emits window-close-requested event for frontend to handle This provides better UX by giving users clear choices every time they close, preventing accidental closures during active work sessions.
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
<script lang="ts">
|
||||
interface Props {
|
||||
isOpen: boolean;
|
||||
hasActiveConversation: boolean;
|
||||
onClose: () => void;
|
||||
onMinimize: () => void;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
const { isOpen, hasActiveConversation, onClose, onMinimize, onCancel }: Props = $props();
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if (!isOpen) return;
|
||||
|
||||
if (event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
onCancel();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
{#if isOpen}
|
||||
<div
|
||||
class="fixed inset-0 bg-black/50 backdrop-blur-sm z-50 flex items-center justify-center p-4"
|
||||
onclick={onCancel}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
onkeydown={(e) => e.key === " " && onCancel()}
|
||||
>
|
||||
<div
|
||||
class="bg-[var(--bg-primary)] border border-[var(--border-color)] rounded-lg shadow-xl max-w-md w-full"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
onkeydown={(e) => e.stopPropagation()}
|
||||
role="dialog"
|
||||
aria-labelledby="confirm-title"
|
||||
aria-describedby="confirm-message"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div class="p-6">
|
||||
<div class="flex items-start gap-4">
|
||||
<div
|
||||
class="w-10 h-10 rounded-lg bg-yellow-500/20 flex items-center justify-center flex-shrink-0"
|
||||
>
|
||||
<svg
|
||||
class="w-6 h-6 text-yellow-500"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<h3 id="confirm-title" class="text-lg font-semibold text-[var(--text-primary)] mb-1">
|
||||
Close Hikari Desktop?
|
||||
</h3>
|
||||
<p id="confirm-message" class="text-sm text-[var(--text-secondary)]">
|
||||
{#if hasActiveConversation}
|
||||
You have an active conversation with Claude. Are you sure you want to close the
|
||||
application? Your conversation history will be saved, but any in-progress tasks will
|
||||
be interrupted.
|
||||
{:else}
|
||||
Are you sure you want to close the application?
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3 mt-6 justify-end">
|
||||
<button
|
||||
onclick={onCancel}
|
||||
class="px-4 py-2 text-sm font-medium text-gray-300 bg-[var(--bg-secondary)] hover:bg-[var(--bg-tertiary)] border border-[var(--border-color)] rounded-lg transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onclick={onMinimize}
|
||||
class="px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors"
|
||||
>
|
||||
Minimize to Tray
|
||||
</button>
|
||||
<button
|
||||
onclick={onClose}
|
||||
class="px-4 py-2 text-sm font-medium text-white bg-red-600 hover:bg-red-700 rounded-lg transition-colors"
|
||||
>
|
||||
Close Application
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
[role="dialog"] {
|
||||
animation: slideIn 0.2s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: scale(0.95) translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: scale(1) translateY(0);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -26,7 +26,6 @@
|
||||
notifications_enabled: true,
|
||||
notification_volume: 0.7,
|
||||
always_on_top: false,
|
||||
minimize_to_tray: false,
|
||||
update_checks_enabled: true,
|
||||
character_panel_width: null,
|
||||
font_size: 14,
|
||||
@@ -728,21 +727,6 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Minimize to Tray Toggle -->
|
||||
<div class="mb-4">
|
||||
<label class="flex items-center gap-3 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={config.minimize_to_tray}
|
||||
class="w-4 h-4 text-[var(--accent-primary)] bg-[var(--bg-primary)] border-[var(--border-color)] rounded focus:ring-[var(--accent-primary)] focus:ring-2"
|
||||
/>
|
||||
<span class="text-sm text-[var(--text-primary)]">Minimize to system tray</span>
|
||||
</label>
|
||||
<p class="text-xs text-[var(--text-tertiary)] mt-1 ml-7">
|
||||
Hide to tray instead of closing when you click the X button
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Update Checks Toggle -->
|
||||
<div class="mb-4">
|
||||
<label class="flex items-center gap-3 cursor-pointer">
|
||||
|
||||
@@ -70,7 +70,6 @@
|
||||
update_checks_enabled: true,
|
||||
character_panel_width: null,
|
||||
font_size: 14,
|
||||
minimize_to_tray: false,
|
||||
streamer_mode: false,
|
||||
streamer_hide_paths: false,
|
||||
compact_mode: false,
|
||||
|
||||
@@ -167,7 +167,6 @@ describe("config store", () => {
|
||||
notifications_enabled: true,
|
||||
notification_volume: 0.7,
|
||||
always_on_top: false,
|
||||
minimize_to_tray: true,
|
||||
update_checks_enabled: true,
|
||||
character_panel_width: 300,
|
||||
font_size: 14,
|
||||
@@ -213,7 +212,6 @@ describe("config store", () => {
|
||||
notifications_enabled: true,
|
||||
notification_volume: 0.7,
|
||||
always_on_top: false,
|
||||
minimize_to_tray: false,
|
||||
update_checks_enabled: true,
|
||||
character_panel_width: null,
|
||||
font_size: 14,
|
||||
|
||||
@@ -27,7 +27,6 @@ export interface HikariConfig {
|
||||
notifications_enabled: boolean;
|
||||
notification_volume: number;
|
||||
always_on_top: boolean;
|
||||
minimize_to_tray: boolean;
|
||||
update_checks_enabled: boolean;
|
||||
character_panel_width: number | null;
|
||||
font_size: number;
|
||||
@@ -60,7 +59,6 @@ const defaultConfig: HikariConfig = {
|
||||
notifications_enabled: true,
|
||||
notification_volume: 0.7,
|
||||
always_on_top: false,
|
||||
minimize_to_tray: false,
|
||||
update_checks_enabled: true,
|
||||
character_panel_width: null,
|
||||
font_size: 14,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { onMount, onDestroy } from "svelte";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { get } from "svelte/store";
|
||||
import {
|
||||
initializeTauriListeners,
|
||||
@@ -31,6 +32,7 @@
|
||||
import AchievementNotification from "$lib/components/AchievementNotification.svelte";
|
||||
import AchievementsPanel from "$lib/components/AchievementsPanel.svelte";
|
||||
import UpdateNotification from "$lib/components/UpdateNotification.svelte";
|
||||
import CloseAppConfirmModal from "$lib/components/CloseAppConfirmModal.svelte";
|
||||
import { debugConsoleStore } from "$lib/stores/debugConsole";
|
||||
|
||||
let initialized = false;
|
||||
@@ -38,6 +40,8 @@
|
||||
let achievementPanelOpen = $state(false);
|
||||
let currentCharacterState: CharacterState = $state("idle");
|
||||
let compactModeActive = $state(false);
|
||||
let closeConfirmModalOpen = $state(false);
|
||||
let hasActiveConversation = $state(false);
|
||||
|
||||
// Editor state
|
||||
const isEditorVisible = editorStore.isEditorVisible;
|
||||
@@ -350,6 +354,50 @@
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCloseRequest() {
|
||||
// Check if there's an active conversation with Claude running
|
||||
const activeId = get(claudeStore.activeConversationId);
|
||||
if (activeId) {
|
||||
try {
|
||||
const isRunning = await invoke<boolean>("is_claude_running", {
|
||||
conversationId: activeId,
|
||||
});
|
||||
hasActiveConversation = isRunning;
|
||||
} catch (error) {
|
||||
console.error("Failed to check Claude status:", error);
|
||||
hasActiveConversation = false;
|
||||
}
|
||||
} else {
|
||||
hasActiveConversation = false;
|
||||
}
|
||||
|
||||
// Always show confirmation modal
|
||||
closeConfirmModalOpen = true;
|
||||
}
|
||||
|
||||
async function handleConfirmClose() {
|
||||
closeConfirmModalOpen = false;
|
||||
try {
|
||||
await invoke("close_application");
|
||||
} catch (error) {
|
||||
console.error("Failed to close application:", error);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleMinimizeToTray() {
|
||||
closeConfirmModalOpen = false;
|
||||
try {
|
||||
const window = getCurrentWindow();
|
||||
await window.hide();
|
||||
} catch (error) {
|
||||
console.error("Failed to minimize to tray:", error);
|
||||
}
|
||||
}
|
||||
|
||||
function handleCancelClose() {
|
||||
closeConfirmModalOpen = false;
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
if (!initialized) {
|
||||
initialized = true;
|
||||
@@ -395,6 +443,16 @@
|
||||
|
||||
// Initialize Discord RPC
|
||||
await initializeDiscordRpc();
|
||||
|
||||
// Listen for window close requests
|
||||
const unlisten = await listen("window-close-requested", () => {
|
||||
handleCloseRequest();
|
||||
});
|
||||
|
||||
// Store the unlisten function for cleanup
|
||||
window.addEventListener("beforeunload", () => {
|
||||
unlisten();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -461,6 +519,13 @@
|
||||
onClose={() => (achievementPanelOpen = false)}
|
||||
/>
|
||||
<UpdateNotification bind:this={updateNotification} />
|
||||
<CloseAppConfirmModal
|
||||
isOpen={closeConfirmModalOpen}
|
||||
{hasActiveConversation}
|
||||
onClose={handleConfirmClose}
|
||||
onMinimize={handleMinimizeToTray}
|
||||
onCancel={handleCancelClose}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user