@myrialabs/clopen 0.1.9 → 0.2.0
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 +23 -1
- package/backend/index.ts +25 -1
- package/backend/lib/auth/auth-service.ts +484 -0
- package/backend/lib/auth/index.ts +4 -0
- package/backend/lib/auth/permissions.ts +63 -0
- package/backend/lib/auth/rate-limiter.ts +145 -0
- package/backend/lib/auth/tokens.ts +53 -0
- package/backend/lib/chat/stream-manager.ts +4 -1
- package/backend/lib/database/migrations/024_create_users_table.ts +29 -0
- package/backend/lib/database/migrations/025_create_auth_sessions_table.ts +38 -0
- package/backend/lib/database/migrations/026_create_invite_tokens_table.ts +31 -0
- package/backend/lib/database/migrations/index.ts +21 -0
- package/backend/lib/database/queries/auth-queries.ts +201 -0
- package/backend/lib/database/queries/index.ts +2 -1
- package/backend/lib/database/queries/session-queries.ts +13 -0
- package/backend/lib/database/queries/snapshot-queries.ts +1 -1
- package/backend/lib/engine/adapters/opencode/server.ts +9 -1
- package/backend/lib/engine/adapters/opencode/stream.ts +175 -1
- package/backend/lib/mcp/config.ts +13 -18
- package/backend/lib/mcp/index.ts +9 -0
- package/backend/lib/mcp/remote-server.ts +132 -0
- package/backend/lib/mcp/servers/helper.ts +49 -3
- package/backend/lib/mcp/servers/index.ts +3 -2
- package/backend/lib/preview/browser/browser-audio-capture.ts +20 -3
- package/backend/lib/preview/browser/browser-navigation-tracker.ts +3 -0
- package/backend/lib/preview/browser/browser-pool.ts +73 -176
- package/backend/lib/preview/browser/browser-preview-service.ts +3 -2
- package/backend/lib/preview/browser/browser-tab-manager.ts +261 -23
- package/backend/lib/preview/browser/browser-video-capture.ts +36 -1
- package/backend/lib/snapshot/helpers.ts +22 -49
- package/backend/lib/snapshot/snapshot-service.ts +148 -83
- package/backend/lib/utils/ws.ts +65 -1
- package/backend/ws/auth/index.ts +17 -0
- package/backend/ws/auth/invites.ts +84 -0
- package/backend/ws/auth/login.ts +269 -0
- package/backend/ws/auth/status.ts +41 -0
- package/backend/ws/auth/users.ts +32 -0
- package/backend/ws/chat/stream.ts +13 -0
- package/backend/ws/engine/claude/accounts.ts +3 -1
- package/backend/ws/engine/utils.ts +38 -6
- package/backend/ws/index.ts +4 -4
- package/backend/ws/preview/browser/interact.ts +27 -5
- package/backend/ws/snapshot/restore.ts +111 -12
- package/backend/ws/snapshot/timeline.ts +56 -29
- package/bin/clopen.ts +56 -1
- package/bun.lock +113 -51
- package/frontend/App.svelte +47 -29
- package/frontend/lib/components/auth/InvitePage.svelte +215 -0
- package/frontend/lib/components/auth/LoginPage.svelte +129 -0
- package/frontend/lib/components/auth/SetupPage.svelte +1022 -0
- package/frontend/lib/components/chat/input/ChatInput.svelte +1 -2
- package/frontend/lib/components/chat/input/components/EngineModelPicker.svelte +2 -2
- package/frontend/lib/components/chat/input/composables/use-chat-actions.svelte.ts +4 -4
- package/frontend/lib/components/chat/input/composables/use-textarea-resize.svelte.ts +11 -19
- package/frontend/lib/components/checkpoint/TimelineModal.svelte +15 -3
- package/frontend/lib/components/checkpoint/timeline/TimelineNode.svelte +30 -19
- package/frontend/lib/components/checkpoint/timeline/types.ts +4 -0
- package/frontend/lib/components/common/FolderBrowser.svelte +9 -9
- package/frontend/lib/components/common/UpdateBanner.svelte +2 -2
- package/frontend/lib/components/git/CommitForm.svelte +6 -4
- package/frontend/lib/components/history/HistoryModal.svelte +1 -1
- package/frontend/lib/components/history/HistoryView.svelte +1 -1
- package/frontend/lib/components/preview/browser/BrowserPreview.svelte +1 -1
- package/frontend/lib/components/preview/browser/core/mcp-handlers.svelte.ts +12 -4
- package/frontend/lib/components/settings/SettingsModal.svelte +50 -15
- package/frontend/lib/components/settings/SettingsView.svelte +21 -7
- package/frontend/lib/components/settings/account/AccountSettings.svelte +5 -0
- package/frontend/lib/components/settings/admin/InviteManagement.svelte +239 -0
- package/frontend/lib/components/settings/admin/UserManagement.svelte +127 -0
- package/frontend/lib/components/settings/general/AdvancedSettings.svelte +10 -4
- package/frontend/lib/components/settings/general/AuthModeSettings.svelte +229 -0
- package/frontend/lib/components/settings/general/GeneralSettings.svelte +6 -1
- package/frontend/lib/components/settings/general/UpdateSettings.svelte +5 -5
- package/frontend/lib/components/settings/security/SecuritySettings.svelte +10 -0
- package/frontend/lib/components/settings/system/SystemSettings.svelte +10 -0
- package/frontend/lib/components/settings/user/UserSettings.svelte +147 -74
- package/frontend/lib/components/workspace/PanelHeader.svelte +1 -1
- package/frontend/lib/components/workspace/WorkspaceLayout.svelte +5 -10
- package/frontend/lib/components/workspace/panels/ChatPanel.svelte +3 -2
- package/frontend/lib/services/preview/browser/browser-webcodecs.service.ts +31 -8
- package/frontend/lib/stores/core/sessions.svelte.ts +15 -1
- package/frontend/lib/stores/features/auth.svelte.ts +296 -0
- package/frontend/lib/stores/features/settings.svelte.ts +53 -9
- package/frontend/lib/stores/features/user.svelte.ts +26 -68
- package/frontend/lib/stores/ui/settings-modal.svelte.ts +42 -21
- package/frontend/lib/stores/ui/update.svelte.ts +2 -14
- package/frontend/lib/stores/ui/workspace.svelte.ts +4 -4
- package/package.json +8 -6
- package/shared/types/stores/settings.ts +16 -2
- package/shared/utils/logger.ts +1 -0
- package/shared/utils/ws-client.ts +30 -13
- package/shared/utils/ws-server.ts +42 -4
- package/backend/lib/mcp/stdio-server.ts +0 -103
- package/backend/ws/mcp/index.ts +0 -61
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import {
|
|
2
|
+
import { systemSettings, updateSystemSettings } from '$frontend/lib/stores/features/settings.svelte';
|
|
3
3
|
import { updateState, checkForUpdate, runUpdate } from '$frontend/lib/stores/ui/update.svelte';
|
|
4
4
|
import Icon from '../../common/Icon.svelte';
|
|
5
5
|
|
|
6
6
|
function toggleAutoUpdate() {
|
|
7
|
-
|
|
7
|
+
updateSystemSettings({ autoUpdate: !systemSettings.autoUpdate });
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
function handleCheckNow() {
|
|
@@ -108,14 +108,14 @@
|
|
|
108
108
|
<button
|
|
109
109
|
type="button"
|
|
110
110
|
role="switch"
|
|
111
|
-
aria-checked={
|
|
111
|
+
aria-checked={systemSettings.autoUpdate}
|
|
112
112
|
onclick={toggleAutoUpdate}
|
|
113
113
|
class="relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-violet-500/30
|
|
114
|
-
{
|
|
114
|
+
{systemSettings.autoUpdate ? 'bg-violet-600' : 'bg-slate-300 dark:bg-slate-600'}"
|
|
115
115
|
>
|
|
116
116
|
<span
|
|
117
117
|
class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow-lg ring-0 transition duration-200 ease-in-out
|
|
118
|
-
{
|
|
118
|
+
{systemSettings.autoUpdate ? 'translate-x-5' : 'translate-x-0'}"
|
|
119
119
|
></span>
|
|
120
120
|
</button>
|
|
121
121
|
</div>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import UpdateSettings from '../general/UpdateSettings.svelte';
|
|
3
|
+
import DataManagementSettings from '../general/DataManagementSettings.svelte';
|
|
4
|
+
</script>
|
|
5
|
+
|
|
6
|
+
<UpdateSettings />
|
|
7
|
+
|
|
8
|
+
<div class="mt-6">
|
|
9
|
+
<DataManagementSettings />
|
|
10
|
+
</div>
|
|
@@ -1,121 +1,127 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
import {
|
|
2
|
+
import { authStore } from '$frontend/lib/stores/features/auth.svelte';
|
|
3
|
+
import { systemSettings } from '$frontend/lib/stores/features/settings.svelte';
|
|
3
4
|
import { addNotification } from '$frontend/lib/stores/ui/notification.svelte';
|
|
4
5
|
import Icon from '../../common/Icon.svelte';
|
|
6
|
+
import Dialog from '../../common/Dialog.svelte';
|
|
5
7
|
import { debug } from '$shared/utils/logger';
|
|
6
8
|
|
|
9
|
+
const isNoAuth = $derived(systemSettings.authMode === 'none');
|
|
10
|
+
|
|
7
11
|
// State
|
|
8
12
|
let userNameInput = $state('');
|
|
9
13
|
let isEditing = $state(false);
|
|
10
14
|
let isSaving = $state(false);
|
|
11
15
|
|
|
16
|
+
// PAT state
|
|
17
|
+
let showPAT = $state(false);
|
|
18
|
+
let currentPAT = $state('');
|
|
19
|
+
let isRegeneratingPAT = $state(false);
|
|
20
|
+
let showRegenerateConfirm = $state(false);
|
|
21
|
+
|
|
22
|
+
const user = $derived(authStore.currentUser);
|
|
23
|
+
|
|
12
24
|
// Update input when user changes
|
|
13
25
|
$effect(() => {
|
|
14
|
-
if (
|
|
15
|
-
userNameInput =
|
|
26
|
+
if (user?.name) {
|
|
27
|
+
userNameInput = user.name;
|
|
16
28
|
}
|
|
17
29
|
});
|
|
18
30
|
|
|
19
|
-
// Handle save user name
|
|
20
31
|
async function saveUserName() {
|
|
21
32
|
if (!userNameInput.trim()) {
|
|
22
|
-
addNotification({
|
|
23
|
-
type: 'error',
|
|
24
|
-
title: 'Validation Error',
|
|
25
|
-
message: 'Name cannot be empty'
|
|
26
|
-
});
|
|
33
|
+
addNotification({ type: 'error', title: 'Validation Error', message: 'Name cannot be empty' });
|
|
27
34
|
return;
|
|
28
35
|
}
|
|
29
36
|
|
|
30
37
|
isSaving = true;
|
|
31
|
-
|
|
32
38
|
try {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
isEditing = false;
|
|
37
|
-
addNotification({
|
|
38
|
-
type: 'success',
|
|
39
|
-
title: 'Updated',
|
|
40
|
-
message: 'Display name updated successfully'
|
|
41
|
-
});
|
|
42
|
-
} else {
|
|
43
|
-
addNotification({
|
|
44
|
-
type: 'error',
|
|
45
|
-
title: 'Error',
|
|
46
|
-
message: 'Failed to update user name'
|
|
47
|
-
});
|
|
48
|
-
}
|
|
39
|
+
await authStore.updateName(userNameInput.trim());
|
|
40
|
+
isEditing = false;
|
|
41
|
+
addNotification({ type: 'success', title: 'Updated', message: 'Display name updated' });
|
|
49
42
|
} catch (error) {
|
|
50
43
|
debug.error('settings', 'Error updating user name:', error);
|
|
51
|
-
addNotification({
|
|
52
|
-
type: 'error',
|
|
53
|
-
title: 'Error',
|
|
54
|
-
message: 'An error occurred while updating user name'
|
|
55
|
-
});
|
|
44
|
+
addNotification({ type: 'error', title: 'Error', message: 'Failed to update display name' });
|
|
56
45
|
} finally {
|
|
57
46
|
isSaving = false;
|
|
58
47
|
}
|
|
59
48
|
}
|
|
60
49
|
|
|
61
|
-
// Handle cancel edit
|
|
62
50
|
function cancelEdit() {
|
|
63
|
-
userNameInput =
|
|
51
|
+
userNameInput = user?.name || '';
|
|
64
52
|
isEditing = false;
|
|
65
53
|
}
|
|
66
54
|
|
|
67
|
-
// Handle start edit
|
|
68
55
|
function startEdit() {
|
|
69
56
|
isEditing = true;
|
|
70
57
|
}
|
|
58
|
+
|
|
59
|
+
async function regeneratePAT() {
|
|
60
|
+
isRegeneratingPAT = true;
|
|
61
|
+
try {
|
|
62
|
+
const pat = await authStore.regeneratePAT();
|
|
63
|
+
currentPAT = pat;
|
|
64
|
+
showPAT = true;
|
|
65
|
+
showRegenerateConfirm = false;
|
|
66
|
+
addNotification({ type: 'success', title: 'Regenerated', message: 'Personal Access Token has been regenerated' });
|
|
67
|
+
} catch (error) {
|
|
68
|
+
debug.error('settings', 'Error regenerating PAT:', error);
|
|
69
|
+
addNotification({ type: 'error', title: 'Error', message: 'Failed to regenerate token' });
|
|
70
|
+
} finally {
|
|
71
|
+
isRegeneratingPAT = false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function copyPAT() {
|
|
76
|
+
navigator.clipboard.writeText(currentPAT);
|
|
77
|
+
addNotification({ type: 'success', title: 'Copied', message: 'Token copied to clipboard' });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function handleLogout() {
|
|
81
|
+
await authStore.logout();
|
|
82
|
+
}
|
|
71
83
|
</script>
|
|
72
84
|
|
|
73
85
|
<div class="py-1">
|
|
74
86
|
<h3 class="text-base font-bold text-slate-900 dark:text-slate-100 mb-1.5">User Profile</h3>
|
|
75
87
|
<p class="text-sm text-slate-600 dark:text-slate-500 mb-5">
|
|
76
|
-
Manage your identity and
|
|
88
|
+
Manage your identity and access
|
|
77
89
|
</p>
|
|
78
90
|
|
|
79
|
-
{#if !
|
|
91
|
+
{#if !user}
|
|
80
92
|
<div class="flex items-center justify-center gap-3 py-10 text-slate-600 dark:text-slate-500 text-sm">
|
|
81
|
-
<div
|
|
82
|
-
class="w-5 h-5 border-2 border-violet-500/20 border-t-violet-600 rounded-full animate-spin"
|
|
83
|
-
></div>
|
|
93
|
+
<div class="w-5 h-5 border-2 border-violet-500/20 border-t-violet-600 rounded-full animate-spin"></div>
|
|
84
94
|
<span>Loading user settings...</span>
|
|
85
95
|
</div>
|
|
86
96
|
{:else}
|
|
87
97
|
<div class="flex flex-col gap-4">
|
|
88
98
|
<!-- Current User Card -->
|
|
89
|
-
<div
|
|
90
|
-
class="flex items-center gap-3.5 p-4.5 bg-gradient-to-br from-violet-500/10 to-purple-500/5 dark:from-violet-500/10 dark:to-purple-500/8 border border-violet-500/20 rounded-xl"
|
|
91
|
-
>
|
|
99
|
+
<div class="flex items-center gap-3.5 p-4.5 bg-gradient-to-br from-violet-500/10 to-purple-500/5 dark:from-violet-500/10 dark:to-purple-500/8 border border-violet-500/20 rounded-xl">
|
|
92
100
|
<div
|
|
93
101
|
class="flex items-center justify-center w-12 h-12 rounded-xl text-lg font-bold text-white shrink-0"
|
|
94
|
-
style="background-color: {
|
|
102
|
+
style="background-color: {user.color || '#7c3aed'}"
|
|
95
103
|
>
|
|
96
|
-
{
|
|
104
|
+
{user.avatar || 'U'}
|
|
97
105
|
</div>
|
|
98
106
|
<div class="flex-1 min-w-0">
|
|
99
107
|
<div class="text-base font-semibold text-slate-900 dark:text-slate-100 mb-0.5">
|
|
100
|
-
{
|
|
108
|
+
{user.name}
|
|
109
|
+
</div>
|
|
110
|
+
<div class="text-xs text-slate-600 dark:text-slate-500">
|
|
111
|
+
{user.role === 'admin' ? 'Administrator' : 'Member'}
|
|
101
112
|
</div>
|
|
102
|
-
<div class="text-xs text-slate-600 dark:text-slate-500">Anonymous user identity</div>
|
|
103
113
|
</div>
|
|
104
|
-
<div
|
|
105
|
-
class="flex items-center gap-1.5 py-1.5 px-3 bg-emerald-500/15 rounded-full text-xs font-medium text-emerald-500"
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
114
|
+
<div class="flex items-center gap-2">
|
|
115
|
+
<span class="inline-flex items-center gap-1.5 py-1.5 px-3 bg-{user.role === 'admin' ? 'violet' : 'emerald'}-500/15 rounded-full text-xs font-medium text-{user.role === 'admin' ? 'violet' : 'emerald'}-500">
|
|
116
|
+
<Icon name="lucide:{user.role === 'admin' ? 'shield' : 'user'}" class="w-3 h-3" />
|
|
117
|
+
{user.role === 'admin' ? 'Admin' : 'Member'}
|
|
118
|
+
</span>
|
|
109
119
|
</div>
|
|
110
120
|
</div>
|
|
111
121
|
|
|
112
122
|
<!-- Edit Display Name -->
|
|
113
|
-
<div
|
|
114
|
-
class="
|
|
115
|
-
>
|
|
116
|
-
<div
|
|
117
|
-
class="flex items-center gap-2 text-sm font-semibold text-slate-500 mb-3"
|
|
118
|
-
>
|
|
123
|
+
<div class="p-4 bg-slate-100/80 dark:bg-slate-800/80 border border-slate-200 dark:border-slate-800 rounded-xl">
|
|
124
|
+
<div class="flex items-center gap-2 text-sm font-semibold text-slate-500 mb-3">
|
|
119
125
|
<Icon name="lucide:pencil" class="w-4 h-4 opacity-70" />
|
|
120
126
|
<span>Display Name</span>
|
|
121
127
|
</div>
|
|
@@ -136,9 +142,7 @@
|
|
|
136
142
|
disabled={!userNameInput.trim() || isSaving}
|
|
137
143
|
>
|
|
138
144
|
{#if isSaving}
|
|
139
|
-
<div
|
|
140
|
-
class="w-3.5 h-3.5 border-2 border-white/30 border-t-white rounded-full animate-spin"
|
|
141
|
-
></div>
|
|
145
|
+
<div class="w-3.5 h-3.5 border-2 border-white/30 border-t-white rounded-full animate-spin"></div>
|
|
142
146
|
Saving...
|
|
143
147
|
{:else}
|
|
144
148
|
<Icon name="lucide:check" class="w-4 h-4" />
|
|
@@ -158,7 +162,7 @@
|
|
|
158
162
|
{:else}
|
|
159
163
|
<div class="flex items-center justify-between gap-3">
|
|
160
164
|
<div class="text-sm text-slate-900 dark:text-slate-100">
|
|
161
|
-
{
|
|
165
|
+
{user.name}
|
|
162
166
|
</div>
|
|
163
167
|
<button
|
|
164
168
|
type="button"
|
|
@@ -172,26 +176,95 @@
|
|
|
172
176
|
{/if}
|
|
173
177
|
</div>
|
|
174
178
|
|
|
179
|
+
{#if !isNoAuth}
|
|
180
|
+
<!-- Personal Access Token -->
|
|
181
|
+
<div class="p-4 bg-slate-100/80 dark:bg-slate-800/80 border border-slate-200 dark:border-slate-800 rounded-xl">
|
|
182
|
+
<div class="flex items-center gap-2 text-sm font-semibold text-slate-500 mb-3">
|
|
183
|
+
<Icon name="lucide:key-round" class="w-4 h-4 opacity-70" />
|
|
184
|
+
<span>Personal Access Token</span>
|
|
185
|
+
</div>
|
|
186
|
+
<p class="text-xs text-slate-600 dark:text-slate-500 mb-3">
|
|
187
|
+
Use this token to log in from other devices. Keep it secret.
|
|
188
|
+
</p>
|
|
189
|
+
|
|
190
|
+
{#if showPAT && currentPAT}
|
|
191
|
+
<div class="flex flex-col gap-2 mb-3">
|
|
192
|
+
<div class="flex items-center gap-2">
|
|
193
|
+
<code class="flex-1 py-2.5 px-3.5 bg-slate-50 dark:bg-slate-900/80 border border-emerald-500/30 rounded-lg font-mono text-xs text-slate-700 dark:text-slate-300 break-all">
|
|
194
|
+
{currentPAT}
|
|
195
|
+
</code>
|
|
196
|
+
<button
|
|
197
|
+
type="button"
|
|
198
|
+
onclick={copyPAT}
|
|
199
|
+
class="flex items-center justify-center w-9 h-9 rounded-lg bg-emerald-500/10 hover:bg-emerald-500/20 text-emerald-600 dark:text-emerald-400 transition-all"
|
|
200
|
+
title="Copy token"
|
|
201
|
+
>
|
|
202
|
+
<Icon name="lucide:copy" class="w-4 h-4" />
|
|
203
|
+
</button>
|
|
204
|
+
</div>
|
|
205
|
+
<div class="flex items-center gap-1.5 text-xs text-amber-600 dark:text-amber-400">
|
|
206
|
+
<Icon name="lucide:triangle-alert" class="w-3.5 h-3.5" />
|
|
207
|
+
<span>This token is shown only once. Copy and store it securely.</span>
|
|
208
|
+
</div>
|
|
209
|
+
</div>
|
|
210
|
+
{/if}
|
|
211
|
+
|
|
212
|
+
<button
|
|
213
|
+
type="button"
|
|
214
|
+
onclick={() => { showRegenerateConfirm = true; }}
|
|
215
|
+
disabled={isRegeneratingPAT}
|
|
216
|
+
class="inline-flex items-center gap-1.5 py-2 px-3.5 bg-amber-500/10 border border-amber-500/20 rounded-lg text-amber-600 dark:text-amber-400 text-xs font-semibold cursor-pointer transition-all duration-150 hover:bg-amber-500/20 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
217
|
+
>
|
|
218
|
+
{#if isRegeneratingPAT}
|
|
219
|
+
<div class="w-3.5 h-3.5 border-2 border-amber-600/30 border-t-amber-600 rounded-full animate-spin"></div>
|
|
220
|
+
Regenerating...
|
|
221
|
+
{:else}
|
|
222
|
+
<Icon name="lucide:refresh-cw" class="w-3.5 h-3.5" />
|
|
223
|
+
Regenerate Token
|
|
224
|
+
{/if}
|
|
225
|
+
</button>
|
|
226
|
+
</div>
|
|
227
|
+
{/if}
|
|
228
|
+
|
|
175
229
|
<!-- User ID -->
|
|
176
|
-
<div
|
|
177
|
-
class="
|
|
178
|
-
>
|
|
179
|
-
<div
|
|
180
|
-
class="flex items-center gap-2 text-sm font-semibold text-slate-500 mb-3"
|
|
181
|
-
>
|
|
230
|
+
<div class="p-4 bg-slate-100/80 dark:bg-slate-800/80 border border-slate-200 dark:border-slate-800 rounded-xl">
|
|
231
|
+
<div class="flex items-center gap-2 text-sm font-semibold text-slate-500 mb-3">
|
|
182
232
|
<Icon name="lucide:fingerprint" class="w-4 h-4 opacity-70" />
|
|
183
233
|
<span>User ID</span>
|
|
184
234
|
</div>
|
|
185
235
|
<div class="flex flex-col gap-1.5">
|
|
186
|
-
<code
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
>
|
|
190
|
-
<span class="text-xs text-slate-600 dark:text-slate-500"
|
|
191
|
-
>Unique identifier for this session</span
|
|
192
|
-
>
|
|
236
|
+
<code class="py-2.5 px-3.5 bg-slate-50 dark:bg-slate-900/80 border border-slate-200 dark:border-slate-800 rounded-lg font-mono text-xs text-slate-500 break-all">
|
|
237
|
+
{user.id}
|
|
238
|
+
</code>
|
|
193
239
|
</div>
|
|
194
240
|
</div>
|
|
241
|
+
|
|
242
|
+
{#if !isNoAuth}
|
|
243
|
+
<!-- Logout -->
|
|
244
|
+
<div class="pt-2">
|
|
245
|
+
<button
|
|
246
|
+
type="button"
|
|
247
|
+
onclick={handleLogout}
|
|
248
|
+
class="inline-flex items-center gap-1.5 py-2.5 px-4 bg-red-500/10 border border-red-500/20 rounded-lg text-red-600 dark:text-red-400 text-sm font-semibold cursor-pointer transition-all duration-150 hover:bg-red-500/20 hover:border-red-600/40"
|
|
249
|
+
>
|
|
250
|
+
<Icon name="lucide:log-out" class="w-4 h-4" />
|
|
251
|
+
Sign Out
|
|
252
|
+
</button>
|
|
253
|
+
</div>
|
|
254
|
+
{/if}
|
|
195
255
|
</div>
|
|
196
256
|
{/if}
|
|
197
257
|
</div>
|
|
258
|
+
|
|
259
|
+
<!-- Regenerate PAT Confirmation Dialog -->
|
|
260
|
+
<Dialog
|
|
261
|
+
bind:isOpen={showRegenerateConfirm}
|
|
262
|
+
onClose={() => { showRegenerateConfirm = false; }}
|
|
263
|
+
title="Regenerate Access Token"
|
|
264
|
+
type="warning"
|
|
265
|
+
message="This will invalidate your current Personal Access Token. Any devices using the old token will need to log in again with the new one. Continue?"
|
|
266
|
+
confirmText="Regenerate"
|
|
267
|
+
cancelText="Cancel"
|
|
268
|
+
showCancel={true}
|
|
269
|
+
onConfirm={regeneratePAT}
|
|
270
|
+
/>
|
|
@@ -277,7 +277,7 @@
|
|
|
277
277
|
>
|
|
278
278
|
<Icon name="lucide:history" class={isMobile ? 'w-4.5 h-4.5' : 'w-4 h-4'} />
|
|
279
279
|
</button>
|
|
280
|
-
|
|
280
|
+
{#if sessionState.messages.length > 0 || sessionState.hasMessageHistory}
|
|
281
281
|
<button
|
|
282
282
|
type="button"
|
|
283
283
|
class="flex items-center justify-center {isMobile ? 'w-9 h-9' : 'w-6 h-6'} bg-transparent border-none rounded-md text-slate-500 cursor-pointer transition-all duration-150 hover:bg-violet-500/10 hover:text-slate-900 dark:hover:text-slate-100"
|
|
@@ -26,8 +26,7 @@
|
|
|
26
26
|
import { initializeProjects } from '$frontend/lib/stores/core/projects.svelte';
|
|
27
27
|
import { initializeSessions } from '$frontend/lib/stores/core/sessions.svelte';
|
|
28
28
|
import { initializeNotifications } from '$frontend/lib/stores/ui/notification.svelte';
|
|
29
|
-
import { applyServerSettings } from '$frontend/lib/stores/features/settings.svelte';
|
|
30
|
-
import { userStore } from '$frontend/lib/stores/features/user.svelte';
|
|
29
|
+
import { applyServerSettings, loadSystemSettings } from '$frontend/lib/stores/features/settings.svelte';
|
|
31
30
|
import { initPresence } from '$frontend/lib/stores/core/presence.svelte';
|
|
32
31
|
import ws from '$frontend/lib/utils/ws';
|
|
33
32
|
import { debug } from '$shared/utils/logger';
|
|
@@ -78,14 +77,9 @@
|
|
|
78
77
|
initializeNotifications();
|
|
79
78
|
initializeWorkspace();
|
|
80
79
|
|
|
81
|
-
// Step 2:
|
|
82
|
-
// userStore.initialize() reads localStorage (fast) and sets WS context locally.
|
|
83
|
-
// waitUntilConnected() waits for WS to connect and sync any pending context.
|
|
80
|
+
// Step 2: WebSocket is already connected (auth completed before this mounts)
|
|
84
81
|
setProgress(20, 'Connecting...');
|
|
85
|
-
await
|
|
86
|
-
userStore.initialize(),
|
|
87
|
-
ws.waitUntilConnected(10000)
|
|
88
|
-
]);
|
|
82
|
+
await ws.waitUntilConnected(10000);
|
|
89
83
|
|
|
90
84
|
// Step 3: Restore user state from server
|
|
91
85
|
setProgress(30, 'Restoring state...');
|
|
@@ -97,13 +91,14 @@
|
|
|
97
91
|
debug.warn('workspace', 'Failed to restore server state, using defaults:', err);
|
|
98
92
|
}
|
|
99
93
|
|
|
100
|
-
// Step 4: Apply restored state + setup presence
|
|
94
|
+
// Step 4: Apply restored state + load system settings + setup presence
|
|
101
95
|
setProgress(40);
|
|
102
96
|
if (serverState?.settings) {
|
|
103
97
|
applyServerSettings(serverState.settings);
|
|
104
98
|
}
|
|
105
99
|
restoreLastView(serverState?.lastView);
|
|
106
100
|
restoreUnreadSessions(serverState?.unreadSessions);
|
|
101
|
+
await loadSystemSettings();
|
|
107
102
|
initPresence();
|
|
108
103
|
|
|
109
104
|
// Step 5: Load projects (with server-restored currentProjectId)
|
|
@@ -24,10 +24,11 @@
|
|
|
24
24
|
|
|
25
25
|
const { showMobileHeader = false }: Props = $props();
|
|
26
26
|
|
|
27
|
-
// Welcome state - don't show during restoration
|
|
27
|
+
// Welcome state - don't show during restoration or when session has history (restored to initial)
|
|
28
28
|
const isWelcomeState = $derived(
|
|
29
29
|
sessionState.messages.length === 0 &&
|
|
30
|
-
!appState.isRestoring
|
|
30
|
+
!appState.isRestoring &&
|
|
31
|
+
!sessionState.hasMessageHistory
|
|
31
32
|
);
|
|
32
33
|
|
|
33
34
|
// Check if we should show input (not during restoration)
|
|
@@ -175,6 +175,17 @@ export class BrowserWebCodecsService {
|
|
|
175
175
|
return false;
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
+
// Pre-initialize AudioContext NOW, during user gesture context.
|
|
179
|
+
// Browsers require a user gesture to start AudioContext — creating it
|
|
180
|
+
// later (e.g. when first audio chunk arrives) results in a permanently
|
|
181
|
+
// suspended context that never plays audio.
|
|
182
|
+
if (!this.audioContext || this.audioContext.state === 'closed') {
|
|
183
|
+
this.audioContext = new AudioContext({ sampleRate: 48000 });
|
|
184
|
+
}
|
|
185
|
+
if (this.audioContext.state === 'suspended') {
|
|
186
|
+
await this.audioContext.resume().catch(() => {});
|
|
187
|
+
}
|
|
188
|
+
|
|
178
189
|
// Clean up any existing connection
|
|
179
190
|
if (this.peerConnection || this.isConnected || this.sessionId) {
|
|
180
191
|
debug.log('webcodecs', 'Cleaning up previous connection');
|
|
@@ -546,7 +557,7 @@ export class BrowserWebCodecsService {
|
|
|
546
557
|
private async initAudioDecoder(): Promise<void> {
|
|
547
558
|
this.audioCodecConfig = {
|
|
548
559
|
codec: 'opus',
|
|
549
|
-
sampleRate:
|
|
560
|
+
sampleRate: 48000,
|
|
550
561
|
numberOfChannels: 2
|
|
551
562
|
};
|
|
552
563
|
|
|
@@ -580,8 +591,17 @@ export class BrowserWebCodecsService {
|
|
|
580
591
|
*/
|
|
581
592
|
private async initAudioContext(): Promise<void> {
|
|
582
593
|
try {
|
|
583
|
-
|
|
584
|
-
|
|
594
|
+
// Reuse AudioContext created in startStreaming (user gesture context)
|
|
595
|
+
if (!this.audioContext || this.audioContext.state === 'closed') {
|
|
596
|
+
this.audioContext = new AudioContext({ sampleRate: 48000 });
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Resume if suspended (may happen without user gesture)
|
|
600
|
+
if (this.audioContext.state === 'suspended') {
|
|
601
|
+
await this.audioContext.resume();
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
debug.log('webcodecs', `AudioContext initialized (state: ${this.audioContext.state})`);
|
|
585
605
|
} catch (error) {
|
|
586
606
|
debug.error('webcodecs', 'AudioContext init error:', error);
|
|
587
607
|
}
|
|
@@ -737,6 +757,11 @@ export class BrowserWebCodecsService {
|
|
|
737
757
|
private playAudioFrame(audioData: AudioData): void {
|
|
738
758
|
if (!this.audioContext) return;
|
|
739
759
|
|
|
760
|
+
// Safety net: resume AudioContext if it somehow got suspended
|
|
761
|
+
if (this.audioContext.state === 'suspended') {
|
|
762
|
+
this.audioContext.resume().catch(() => {});
|
|
763
|
+
}
|
|
764
|
+
|
|
740
765
|
try {
|
|
741
766
|
// Create AudioBuffer
|
|
742
767
|
const buffer = this.audioContext.createBuffer(
|
|
@@ -1151,11 +1176,9 @@ export class BrowserWebCodecsService {
|
|
|
1151
1176
|
this.audioDecoder = null;
|
|
1152
1177
|
}
|
|
1153
1178
|
|
|
1154
|
-
//
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
this.audioContext = null;
|
|
1158
|
-
}
|
|
1179
|
+
// Keep AudioContext alive across reconnections — it was created during
|
|
1180
|
+
// user gesture in startStreaming() and closing it means we can't resume
|
|
1181
|
+
// without another user gesture. Just reset playback state below.
|
|
1159
1182
|
|
|
1160
1183
|
// Close data channel
|
|
1161
1184
|
if (this.dataChannel) {
|
|
@@ -22,6 +22,8 @@ interface SessionState {
|
|
|
22
22
|
messages: SDKMessageFormatter[];
|
|
23
23
|
isLoading: boolean;
|
|
24
24
|
error: string | null;
|
|
25
|
+
/** True if the current session has message history (even if HEAD is null after restore to initial) */
|
|
26
|
+
hasMessageHistory: boolean;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
// Session state using Svelte 5 runes
|
|
@@ -30,7 +32,8 @@ export const sessionState = $state<SessionState>({
|
|
|
30
32
|
currentSession: null,
|
|
31
33
|
messages: [],
|
|
32
34
|
isLoading: false,
|
|
33
|
-
error: null
|
|
35
|
+
error: null,
|
|
36
|
+
hasMessageHistory: false
|
|
34
37
|
});
|
|
35
38
|
|
|
36
39
|
// ========================================
|
|
@@ -206,6 +209,7 @@ export function updateMessages(messages: SDKMessageFormatter[]) {
|
|
|
206
209
|
|
|
207
210
|
export function clearMessages() {
|
|
208
211
|
sessionState.messages = [];
|
|
212
|
+
sessionState.hasMessageHistory = false;
|
|
209
213
|
}
|
|
210
214
|
|
|
211
215
|
export async function loadMessagesForSession(sessionId: string) {
|
|
@@ -215,12 +219,22 @@ export async function loadMessagesForSession(sessionId: string) {
|
|
|
215
219
|
if (response && Array.isArray(response)) {
|
|
216
220
|
// Messages from server already have correct SDKMessageFormatter shape with metadata
|
|
217
221
|
sessionState.messages = response as SDKMessageFormatter[];
|
|
222
|
+
|
|
223
|
+
if (response.length > 0) {
|
|
224
|
+
sessionState.hasMessageHistory = true;
|
|
225
|
+
} else {
|
|
226
|
+
// HEAD might be null (restored to initial) — check if session has any messages at all
|
|
227
|
+
const allResponse = await ws.http('messages:list', { session_id: sessionId, include_all: true });
|
|
228
|
+
sessionState.hasMessageHistory = Array.isArray(allResponse) && allResponse.length > 0;
|
|
229
|
+
}
|
|
218
230
|
} else {
|
|
219
231
|
sessionState.messages = [];
|
|
232
|
+
sessionState.hasMessageHistory = false;
|
|
220
233
|
}
|
|
221
234
|
} catch (error) {
|
|
222
235
|
debug.error('session', 'Error loading messages:', error);
|
|
223
236
|
sessionState.messages = [];
|
|
237
|
+
sessionState.hasMessageHistory = false;
|
|
224
238
|
}
|
|
225
239
|
}
|
|
226
240
|
|