@myrialabs/clopen 0.2.10 → 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/README.md +61 -27
- package/backend/chat/stream-manager.ts +114 -16
- 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/engine/adapters/opencode/stream.ts +37 -19
- package/backend/index.ts +18 -2
- package/backend/mcp/servers/browser-automation/browser.ts +2 -0
- package/backend/preview/browser/browser-mcp-control.ts +16 -0
- package/backend/preview/browser/browser-navigation-tracker.ts +31 -3
- package/backend/preview/browser/browser-preview-service.ts +0 -34
- package/backend/preview/browser/browser-video-capture.ts +13 -1
- package/backend/preview/browser/scripts/audio-stream.ts +5 -0
- package/backend/preview/browser/types.ts +7 -6
- 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/preview/browser/interact.ts +46 -50
- package/backend/ws/preview/browser/webcodecs.ts +24 -15
- 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/feedback/NotificationToast.svelte +26 -11
- package/frontend/components/common/form/FolderBrowser.svelte +17 -4
- package/frontend/components/common/overlay/Dialog.svelte +17 -15
- package/frontend/components/files/FileNode.svelte +16 -73
- package/frontend/components/git/CommitForm.svelte +1 -1
- package/frontend/components/history/HistoryModal.svelte +94 -19
- package/frontend/components/history/HistoryView.svelte +29 -36
- package/frontend/components/preview/browser/components/Canvas.svelte +119 -42
- package/frontend/components/preview/browser/components/Container.svelte +18 -3
- package/frontend/components/preview/browser/components/Toolbar.svelte +23 -21
- package/frontend/components/preview/browser/core/coordinator.svelte.ts +13 -1
- package/frontend/components/preview/browser/core/stream-handler.svelte.ts +31 -7
- package/frontend/components/preview/browser/core/tab-operations.svelte.ts +5 -4
- 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 +111 -16
- package/frontend/services/notification/global-stream-monitor.ts +5 -2
- package/frontend/services/notification/push.service.ts +2 -2
- package/frontend/services/preview/browser/browser-webcodecs.service.ts +170 -46
- package/frontend/stores/core/app.svelte.ts +10 -2
- package/frontend/stores/core/sessions.svelte.ts +4 -1
- package/package.json +2 -2
|
@@ -64,6 +64,19 @@
|
|
|
64
64
|
}
|
|
65
65
|
});
|
|
66
66
|
});
|
|
67
|
+
|
|
68
|
+
// Force reactive tracking for assistant text streaming.
|
|
69
|
+
// Without an explicit $effect that reads partialText, Svelte 5's derived chain
|
|
70
|
+
// may not re-render the component when partialText changes on a proxied object.
|
|
71
|
+
// Reasoning gets this implicitly via the auto-scroll effect above.
|
|
72
|
+
$effect(() => {
|
|
73
|
+
if (roleCategory !== 'assistant') return;
|
|
74
|
+
if (message.type !== 'stream_event') return;
|
|
75
|
+
if (!('partialText' in message)) return;
|
|
76
|
+
// Reading partialText subscribes this effect to changes,
|
|
77
|
+
// which forces the component to re-evaluate its derived values
|
|
78
|
+
const _track = message.partialText;
|
|
79
|
+
});
|
|
67
80
|
</script>
|
|
68
81
|
|
|
69
82
|
<div class="relative overflow-hidden">
|
|
@@ -48,15 +48,30 @@
|
|
|
48
48
|
function getColorClasses(type: string) {
|
|
49
49
|
switch (type) {
|
|
50
50
|
case 'success':
|
|
51
|
-
return 'bg-green-50 border-green-
|
|
51
|
+
return 'bg-green-50 border-green-300 text-green-900 dark:bg-green-950 dark:border-green-700 dark:text-green-100';
|
|
52
52
|
case 'error':
|
|
53
|
-
return 'bg-red-50 border-red-
|
|
53
|
+
return 'bg-red-50 border-red-300 text-red-900 dark:bg-red-950 dark:border-red-700 dark:text-red-100';
|
|
54
54
|
case 'warning':
|
|
55
|
-
return 'bg-amber-50 border-amber-
|
|
55
|
+
return 'bg-amber-50 border-amber-300 text-amber-900 dark:bg-amber-950 dark:border-amber-700 dark:text-amber-100';
|
|
56
56
|
case 'info':
|
|
57
|
-
return 'bg-
|
|
57
|
+
return 'bg-blue-50 border-blue-300 text-blue-900 dark:bg-slate-800 dark:border-slate-600 dark:text-slate-100';
|
|
58
58
|
default:
|
|
59
|
-
return 'bg-slate-50 border-slate-
|
|
59
|
+
return 'bg-slate-50 border-slate-300 text-slate-900 dark:bg-slate-800 dark:border-slate-600 dark:text-slate-100';
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function getIconColorClass(type: string) {
|
|
64
|
+
switch (type) {
|
|
65
|
+
case 'success':
|
|
66
|
+
return 'text-green-600 dark:text-green-400';
|
|
67
|
+
case 'error':
|
|
68
|
+
return 'text-red-600 dark:text-red-400';
|
|
69
|
+
case 'warning':
|
|
70
|
+
return 'text-amber-600 dark:text-amber-400';
|
|
71
|
+
case 'info':
|
|
72
|
+
return 'text-blue-600 dark:text-blue-400';
|
|
73
|
+
default:
|
|
74
|
+
return 'text-slate-600 dark:text-slate-400';
|
|
60
75
|
}
|
|
61
76
|
}
|
|
62
77
|
</script>
|
|
@@ -68,27 +83,27 @@
|
|
|
68
83
|
role="alert"
|
|
69
84
|
aria-live="polite"
|
|
70
85
|
>
|
|
71
|
-
<div class="
|
|
86
|
+
<div class="border rounded-lg p-4 shadow-lg {getColorClasses(notification.type)}">
|
|
72
87
|
<div class="flex items-start space-x-3">
|
|
73
|
-
<div class="flex-shrink-0">
|
|
88
|
+
<div class="flex-shrink-0 {getIconColorClass(notification.type)}">
|
|
74
89
|
<Icon name={getIcon(notification.type)} class="w-5 h-5" />
|
|
75
90
|
</div>
|
|
76
91
|
|
|
77
92
|
<div class="flex-1 min-w-0">
|
|
78
93
|
<div class="flex items-center justify-between">
|
|
79
|
-
<h4 class="font-
|
|
94
|
+
<h4 class="font-semibold text-sm">
|
|
80
95
|
{notification.title}
|
|
81
96
|
</h4>
|
|
82
97
|
<button
|
|
83
98
|
onclick={handleDismiss}
|
|
84
|
-
class="flex-shrink-0 ml-2 p-1
|
|
99
|
+
class="flex flex-shrink-0 ml-2 p-1 rounded opacity-60 hover:opacity-100 transition-opacity"
|
|
85
100
|
aria-label="Dismiss notification"
|
|
86
101
|
>
|
|
87
102
|
<Icon name="lucide:x" class="w-4 h-4" />
|
|
88
103
|
</button>
|
|
89
104
|
</div>
|
|
90
105
|
|
|
91
|
-
<p class="text-sm opacity-
|
|
106
|
+
<p class="text-sm opacity-80 mt-1">
|
|
92
107
|
{notification.message}
|
|
93
108
|
</p>
|
|
94
109
|
|
|
@@ -100,7 +115,7 @@
|
|
|
100
115
|
action.action();
|
|
101
116
|
handleDismiss();
|
|
102
117
|
}}
|
|
103
|
-
class="text-xs font-medium px-3 py-1 bg-
|
|
118
|
+
class="text-xs font-medium px-3 py-1 bg-black/10 hover:bg-black/20 dark:bg-white/10 dark:hover:bg-white/20 rounded-md transition-colors"
|
|
104
119
|
>
|
|
105
120
|
{action.label}
|
|
106
121
|
</button>
|
|
@@ -41,6 +41,11 @@
|
|
|
41
41
|
let showDeleteFolder = $state(false);
|
|
42
42
|
let folderToDelete: FileItem | null = $state(null);
|
|
43
43
|
let deleteFolderConfirmName = $state('');
|
|
44
|
+
let showHidden = $state(false);
|
|
45
|
+
|
|
46
|
+
const filteredItems = $derived(
|
|
47
|
+
showHidden ? items : items.filter(item => !item.name.startsWith('.'))
|
|
48
|
+
);
|
|
44
49
|
|
|
45
50
|
// Derived: whether directory access is restricted
|
|
46
51
|
const hasRestrictions = $derived(systemSettings.allowedBasePaths && systemSettings.allowedBasePaths.length > 0);
|
|
@@ -611,6 +616,14 @@
|
|
|
611
616
|
</div>
|
|
612
617
|
|
|
613
618
|
<div class="flex items-center space-x-2">
|
|
619
|
+
<button
|
|
620
|
+
onclick={() => showHidden = !showHidden}
|
|
621
|
+
class="px-3 py-1.5 text-xs rounded-lg transition-colors {showHidden ? 'bg-violet-100 dark:bg-violet-900/30 text-violet-700 dark:text-violet-300' : 'bg-slate-200 dark:bg-slate-700 hover:bg-slate-300 dark:hover:bg-slate-600 text-slate-700 dark:text-slate-300'}"
|
|
622
|
+
title={showHidden ? 'Hide hidden folders' : 'Show hidden folders'}
|
|
623
|
+
>
|
|
624
|
+
<Icon name={showHidden ? 'lucide:eye' : 'lucide:eye-off'} class="inline sm:mr-1" />
|
|
625
|
+
<span class="hidden sm:inline">Hidden</span>
|
|
626
|
+
</button>
|
|
614
627
|
<button
|
|
615
628
|
onclick={() => showCreateFolder = true}
|
|
616
629
|
class="px-3 py-1.5 text-xs rounded-lg bg-slate-200 dark:bg-slate-700 hover:bg-slate-300 dark:hover:bg-slate-600 text-slate-700 dark:text-slate-300 transition-colors"
|
|
@@ -651,24 +664,24 @@
|
|
|
651
664
|
</Button>
|
|
652
665
|
</div>
|
|
653
666
|
</div>
|
|
654
|
-
{:else if showLoadingSpinner &&
|
|
667
|
+
{:else if showLoadingSpinner && filteredItems.length === 0}
|
|
655
668
|
<div class="flex items-center justify-center py-12">
|
|
656
669
|
<div class="text-center">
|
|
657
670
|
<div class="animate-spin rounded-full h-8 w-8 border-2 border-violet-500 border-t-transparent mx-auto mb-4"></div>
|
|
658
671
|
<p class="text-slate-600 dark:text-slate-400">Loading directory...</p>
|
|
659
672
|
</div>
|
|
660
673
|
</div>
|
|
661
|
-
{:else if
|
|
674
|
+
{:else if filteredItems.length === 0}
|
|
662
675
|
<div class="flex items-center justify-center py-12">
|
|
663
676
|
<div class="text-center">
|
|
664
677
|
<Icon name="lucide:folder-x" class="text-4xl text-slate-400 mx-auto mb-4" />
|
|
665
678
|
<p class="text-slate-600 dark:text-slate-400">No folders found</p>
|
|
666
|
-
<p class="text-sm text-slate-500 dark:text-slate-500 mt-2">This directory doesn't contain any subdirectories</p>
|
|
679
|
+
<p class="text-sm text-slate-500 dark:text-slate-500 mt-2">{items.length > 0 ? 'Toggle "Hidden" to show hidden folders' : 'This directory doesn\'t contain any subdirectories'}</p>
|
|
667
680
|
</div>
|
|
668
681
|
</div>
|
|
669
682
|
{:else}
|
|
670
683
|
<div class="space-y-2 transition-opacity duration-300 {loading ? 'opacity-75' : 'opacity-100'}">
|
|
671
|
-
{#each
|
|
684
|
+
{#each filteredItems as item (item.path)}
|
|
672
685
|
<div
|
|
673
686
|
class="flex items-center space-x-3 py-3 px-4 rounded-xl border transition-all duration-200 cursor-pointer {selectedPath === item.path
|
|
674
687
|
? 'bg-violet-50 dark:bg-violet-900/20 border-violet-200 dark:border-violet-700'
|
|
@@ -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}
|
|
@@ -62,50 +62,29 @@
|
|
|
62
62
|
|
|
63
63
|
let nodeElement: HTMLDivElement;
|
|
64
64
|
let menuButtonElement: HTMLButtonElement;
|
|
65
|
-
let
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const rect = menuButtonElement.getBoundingClientRect();
|
|
76
|
-
const dockContainer = nodeElement?.closest('.overflow-auto');
|
|
77
|
-
|
|
78
|
-
if (!dockContainer) {
|
|
79
|
-
// Fallback ke viewport jika tidak ada container
|
|
80
|
-
const viewportHeight = window.innerHeight;
|
|
81
|
-
const menuHeight = 100;
|
|
82
|
-
showAbove = rect.bottom + menuHeight > viewportHeight && rect.top > menuHeight;
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
const dockRect = dockContainer.getBoundingClientRect();
|
|
87
|
-
const menuHeight = 100; // Estimasi tinggi menu dropdown
|
|
88
|
-
|
|
89
|
-
// Hitung ruang yang tersedia di bawah dan di atas dalam dock container
|
|
90
|
-
const spaceBelow = dockRect.bottom - rect.bottom;
|
|
91
|
-
const spaceAbove = rect.top - dockRect.top;
|
|
92
|
-
|
|
93
|
-
// Jika tidak cukup ruang di bawah untuk menu dan ada cukup ruang di atas, tampilkan di atas
|
|
94
|
-
showAbove = spaceBelow < menuHeight && spaceAbove > menuHeight;
|
|
65
|
+
let menuStyle = $state('');
|
|
66
|
+
|
|
67
|
+
function computeMenuStyle(x: number, y: number, alignRight: boolean): string {
|
|
68
|
+
const menuHeight = 200;
|
|
69
|
+
const isAbove = y + menuHeight > window.innerHeight && y > menuHeight;
|
|
70
|
+
const verticalStyle = isAbove
|
|
71
|
+
? `bottom: ${window.innerHeight - y}px;`
|
|
72
|
+
: `top: ${y}px;`;
|
|
73
|
+
const horizontalStyle = alignRight ? `right: ${x}px;` : `left: ${x}px;`;
|
|
74
|
+
return `${horizontalStyle} ${verticalStyle}`;
|
|
95
75
|
}
|
|
96
76
|
|
|
97
77
|
function toggleMenu(event: Event) {
|
|
98
78
|
event.stopPropagation();
|
|
99
79
|
if (!isMenuOpen) {
|
|
100
|
-
|
|
101
|
-
|
|
80
|
+
const rect = menuButtonElement.getBoundingClientRect();
|
|
81
|
+
menuStyle = computeMenuStyle(window.innerWidth - rect.right, rect.bottom, true);
|
|
102
82
|
}
|
|
103
83
|
onMenuToggle?.(file.path);
|
|
104
84
|
}
|
|
105
85
|
|
|
106
86
|
function closeMenu() {
|
|
107
|
-
onMenuToggle?.(file.path);
|
|
108
|
-
menuOpenedViaContextMenu = false;
|
|
87
|
+
onMenuToggle?.(file.path);
|
|
109
88
|
}
|
|
110
89
|
|
|
111
90
|
function getDisplayIcon(fileName: string, isDirectory: boolean): IconName {
|
|
@@ -127,45 +106,16 @@
|
|
|
127
106
|
function handleContextMenu(event: MouseEvent) {
|
|
128
107
|
event.preventDefault();
|
|
129
108
|
if (!isMenuOpen) {
|
|
130
|
-
|
|
131
|
-
contextMenuX = event.clientX;
|
|
132
|
-
contextMenuY = event.clientY;
|
|
133
|
-
menuOpenedViaContextMenu = true;
|
|
134
|
-
// Check position based on mouse Y relative to dock container
|
|
135
|
-
checkContextMenuPosition(event.clientY);
|
|
109
|
+
menuStyle = computeMenuStyle(event.clientX, event.clientY, false);
|
|
136
110
|
}
|
|
137
111
|
onMenuToggle?.(file.path);
|
|
138
112
|
}
|
|
139
113
|
|
|
140
|
-
function checkContextMenuPosition(mouseY: number) {
|
|
141
|
-
const dockContainer = nodeElement?.closest('.overflow-auto');
|
|
142
|
-
const menuHeight = 100;
|
|
143
|
-
|
|
144
|
-
if (!dockContainer) {
|
|
145
|
-
const viewportHeight = window.innerHeight;
|
|
146
|
-
showAbove = mouseY + menuHeight > viewportHeight && mouseY > menuHeight;
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const dockRect = dockContainer.getBoundingClientRect();
|
|
151
|
-
const spaceBelow = dockRect.bottom - mouseY;
|
|
152
|
-
const spaceAbove = mouseY - dockRect.top;
|
|
153
|
-
showAbove = spaceBelow < menuHeight && spaceAbove > menuHeight;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
114
|
function handleAction(action: string, event: Event) {
|
|
157
115
|
event.stopPropagation();
|
|
158
116
|
onAction?.(action, file);
|
|
159
117
|
}
|
|
160
118
|
|
|
161
|
-
function formatFileSize(bytes: number): string {
|
|
162
|
-
if (bytes === 0) return '0 B';
|
|
163
|
-
const k = 1024;
|
|
164
|
-
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
165
|
-
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
166
|
-
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
|
167
|
-
}
|
|
168
|
-
|
|
169
119
|
// Close menu when clicking outside
|
|
170
120
|
onMount(() => {
|
|
171
121
|
function handleClickOutside(event: MouseEvent) {
|
|
@@ -226,13 +176,6 @@
|
|
|
226
176
|
{/if}
|
|
227
177
|
</span>
|
|
228
178
|
|
|
229
|
-
<!-- File metadata -->
|
|
230
|
-
{#if file.type === 'file'}
|
|
231
|
-
<span class="flex-shrink-0 text-xs text-slate-400 dark:text-slate-500 lg:group-hover:hidden">
|
|
232
|
-
{formatFileSize(file.size || 0)}
|
|
233
|
-
</span>
|
|
234
|
-
{/if}
|
|
235
|
-
|
|
236
179
|
<!-- Actions menu (always visible, triggered by click) -->
|
|
237
180
|
<div class="flex-shrink-0">
|
|
238
181
|
<div class="relative">
|
|
@@ -250,8 +193,8 @@
|
|
|
250
193
|
<div
|
|
251
194
|
role="menu"
|
|
252
195
|
tabindex="-1"
|
|
253
|
-
class="
|
|
254
|
-
style={
|
|
196
|
+
class="fixed bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg py-1 w-44 max-h-80 overflow-y-auto z-50 shadow-lg"
|
|
197
|
+
style={menuStyle}
|
|
255
198
|
onclick={(e) => e.stopPropagation()}
|
|
256
199
|
>
|
|
257
200
|
<!-- New File & New Folder (hanya untuk directory) -->
|
|
@@ -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({
|