@myrialabs/clopen 0.2.11 → 0.2.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/backend/chat/stream-manager.ts +103 -9
- package/backend/database/queries/project-queries.ts +1 -4
- package/backend/database/queries/session-queries.ts +36 -1
- package/backend/database/queries/snapshot-queries.ts +122 -0
- package/backend/database/utils/connection.ts +17 -11
- package/backend/engine/adapters/claude/stream.ts +12 -2
- package/backend/index.ts +13 -2
- package/backend/snapshot/blob-store.ts +52 -72
- package/backend/snapshot/snapshot-service.ts +24 -0
- package/backend/terminal/stream-manager.ts +41 -2
- package/backend/ws/chat/stream.ts +14 -7
- package/backend/ws/engine/claude/accounts.ts +6 -8
- package/backend/ws/projects/crud.ts +72 -7
- package/backend/ws/sessions/crud.ts +119 -2
- package/backend/ws/system/operations.ts +14 -39
- package/frontend/components/auth/SetupPage.svelte +1 -1
- package/frontend/components/chat/input/ChatInput.svelte +14 -1
- package/frontend/components/chat/message/MessageBubble.svelte +13 -0
- package/frontend/components/common/form/FolderBrowser.svelte +17 -4
- package/frontend/components/common/overlay/Dialog.svelte +17 -15
- package/frontend/components/files/FileNode.svelte +0 -15
- package/frontend/components/history/HistoryModal.svelte +94 -19
- package/frontend/components/history/HistoryView.svelte +29 -36
- package/frontend/components/settings/engines/AIEnginesSettings.svelte +1 -1
- package/frontend/components/settings/general/DataManagementSettings.svelte +1 -54
- package/frontend/components/workspace/DesktopNavigator.svelte +57 -10
- package/frontend/components/workspace/MobileNavigator.svelte +57 -10
- package/frontend/components/workspace/WorkspaceLayout.svelte +0 -8
- package/frontend/services/chat/chat.service.ts +86 -13
- package/frontend/services/notification/global-stream-monitor.ts +5 -2
- package/frontend/stores/core/app.svelte.ts +10 -2
- package/frontend/stores/core/sessions.svelte.ts +4 -1
- package/package.json +1 -1
|
@@ -226,24 +226,26 @@
|
|
|
226
226
|
</div>
|
|
227
227
|
</div>
|
|
228
228
|
{/if}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
229
|
+
|
|
230
|
+
{#if !children}
|
|
231
|
+
<div class="flex justify-end gap-3 pt-2">
|
|
232
|
+
{#if showCancel}
|
|
233
|
+
<button
|
|
234
|
+
onclick={handleCancel}
|
|
235
|
+
class="px-6 py-2.5 bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 text-slate-700 dark:text-slate-300 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-700 transition-all duration-200 font-semibold"
|
|
236
|
+
>
|
|
237
|
+
{cancelText}
|
|
238
|
+
</button>
|
|
239
|
+
{/if}
|
|
232
240
|
<button
|
|
233
|
-
onclick={
|
|
234
|
-
|
|
241
|
+
onclick={handleConfirm}
|
|
242
|
+
disabled={confirmDisabled}
|
|
243
|
+
class="px-6 py-2.5 {colors.button} rounded-lg transition-all duration-200 font-semibold focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-current"
|
|
235
244
|
>
|
|
236
|
-
{
|
|
245
|
+
{confirmText}
|
|
237
246
|
</button>
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
onclick={handleConfirm}
|
|
241
|
-
disabled={confirmDisabled}
|
|
242
|
-
class="px-6 py-2.5 {colors.button} rounded-lg transition-all duration-200 font-semibold focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-current"
|
|
243
|
-
>
|
|
244
|
-
{confirmText}
|
|
245
|
-
</button>
|
|
246
|
-
</div>
|
|
247
|
+
</div>
|
|
248
|
+
{/if}
|
|
247
249
|
</div>
|
|
248
250
|
</div>
|
|
249
251
|
{/if}
|
|
@@ -116,14 +116,6 @@
|
|
|
116
116
|
onAction?.(action, file);
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
-
function formatFileSize(bytes: number): string {
|
|
120
|
-
if (bytes === 0) return '0 B';
|
|
121
|
-
const k = 1024;
|
|
122
|
-
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
123
|
-
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
124
|
-
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
|
125
|
-
}
|
|
126
|
-
|
|
127
119
|
// Close menu when clicking outside
|
|
128
120
|
onMount(() => {
|
|
129
121
|
function handleClickOutside(event: MouseEvent) {
|
|
@@ -184,13 +176,6 @@
|
|
|
184
176
|
{/if}
|
|
185
177
|
</span>
|
|
186
178
|
|
|
187
|
-
<!-- File metadata -->
|
|
188
|
-
{#if file.type === 'file'}
|
|
189
|
-
<span class="flex-shrink-0 text-xs text-slate-400 dark:text-slate-500 lg:group-hover:hidden">
|
|
190
|
-
{formatFileSize(file.size || 0)}
|
|
191
|
-
</span>
|
|
192
|
-
{/if}
|
|
193
|
-
|
|
194
179
|
<!-- Actions menu (always visible, triggered by click) -->
|
|
195
180
|
<div class="flex-shrink-0">
|
|
196
181
|
<div class="relative">
|
|
@@ -275,7 +275,7 @@
|
|
|
275
275
|
}
|
|
276
276
|
}
|
|
277
277
|
|
|
278
|
-
// Delete session state
|
|
278
|
+
// Delete single session state
|
|
279
279
|
let showDeleteDialog = $state(false);
|
|
280
280
|
let sessionToDelete = $state<ChatSession | null>(null);
|
|
281
281
|
|
|
@@ -299,7 +299,7 @@
|
|
|
299
299
|
addNotification({
|
|
300
300
|
type: 'success',
|
|
301
301
|
title: 'Session Deleted',
|
|
302
|
-
message: 'Chat session
|
|
302
|
+
message: 'Chat session and related data have been deleted',
|
|
303
303
|
duration: 3000
|
|
304
304
|
});
|
|
305
305
|
|
|
@@ -321,6 +321,54 @@
|
|
|
321
321
|
sessionToDelete = null;
|
|
322
322
|
}
|
|
323
323
|
|
|
324
|
+
// Delete all sessions state
|
|
325
|
+
let showDeleteAllDialog = $state(false);
|
|
326
|
+
let deletingAll = $state(false);
|
|
327
|
+
|
|
328
|
+
async function confirmDeleteAllSessions() {
|
|
329
|
+
deletingAll = true;
|
|
330
|
+
try {
|
|
331
|
+
const result = await ws.http('sessions:delete-all', {});
|
|
332
|
+
|
|
333
|
+
// Remove all project sessions from local state
|
|
334
|
+
const projectId = projectState.currentProject?.id;
|
|
335
|
+
if (projectId) {
|
|
336
|
+
const toRemove = sessionState.sessions
|
|
337
|
+
.filter(s => s.project_id === projectId)
|
|
338
|
+
.map(s => s.id);
|
|
339
|
+
for (const id of toRemove) {
|
|
340
|
+
removeSession(id);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// Clear cache
|
|
345
|
+
sessionDataCache = {};
|
|
346
|
+
|
|
347
|
+
addNotification({
|
|
348
|
+
type: 'success',
|
|
349
|
+
title: 'All Sessions Deleted',
|
|
350
|
+
message: `${result.deletedCount} sessions and related data have been deleted`,
|
|
351
|
+
duration: 3000
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
showDeleteAllDialog = false;
|
|
355
|
+
} catch (error) {
|
|
356
|
+
debug.error('session', 'Failed to delete all sessions:', error);
|
|
357
|
+
addNotification({
|
|
358
|
+
type: 'error',
|
|
359
|
+
title: 'Error',
|
|
360
|
+
message: 'Failed to delete all sessions',
|
|
361
|
+
duration: 5000
|
|
362
|
+
});
|
|
363
|
+
} finally {
|
|
364
|
+
deletingAll = false;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function closeDeleteAllDialog() {
|
|
369
|
+
showDeleteAllDialog = false;
|
|
370
|
+
}
|
|
371
|
+
|
|
324
372
|
function closeModal() {
|
|
325
373
|
searchQuery = '';
|
|
326
374
|
onClose();
|
|
@@ -331,21 +379,34 @@
|
|
|
331
379
|
{#snippet header()}
|
|
332
380
|
<div class="flex items-center justify-between px-4 py-3 md:px-6 md:py-4">
|
|
333
381
|
<h2 class="text-base md:text-lg font-bold text-slate-900 dark:text-slate-100">Sessions</h2>
|
|
334
|
-
<
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
382
|
+
<div class="flex items-center gap-2">
|
|
383
|
+
{#if filteredSessions.length > 0}
|
|
384
|
+
<button
|
|
385
|
+
type="button"
|
|
386
|
+
class="flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg text-xs font-medium text-red-600 dark:text-red-400 hover:bg-red-500/10 transition-colors"
|
|
387
|
+
onclick={() => (showDeleteAllDialog = true)}
|
|
388
|
+
aria-label="Delete all sessions"
|
|
389
|
+
>
|
|
390
|
+
<Icon name="lucide:trash-2" class="w-3.5 h-3.5" />
|
|
391
|
+
<span class="hidden sm:inline">Delete All</span>
|
|
392
|
+
</button>
|
|
393
|
+
{/if}
|
|
394
|
+
<button
|
|
395
|
+
type="button"
|
|
396
|
+
class="p-1.5 md:p-2 rounded-lg text-slate-500 hover:text-slate-900 dark:hover:text-slate-100 hover:bg-violet-500/10 transition-colors"
|
|
397
|
+
onclick={closeModal}
|
|
398
|
+
aria-label="Close modal"
|
|
399
|
+
>
|
|
400
|
+
<svg class="w-4 h-4 md:w-5 md:h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
401
|
+
<path
|
|
402
|
+
stroke-linecap="round"
|
|
403
|
+
stroke-linejoin="round"
|
|
404
|
+
stroke-width="2"
|
|
405
|
+
d="M6 18L18 6M6 6l12 12"
|
|
406
|
+
/>
|
|
407
|
+
</svg>
|
|
408
|
+
</button>
|
|
409
|
+
</div>
|
|
349
410
|
</div>
|
|
350
411
|
{/snippet}
|
|
351
412
|
|
|
@@ -514,14 +575,28 @@
|
|
|
514
575
|
{/snippet}
|
|
515
576
|
</Modal>
|
|
516
577
|
|
|
517
|
-
<!-- Delete Confirmation Dialog -->
|
|
578
|
+
<!-- Delete Single Session Confirmation Dialog -->
|
|
518
579
|
<Dialog
|
|
519
580
|
bind:isOpen={showDeleteDialog}
|
|
520
581
|
onClose={closeDeleteDialog}
|
|
521
582
|
type="error"
|
|
522
583
|
title="Delete Session"
|
|
523
|
-
message=
|
|
584
|
+
message={sessionToDelete && isSessionStreaming(sessionToDelete.id)
|
|
585
|
+
? 'This session is currently running. Deleting it will stop the active chat and permanently remove all messages, snapshots, and related data.'
|
|
586
|
+
: 'Are you sure you want to delete this session? All messages, snapshots, and related data will be permanently removed.'}
|
|
524
587
|
confirmText="Delete"
|
|
525
588
|
cancelText="Cancel"
|
|
526
589
|
onConfirm={confirmDeleteSession}
|
|
527
590
|
/>
|
|
591
|
+
|
|
592
|
+
<!-- Delete All Sessions Confirmation Dialog -->
|
|
593
|
+
<Dialog
|
|
594
|
+
bind:isOpen={showDeleteAllDialog}
|
|
595
|
+
onClose={closeDeleteAllDialog}
|
|
596
|
+
type="error"
|
|
597
|
+
title="Delete All Sessions"
|
|
598
|
+
message={`Are you sure you want to delete all ${filteredSessions.length} sessions in this project? All messages, snapshots, and related data will be permanently removed. This only affects the current project.`}
|
|
599
|
+
confirmText={deletingAll ? 'Deleting...' : 'Delete All'}
|
|
600
|
+
cancelText="Cancel"
|
|
601
|
+
onConfirm={confirmDeleteAllSessions}
|
|
602
|
+
/>
|
|
@@ -14,8 +14,17 @@
|
|
|
14
14
|
import { showConfirm } from '$frontend/stores/ui/dialog.svelte';
|
|
15
15
|
import Modal from '$frontend/components/common/overlay/Modal.svelte';
|
|
16
16
|
import TimelineModal from '../checkpoint/TimelineModal.svelte';
|
|
17
|
+
import { presenceState } from '$frontend/stores/core/presence.svelte';
|
|
17
18
|
import { debug } from '$shared/utils/logger';
|
|
18
19
|
|
|
20
|
+
// Check if a session has an active stream
|
|
21
|
+
function isSessionStreaming(chatSessionId: string): boolean {
|
|
22
|
+
for (const status of presenceState.statuses.values()) {
|
|
23
|
+
if (status.streams?.some(s => s.status === 'active' && s.chatSessionId === chatSessionId)) return true;
|
|
24
|
+
}
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
|
|
19
28
|
// Use real session data from session store
|
|
20
29
|
const sessions = $derived(sessionState.sessions);
|
|
21
30
|
|
|
@@ -303,27 +312,25 @@
|
|
|
303
312
|
async function deleteSession(session: ChatSession) {
|
|
304
313
|
const sessionData = await getSessionData(session.id);
|
|
305
314
|
const title = sessionData.title;
|
|
306
|
-
|
|
315
|
+
const streaming = isSessionStreaming(session.id);
|
|
316
|
+
|
|
307
317
|
const confirmed = await showConfirm({
|
|
308
318
|
title: 'Delete Session',
|
|
309
|
-
message:
|
|
319
|
+
message: streaming
|
|
320
|
+
? `This session "${title}" is currently running. Deleting it will stop the active chat and permanently remove all messages, snapshots, and related data.`
|
|
321
|
+
: `Are you sure you want to delete session "${title}"? All messages, snapshots, and related data will be permanently removed.`,
|
|
310
322
|
type: 'error',
|
|
311
323
|
confirmText: 'Delete',
|
|
312
324
|
cancelText: 'Cancel'
|
|
313
325
|
});
|
|
314
|
-
|
|
326
|
+
|
|
315
327
|
if (confirmed) {
|
|
316
328
|
try {
|
|
317
|
-
// Delete from database via WebSocket
|
|
318
329
|
await ws.http('sessions:delete', { id: session.id });
|
|
319
|
-
// Remove from local state
|
|
320
330
|
removeSession(session.id);
|
|
321
|
-
// Clear cache
|
|
322
331
|
delete sessionDataCache[session.id];
|
|
323
|
-
// User already knows session was deleted from UI update
|
|
324
332
|
} catch (error) {
|
|
325
333
|
addNotification({
|
|
326
|
-
|
|
327
334
|
type: 'error',
|
|
328
335
|
title: 'Error',
|
|
329
336
|
message: 'Failed to delete session',
|
|
@@ -332,11 +339,11 @@
|
|
|
332
339
|
}
|
|
333
340
|
}
|
|
334
341
|
}
|
|
335
|
-
|
|
342
|
+
|
|
336
343
|
async function clearHistory() {
|
|
337
344
|
const confirmed = await showConfirm({
|
|
338
345
|
title: 'Clear All Session History',
|
|
339
|
-
message: 'Are you sure you want to clear all session history?
|
|
346
|
+
message: 'Are you sure you want to clear all session history? All messages, snapshots, and related data will be permanently removed. This only affects the current project.',
|
|
340
347
|
type: 'error',
|
|
341
348
|
confirmText: 'Clear All',
|
|
342
349
|
cancelText: 'Cancel'
|
|
@@ -349,34 +356,20 @@
|
|
|
349
356
|
}
|
|
350
357
|
|
|
351
358
|
try {
|
|
352
|
-
|
|
353
|
-
const deletePromises = sessions.map(async (session) => {
|
|
354
|
-
try {
|
|
355
|
-
await ws.http('sessions:delete', { id: session.id });
|
|
356
|
-
// Remove from local state
|
|
357
|
-
removeSession(session.id);
|
|
358
|
-
// Clear cache
|
|
359
|
-
delete sessionDataCache[session.id];
|
|
360
|
-
return { success: true };
|
|
361
|
-
} catch (error) {
|
|
362
|
-
debug.error('session', `Error deleting session ${session.id}:`, error);
|
|
363
|
-
return { success: false, error: 'Failed to delete session' };
|
|
364
|
-
}
|
|
365
|
-
});
|
|
366
|
-
|
|
367
|
-
// Wait for all deletions to complete
|
|
368
|
-
const results = await Promise.all(deletePromises);
|
|
359
|
+
const result = await ws.http('sessions:delete-all', {});
|
|
369
360
|
|
|
370
|
-
//
|
|
371
|
-
const
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
type: 'warning',
|
|
375
|
-
title: 'Partial Deletion',
|
|
376
|
-
message: `Failed to delete ${failed.length} session(s)`,
|
|
377
|
-
duration: 5000
|
|
378
|
-
});
|
|
361
|
+
// Remove all sessions from local state
|
|
362
|
+
const toRemove = [...sessions].map(s => s.id);
|
|
363
|
+
for (const id of toRemove) {
|
|
364
|
+
removeSession(id);
|
|
379
365
|
}
|
|
366
|
+
|
|
367
|
+
addNotification({
|
|
368
|
+
type: 'success',
|
|
369
|
+
title: 'All Sessions Deleted',
|
|
370
|
+
message: `${result.deletedCount} sessions and related data have been deleted`,
|
|
371
|
+
duration: 3000
|
|
372
|
+
});
|
|
380
373
|
} catch (error) {
|
|
381
374
|
debug.error('session', 'Error clearing history:', error);
|
|
382
375
|
addNotification({
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
let openCodeCommandCopiedTimer: ReturnType<typeof setTimeout> | null = null;
|
|
74
74
|
|
|
75
75
|
// Debug PTY (xterm.js)
|
|
76
|
-
const showDebug = $state(
|
|
76
|
+
const showDebug = $state(true);
|
|
77
77
|
let debugTermContainer = $state<HTMLDivElement>();
|
|
78
78
|
let debugTerminal: Terminal | null = null;
|
|
79
79
|
let debugFitAddon: FitAddon | null = null;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { addNotification } from '$frontend/stores/ui/notification.svelte';
|
|
3
|
-
import { resetToDefaults } from '$frontend/stores/features/settings.svelte';
|
|
4
3
|
import { showConfirm } from '$frontend/stores/ui/dialog.svelte';
|
|
5
4
|
import Icon from '../../common/display/Icon.svelte';
|
|
6
5
|
import { debug } from '$shared/utils/logger';
|
|
@@ -41,68 +40,16 @@
|
|
|
41
40
|
}
|
|
42
41
|
}
|
|
43
42
|
|
|
44
|
-
async function resetSettings() {
|
|
45
|
-
const confirmed = await showConfirm({
|
|
46
|
-
title: 'Reset Settings',
|
|
47
|
-
message:
|
|
48
|
-
'Are you sure you want to reset all settings to defaults? This will not delete your projects or conversations.',
|
|
49
|
-
type: 'warning',
|
|
50
|
-
confirmText: 'Reset Settings',
|
|
51
|
-
cancelText: 'Cancel'
|
|
52
|
-
});
|
|
53
43
|
|
|
54
|
-
if (confirmed) {
|
|
55
|
-
resetToDefaults();
|
|
56
|
-
addNotification({
|
|
57
|
-
type: 'success',
|
|
58
|
-
title: 'Settings Reset',
|
|
59
|
-
message: 'All settings have been restored to defaults'
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
44
|
</script>
|
|
64
45
|
|
|
65
46
|
<div class="py-1">
|
|
66
47
|
<h3 class="text-base font-bold text-slate-900 dark:text-slate-100 mb-1.5">Data Management</h3>
|
|
67
48
|
<p class="text-sm text-slate-600 dark:text-slate-500 mb-5">
|
|
68
|
-
|
|
49
|
+
Manage your application data
|
|
69
50
|
</p>
|
|
70
51
|
|
|
71
52
|
<div class="flex flex-col gap-4">
|
|
72
|
-
<!-- Storage Info -->
|
|
73
|
-
<div
|
|
74
|
-
class="flex items-center gap-3.5 p-4 bg-slate-100/50 dark:bg-slate-800/50 border border-slate-200 dark:border-slate-700/50 rounded-lg"
|
|
75
|
-
>
|
|
76
|
-
<Icon name="lucide:hard-drive" class="w-5 h-5 text-slate-500 dark:text-slate-400" />
|
|
77
|
-
<div class="flex-1">
|
|
78
|
-
<div class="text-sm font-medium text-slate-900 dark:text-slate-100">Local Storage</div>
|
|
79
|
-
<div class="text-xs text-slate-600 dark:text-slate-400">Data is stored locally in your browser</div>
|
|
80
|
-
</div>
|
|
81
|
-
</div>
|
|
82
|
-
|
|
83
|
-
<!-- Action Cards -->
|
|
84
|
-
<div class="flex flex-col gap-3">
|
|
85
|
-
<!-- Reset Settings -->
|
|
86
|
-
<div
|
|
87
|
-
class="flex items-center justify-between gap-4 py-3 px-4 bg-slate-100/50 dark:bg-slate-800/50 border border-slate-200 dark:border-slate-700/50 rounded-lg"
|
|
88
|
-
>
|
|
89
|
-
<div class="flex items-center gap-3">
|
|
90
|
-
<Icon name="lucide:refresh-cw" class="w-4.5 h-4.5 text-slate-600 dark:text-slate-400" />
|
|
91
|
-
<div class="text-left">
|
|
92
|
-
<div class="text-sm font-medium text-slate-900 dark:text-slate-100">Reset Settings</div>
|
|
93
|
-
<div class="text-xs text-slate-600 dark:text-slate-400">Restore default settings</div>
|
|
94
|
-
</div>
|
|
95
|
-
</div>
|
|
96
|
-
<button
|
|
97
|
-
type="button"
|
|
98
|
-
class="px-4 py-2 bg-slate-200 dark:bg-slate-700 border border-slate-300 dark:border-slate-600 text-slate-700 dark:text-slate-300 rounded-lg text-sm font-medium cursor-pointer transition-all duration-150 hover:bg-slate-300 dark:hover:bg-slate-600 hover:border-slate-400 dark:hover:border-slate-500 disabled:opacity-60 disabled:cursor-not-allowed whitespace-nowrap"
|
|
99
|
-
onclick={resetSettings}
|
|
100
|
-
>
|
|
101
|
-
Reset
|
|
102
|
-
</button>
|
|
103
|
-
</div>
|
|
104
|
-
</div>
|
|
105
|
-
|
|
106
53
|
<!-- Danger Zone -->
|
|
107
54
|
<div class="mt-2">
|
|
108
55
|
<div class="flex items-center gap-2 mb-3 text-xs font-medium text-red-600 dark:text-red-400">
|
|
@@ -93,12 +93,15 @@
|
|
|
93
93
|
}
|
|
94
94
|
|
|
95
95
|
// Delete project
|
|
96
|
-
|
|
97
|
-
|
|
96
|
+
let deletingProject = $state(false);
|
|
97
|
+
|
|
98
|
+
async function confirmDeleteProject(mode: 'remove' | 'full') {
|
|
99
|
+
if (!projectToDelete || deletingProject) return;
|
|
98
100
|
const deleteId = projectToDelete.id!;
|
|
101
|
+
deletingProject = true;
|
|
99
102
|
|
|
100
103
|
try {
|
|
101
|
-
await ws.http('projects:delete', { id: deleteId });
|
|
104
|
+
await ws.http('projects:delete', { id: deleteId, mode });
|
|
102
105
|
removeProject(deleteId);
|
|
103
106
|
existingProjects = existingProjects.filter(p => p.id !== deleteId);
|
|
104
107
|
showDeleteDialog = false;
|
|
@@ -111,6 +114,8 @@
|
|
|
111
114
|
message: 'Failed to delete project',
|
|
112
115
|
duration: 5000
|
|
113
116
|
});
|
|
117
|
+
} finally {
|
|
118
|
+
deletingProject = false;
|
|
114
119
|
}
|
|
115
120
|
}
|
|
116
121
|
|
|
@@ -386,13 +391,55 @@
|
|
|
386
391
|
<Dialog
|
|
387
392
|
bind:isOpen={showDeleteDialog}
|
|
388
393
|
onClose={closeDeleteDialog}
|
|
389
|
-
type="
|
|
390
|
-
title="
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
394
|
+
type="warning"
|
|
395
|
+
title="Remove Project"
|
|
396
|
+
showCancel={false}
|
|
397
|
+
>
|
|
398
|
+
{#snippet children()}
|
|
399
|
+
<div class="flex items-start space-x-4">
|
|
400
|
+
<div class="bg-amber-50 dark:bg-amber-900/20 border-amber-200 dark:border-amber-700/50 rounded-xl p-3 border">
|
|
401
|
+
<Icon name="lucide:triangle-alert" class="w-6 h-6 text-amber-600 dark:text-amber-400" />
|
|
402
|
+
</div>
|
|
403
|
+
<div class="flex-1">
|
|
404
|
+
<h3 class="text-lg font-semibold text-slate-900 dark:text-slate-100">Remove Project</h3>
|
|
405
|
+
<p class="text-sm text-slate-600 dark:text-slate-400 leading-relaxed">
|
|
406
|
+
How would you like to remove <strong>"{projectToDelete?.name}"</strong>?
|
|
407
|
+
</p>
|
|
408
|
+
</div>
|
|
409
|
+
</div>
|
|
410
|
+
<div class="flex flex-col gap-2 pt-2">
|
|
411
|
+
<button
|
|
412
|
+
onclick={() => confirmDeleteProject('remove')}
|
|
413
|
+
disabled={deletingProject}
|
|
414
|
+
class="w-full flex items-center gap-3 p-3 bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-700 transition-all duration-200 text-left disabled:opacity-50"
|
|
415
|
+
>
|
|
416
|
+
<Icon name="lucide:eye-off" class="w-5 h-5 text-slate-500 shrink-0" />
|
|
417
|
+
<div class="flex-1 min-w-0">
|
|
418
|
+
<p class="text-sm font-semibold text-slate-900 dark:text-slate-100">Remove from list</p>
|
|
419
|
+
<p class="text-xs text-slate-500 dark:text-slate-400">Sessions will be restored when you re-add this project.</p>
|
|
420
|
+
</div>
|
|
421
|
+
</button>
|
|
422
|
+
<button
|
|
423
|
+
onclick={() => confirmDeleteProject('full')}
|
|
424
|
+
disabled={deletingProject}
|
|
425
|
+
class="w-full flex items-center gap-3 p-3 bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-700 transition-all duration-200 text-left disabled:opacity-50"
|
|
426
|
+
>
|
|
427
|
+
<Icon name="lucide:trash-2" class="w-5 h-5 text-slate-500 shrink-0" />
|
|
428
|
+
<div class="flex-1 min-w-0">
|
|
429
|
+
<p class="text-sm font-semibold text-slate-900 dark:text-slate-100">Delete with all data</p>
|
|
430
|
+
<p class="text-xs text-slate-500 dark:text-slate-400">Delete all sessions, snapshots, and related data.</p>
|
|
431
|
+
</div>
|
|
432
|
+
</button>
|
|
433
|
+
<button
|
|
434
|
+
onclick={closeDeleteDialog}
|
|
435
|
+
class="w-full py-2 text-sm font-semibold text-slate-600 dark:text-slate-400 bg-slate-100 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg hover:bg-slate-200 dark:hover:bg-slate-700 transition-colors"
|
|
436
|
+
>
|
|
437
|
+
Cancel
|
|
438
|
+
</button>
|
|
439
|
+
<p class="text-xs text-slate-400 dark:text-slate-500 text-center">Your project folder on disk will not be affected.</p>
|
|
440
|
+
</div>
|
|
441
|
+
{/snippet}
|
|
442
|
+
</Dialog>
|
|
396
443
|
|
|
397
444
|
<!-- Tunnel Modal -->
|
|
398
445
|
<TunnelModal bind:isOpen={showTunnelModal} onClose={() => (showTunnelModal = false)} />
|
|
@@ -87,12 +87,15 @@
|
|
|
87
87
|
showDeleteDialog = true;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
|
|
91
|
-
|
|
90
|
+
let deletingProject = $state(false);
|
|
91
|
+
|
|
92
|
+
async function confirmDeleteProject(mode: 'remove' | 'full') {
|
|
93
|
+
if (!projectToDelete || deletingProject) return;
|
|
92
94
|
const deleteId = projectToDelete.id!;
|
|
95
|
+
deletingProject = true;
|
|
93
96
|
|
|
94
97
|
try {
|
|
95
|
-
await ws.http('projects:delete', { id: deleteId });
|
|
98
|
+
await ws.http('projects:delete', { id: deleteId, mode });
|
|
96
99
|
removeProject(deleteId);
|
|
97
100
|
showDeleteDialog = false;
|
|
98
101
|
projectToDelete = null;
|
|
@@ -104,6 +107,8 @@
|
|
|
104
107
|
message: 'Failed to delete project',
|
|
105
108
|
duration: 5000
|
|
106
109
|
});
|
|
110
|
+
} finally {
|
|
111
|
+
deletingProject = false;
|
|
107
112
|
}
|
|
108
113
|
}
|
|
109
114
|
|
|
@@ -368,13 +373,55 @@
|
|
|
368
373
|
<Dialog
|
|
369
374
|
bind:isOpen={showDeleteDialog}
|
|
370
375
|
onClose={closeDeleteDialog}
|
|
371
|
-
type="
|
|
372
|
-
title="
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
376
|
+
type="warning"
|
|
377
|
+
title="Remove Project"
|
|
378
|
+
showCancel={false}
|
|
379
|
+
>
|
|
380
|
+
{#snippet children()}
|
|
381
|
+
<div class="flex items-start space-x-4">
|
|
382
|
+
<div class="bg-amber-50 dark:bg-amber-900/20 border-amber-200 dark:border-amber-700/50 rounded-xl p-3 border">
|
|
383
|
+
<Icon name="lucide:triangle-alert" class="w-6 h-6 text-amber-600 dark:text-amber-400" />
|
|
384
|
+
</div>
|
|
385
|
+
<div class="flex-1">
|
|
386
|
+
<h3 class="text-lg font-semibold text-slate-900 dark:text-slate-100">Remove Project</h3>
|
|
387
|
+
<p class="text-sm text-slate-600 dark:text-slate-400 leading-relaxed">
|
|
388
|
+
How would you like to remove <strong>"{projectToDelete?.name}"</strong>?
|
|
389
|
+
</p>
|
|
390
|
+
</div>
|
|
391
|
+
</div>
|
|
392
|
+
<div class="flex flex-col gap-2 pt-2">
|
|
393
|
+
<button
|
|
394
|
+
onclick={() => confirmDeleteProject('remove')}
|
|
395
|
+
disabled={deletingProject}
|
|
396
|
+
class="w-full flex items-center gap-3 p-3 bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-700 transition-all duration-200 text-left disabled:opacity-50"
|
|
397
|
+
>
|
|
398
|
+
<Icon name="lucide:eye-off" class="w-5 h-5 text-slate-500 shrink-0" />
|
|
399
|
+
<div class="flex-1 min-w-0">
|
|
400
|
+
<p class="text-sm font-semibold text-slate-900 dark:text-slate-100">Remove from list</p>
|
|
401
|
+
<p class="text-xs text-slate-500 dark:text-slate-400">Sessions will be restored when you re-add this project.</p>
|
|
402
|
+
</div>
|
|
403
|
+
</button>
|
|
404
|
+
<button
|
|
405
|
+
onclick={() => confirmDeleteProject('full')}
|
|
406
|
+
disabled={deletingProject}
|
|
407
|
+
class="w-full flex items-center gap-3 p-3 bg-slate-50 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-700 transition-all duration-200 text-left disabled:opacity-50"
|
|
408
|
+
>
|
|
409
|
+
<Icon name="lucide:trash-2" class="w-5 h-5 text-slate-500 shrink-0" />
|
|
410
|
+
<div class="flex-1 min-w-0">
|
|
411
|
+
<p class="text-sm font-semibold text-slate-900 dark:text-slate-100">Delete with all data</p>
|
|
412
|
+
<p class="text-xs text-slate-500 dark:text-slate-400">Delete all sessions, snapshots, and related data.</p>
|
|
413
|
+
</div>
|
|
414
|
+
</button>
|
|
415
|
+
<button
|
|
416
|
+
onclick={closeDeleteDialog}
|
|
417
|
+
class="w-full py-2 text-sm font-semibold text-slate-600 dark:text-slate-400 bg-slate-100 dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg hover:bg-slate-200 dark:hover:bg-slate-700 transition-colors"
|
|
418
|
+
>
|
|
419
|
+
Cancel
|
|
420
|
+
</button>
|
|
421
|
+
<p class="text-xs text-slate-400 dark:text-slate-500 text-center">Your project folder on disk will not be affected.</p>
|
|
422
|
+
</div>
|
|
423
|
+
{/snippet}
|
|
424
|
+
</Dialog>
|
|
378
425
|
|
|
379
426
|
<!-- Tunnel Modal -->
|
|
380
427
|
<TunnelModal bind:isOpen={showTunnelModal} onClose={() => (showTunnelModal = false)} />
|
|
@@ -139,14 +139,6 @@
|
|
|
139
139
|
<div
|
|
140
140
|
class="h-full w-full overflow-hidden {isMobile ? 'bg-white/90 dark:bg-slate-900/98' : 'bg-slate-50 dark:bg-slate-900/70'} text-slate-900 dark:text-slate-100 font-sans"
|
|
141
141
|
>
|
|
142
|
-
<!-- Skip link for accessibility -->
|
|
143
|
-
<a
|
|
144
|
-
href="#main-content"
|
|
145
|
-
class="sr-only focus:not-sr-only focus:absolute focus:z-50 focus:p-4 focus:bg-violet-600 focus:text-white"
|
|
146
|
-
>
|
|
147
|
-
Skip to main content
|
|
148
|
-
</a>
|
|
149
|
-
|
|
150
142
|
{#if isMobile}
|
|
151
143
|
<!-- Mobile Layout -->
|
|
152
144
|
<div class="flex flex-col h-full w-full" in:fade={{ duration: 200 }}>
|