@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
|
@@ -0,0 +1,1022 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { onMount, onDestroy } from 'svelte';
|
|
3
|
+
import { authStore } from '$frontend/lib/stores/features/auth.svelte';
|
|
4
|
+
import { themeStore, toggleDarkMode, isDarkMode, initializeTheme } from '$frontend/lib/stores/ui/theme.svelte';
|
|
5
|
+
import { settings, updateSettings, applyFontSize } from '$frontend/lib/stores/features/settings.svelte';
|
|
6
|
+
import { ENGINES } from '$shared/constants/engines';
|
|
7
|
+
import { claudeAccountsStore, type ClaudeAccountItem } from '$frontend/lib/stores/features/claude-accounts.svelte';
|
|
8
|
+
import Icon from '$frontend/lib/components/common/Icon.svelte';
|
|
9
|
+
import ws from '$frontend/lib/utils/ws';
|
|
10
|
+
import type { AuthMode } from '$shared/types/stores/settings';
|
|
11
|
+
import type { IconName } from '$shared/types/ui/icons';
|
|
12
|
+
|
|
13
|
+
// Ensure theme is initialized (normally done in WorkspaceLayout which hasn't mounted yet)
|
|
14
|
+
onMount(() => {
|
|
15
|
+
initializeTheme();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
// ─── Wizard state ───
|
|
19
|
+
type WizardStep = 'auth-mode' | 'admin-account' | 'engines' | 'preferences';
|
|
20
|
+
const ALL_STEPS: WizardStep[] = ['auth-mode', 'admin-account', 'engines', 'preferences'];
|
|
21
|
+
|
|
22
|
+
let currentStep = $state<WizardStep>('auth-mode');
|
|
23
|
+
let completedSteps = $state<Set<WizardStep>>(new Set());
|
|
24
|
+
let selectedAuthMode = $state<AuthMode>('required');
|
|
25
|
+
|
|
26
|
+
// Whether this is a returning existing user (data exists, just re-onboarding)
|
|
27
|
+
let isExistingUser = $state(false);
|
|
28
|
+
let existingUserName = $state('');
|
|
29
|
+
let initializedFromUser = $state(false);
|
|
30
|
+
|
|
31
|
+
// Restore existing data once on load (not reactively on every adminName change)
|
|
32
|
+
$effect(() => {
|
|
33
|
+
if (authStore.currentUser && !initializedFromUser) {
|
|
34
|
+
initializedFromUser = true;
|
|
35
|
+
isExistingUser = true;
|
|
36
|
+
existingUserName = authStore.currentUser.name;
|
|
37
|
+
adminName = authStore.currentUser.name;
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// Sync auth mode from server (reactive)
|
|
42
|
+
$effect(() => {
|
|
43
|
+
if (authStore.authMode) {
|
|
44
|
+
selectedAuthMode = authStore.authMode;
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
function getVisibleSteps(): WizardStep[] {
|
|
49
|
+
if (selectedAuthMode === 'none') {
|
|
50
|
+
return ALL_STEPS.filter(s => s !== 'admin-account');
|
|
51
|
+
}
|
|
52
|
+
return [...ALL_STEPS];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const visibleSteps = $derived(getVisibleSteps());
|
|
56
|
+
|
|
57
|
+
function goToNextStep() {
|
|
58
|
+
completedSteps.add(currentStep);
|
|
59
|
+
completedSteps = new Set(completedSteps);
|
|
60
|
+
|
|
61
|
+
const visible = getVisibleSteps();
|
|
62
|
+
const idx = visible.indexOf(currentStep);
|
|
63
|
+
if (idx < visible.length - 1) {
|
|
64
|
+
currentStep = visible[idx + 1];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function goToPrevStep() {
|
|
69
|
+
const visible = getVisibleSteps();
|
|
70
|
+
const idx = visible.indexOf(currentStep);
|
|
71
|
+
if (idx > 0) {
|
|
72
|
+
const destIdx = idx - 1;
|
|
73
|
+
// Clear destination and all forward steps from completed
|
|
74
|
+
for (let i = destIdx; i < visible.length; i++) {
|
|
75
|
+
completedSteps.delete(visible[i]);
|
|
76
|
+
}
|
|
77
|
+
completedSteps = new Set(completedSteps);
|
|
78
|
+
currentStep = visible[destIdx];
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function finishWizard() {
|
|
83
|
+
authStore.completeSetup();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// ─── Step Labels ───
|
|
87
|
+
const stepLabels: Record<WizardStep, { label: string; icon: IconName }> = {
|
|
88
|
+
'auth-mode': { label: 'Login', icon: 'lucide:shield' },
|
|
89
|
+
'admin-account': { label: 'Account', icon: 'lucide:user-plus' },
|
|
90
|
+
'engines': { label: 'Engines', icon: 'lucide:cpu' },
|
|
91
|
+
'preferences': { label: 'Preferences', icon: 'lucide:palette' }
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// ─── Step 1: Auth Mode ───
|
|
95
|
+
let authModeLoading = $state(false);
|
|
96
|
+
let authModeError = $state('');
|
|
97
|
+
|
|
98
|
+
async function confirmAuthMode() {
|
|
99
|
+
authModeError = '';
|
|
100
|
+
authModeLoading = true;
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
if (selectedAuthMode === 'none') {
|
|
104
|
+
await authStore.setupNoAuth();
|
|
105
|
+
completedSteps.add('auth-mode');
|
|
106
|
+
completedSteps.add('admin-account');
|
|
107
|
+
completedSteps = new Set(completedSteps);
|
|
108
|
+
currentStep = 'engines';
|
|
109
|
+
} else {
|
|
110
|
+
goToNextStep();
|
|
111
|
+
}
|
|
112
|
+
} catch (err) {
|
|
113
|
+
authModeError = err instanceof Error ? err.message : 'Setup failed';
|
|
114
|
+
} finally {
|
|
115
|
+
authModeLoading = false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ─── Step 2: Admin Account ───
|
|
120
|
+
let adminName = $state('');
|
|
121
|
+
let adminError = $state('');
|
|
122
|
+
let adminLoading = $state(false);
|
|
123
|
+
let showPAT = $state(false);
|
|
124
|
+
let patCopied = $state(false);
|
|
125
|
+
|
|
126
|
+
async function handleCreateAdmin() {
|
|
127
|
+
// If name is empty, skip this step (can be configured later in Settings)
|
|
128
|
+
if (!adminName.trim()) {
|
|
129
|
+
goToNextStep();
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
adminError = '';
|
|
133
|
+
adminLoading = true;
|
|
134
|
+
try {
|
|
135
|
+
if (isExistingUser) {
|
|
136
|
+
// Existing user — just update name if changed and proceed
|
|
137
|
+
if (adminName.trim() !== existingUserName) {
|
|
138
|
+
await authStore.updateName(adminName.trim());
|
|
139
|
+
}
|
|
140
|
+
goToNextStep();
|
|
141
|
+
} else {
|
|
142
|
+
await authStore.setup(adminName.trim());
|
|
143
|
+
showPAT = true;
|
|
144
|
+
}
|
|
145
|
+
} catch (err) {
|
|
146
|
+
adminError = err instanceof Error ? err.message : 'Setup failed';
|
|
147
|
+
} finally {
|
|
148
|
+
adminLoading = false;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function copyPAT() {
|
|
153
|
+
if (authStore.personalAccessToken) {
|
|
154
|
+
await navigator.clipboard.writeText(authStore.personalAccessToken);
|
|
155
|
+
patCopied = true;
|
|
156
|
+
setTimeout(() => { patCopied = false; }, 2000);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function handleAdminKeydown(e: KeyboardEvent) {
|
|
161
|
+
if (e.key === 'Enter' && !showPAT) {
|
|
162
|
+
handleCreateAdmin();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ─── Step 3: AI Engines ───
|
|
167
|
+
interface EngineStatus {
|
|
168
|
+
installed: boolean;
|
|
169
|
+
version: string | null;
|
|
170
|
+
backendOS: 'windows' | 'macos' | 'linux';
|
|
171
|
+
activeAccount?: { id: number; name: string } | null;
|
|
172
|
+
accountsCount?: number;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
let claudeStatus = $state<EngineStatus | null>(null);
|
|
176
|
+
let openCodeStatus = $state<EngineStatus | null>(null);
|
|
177
|
+
let isLoadingEngines = $state(false);
|
|
178
|
+
const claudeAccounts = $derived(claudeAccountsStore.accounts);
|
|
179
|
+
|
|
180
|
+
const claudeEngine = ENGINES.find(e => e.type === 'claude-code')!;
|
|
181
|
+
const openCodeEngine = ENGINES.find(e => e.type === 'opencode')!;
|
|
182
|
+
|
|
183
|
+
// Claude Code account setup flow
|
|
184
|
+
type ClaudeSetupStep = 'idle' | 'loading-url' | 'waiting-code' | 'submitting' | 'success' | 'error';
|
|
185
|
+
let claudeSetupStep = $state<ClaudeSetupStep>('idle');
|
|
186
|
+
let claudeSetupId = $state<string | null>(null);
|
|
187
|
+
let claudeAuthUrl = $state<string | null>(null);
|
|
188
|
+
let claudeAuthCode = $state('');
|
|
189
|
+
let claudeAccountName = $state('');
|
|
190
|
+
let claudeSetupError = $state('');
|
|
191
|
+
let claudeUrlCopied = $state(false);
|
|
192
|
+
|
|
193
|
+
// Event listener cleanup
|
|
194
|
+
const wsCleanups: Array<() => void> = [];
|
|
195
|
+
|
|
196
|
+
onMount(() => {
|
|
197
|
+
wsCleanups.push(
|
|
198
|
+
ws.on('engine:claude-account-setup-url', (data: { setupId: string; authUrl: string }) => {
|
|
199
|
+
claudeSetupId = data.setupId;
|
|
200
|
+
claudeAuthUrl = data.authUrl;
|
|
201
|
+
claudeSetupStep = 'waiting-code';
|
|
202
|
+
}),
|
|
203
|
+
ws.on('engine:claude-account-setup-complete', async () => {
|
|
204
|
+
claudeSetupStep = 'success';
|
|
205
|
+
await claudeAccountsStore.refresh();
|
|
206
|
+
}),
|
|
207
|
+
ws.on('engine:claude-account-setup-error', (data: { setupId: string; message: string }) => {
|
|
208
|
+
claudeSetupError = data.message;
|
|
209
|
+
claudeSetupStep = 'error';
|
|
210
|
+
})
|
|
211
|
+
);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
onDestroy(() => {
|
|
215
|
+
for (const cleanup of wsCleanups) cleanup();
|
|
216
|
+
wsCleanups.length = 0;
|
|
217
|
+
if (claudeSetupId && claudeSetupStep !== 'idle' && claudeSetupStep !== 'success' && claudeSetupStep !== 'error') {
|
|
218
|
+
ws.emit('engine:claude-account-setup-cancel', { setupId: claudeSetupId });
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
function startClaudeSetup() {
|
|
223
|
+
claudeSetupStep = 'loading-url';
|
|
224
|
+
claudeSetupError = '';
|
|
225
|
+
claudeAuthCode = '';
|
|
226
|
+
claudeAccountName = '';
|
|
227
|
+
ws.emit('engine:claude-account-setup-start', {});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function submitClaudeAuth() {
|
|
231
|
+
if (!claudeSetupId || !claudeAuthCode.trim() || !claudeAccountName.trim()) return;
|
|
232
|
+
claudeSetupStep = 'submitting';
|
|
233
|
+
claudeSetupError = '';
|
|
234
|
+
ws.emit('engine:claude-account-setup-submit', {
|
|
235
|
+
setupId: claudeSetupId,
|
|
236
|
+
code: claudeAuthCode.trim(),
|
|
237
|
+
name: claudeAccountName.trim()
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function cancelClaudeSetup() {
|
|
242
|
+
if (claudeSetupId) {
|
|
243
|
+
ws.emit('engine:claude-account-setup-cancel', { setupId: claudeSetupId });
|
|
244
|
+
}
|
|
245
|
+
resetClaudeSetup();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function resetClaudeSetup() {
|
|
249
|
+
claudeSetupStep = 'idle';
|
|
250
|
+
claudeSetupId = null;
|
|
251
|
+
claudeAuthUrl = null;
|
|
252
|
+
claudeAuthCode = '';
|
|
253
|
+
claudeAccountName = '';
|
|
254
|
+
claudeSetupError = '';
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function copyClaudeAuthUrl() {
|
|
258
|
+
if (!claudeAuthUrl) return;
|
|
259
|
+
await navigator.clipboard.writeText(claudeAuthUrl);
|
|
260
|
+
claudeUrlCopied = true;
|
|
261
|
+
setTimeout(() => { claudeUrlCopied = false; }, 2000);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Install guide state
|
|
265
|
+
type ClaudeInstallTab = 'unix' | 'powershell';
|
|
266
|
+
type OpenCodeInstallTab = 'unix' | 'bun';
|
|
267
|
+
let activeClaudeInstallTab = $state<ClaudeInstallTab>('unix');
|
|
268
|
+
let activeOpenCodeInstallTab = $state<OpenCodeInstallTab>('unix');
|
|
269
|
+
let claudeCommandCopied = $state(false);
|
|
270
|
+
let openCodeCommandCopied = $state(false);
|
|
271
|
+
|
|
272
|
+
const claudeInstallCommands: Record<ClaudeInstallTab, { label: string; command: string }> = {
|
|
273
|
+
unix: { label: 'macOS / Linux / WSL', command: 'curl -fsSL https://claude.ai/install.sh | bash' },
|
|
274
|
+
powershell: { label: 'Windows PowerShell', command: 'irm https://claude.ai/install.ps1 | iex' },
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
const openCodeInstallCommands: Record<OpenCodeInstallTab, { label: string; command: string }> = {
|
|
278
|
+
unix: { label: 'macOS / Linux / WSL', command: 'curl -fsSL https://opencode.ai/install | bash' },
|
|
279
|
+
bun: { label: 'Bun', command: 'bun add -g opencode-ai' },
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
async function copyClaudeCommand() {
|
|
283
|
+
await navigator.clipboard.writeText(claudeInstallCommands[activeClaudeInstallTab].command);
|
|
284
|
+
claudeCommandCopied = true;
|
|
285
|
+
setTimeout(() => { claudeCommandCopied = false; }, 2000);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async function copyOpenCodeCommand() {
|
|
289
|
+
await navigator.clipboard.writeText(openCodeInstallCommands[activeOpenCodeInstallTab].command);
|
|
290
|
+
openCodeCommandCopied = true;
|
|
291
|
+
setTimeout(() => { openCodeCommandCopied = false; }, 2000);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async function checkEngines() {
|
|
295
|
+
isLoadingEngines = true;
|
|
296
|
+
try {
|
|
297
|
+
const [claude, opencode] = await Promise.all([
|
|
298
|
+
ws.http('engine:claude-status', {}).catch(() => null),
|
|
299
|
+
ws.http('engine:opencode-status', {}).catch(() => null)
|
|
300
|
+
]);
|
|
301
|
+
claudeStatus = claude;
|
|
302
|
+
openCodeStatus = opencode;
|
|
303
|
+
|
|
304
|
+
if (claude) {
|
|
305
|
+
activeClaudeInstallTab = claude.backendOS === 'windows' ? 'powershell' : 'unix';
|
|
306
|
+
if (claude.installed) {
|
|
307
|
+
await claudeAccountsStore.refresh();
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
if (opencode) {
|
|
311
|
+
activeOpenCodeInstallTab = opencode.backendOS === 'windows' ? 'bun' : 'unix';
|
|
312
|
+
}
|
|
313
|
+
} catch {
|
|
314
|
+
// Ignore
|
|
315
|
+
}
|
|
316
|
+
isLoadingEngines = false;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Load engines when reaching that step
|
|
320
|
+
$effect(() => {
|
|
321
|
+
if (currentStep === 'engines' && !claudeStatus && !isLoadingEngines) {
|
|
322
|
+
checkEngines();
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
// ─── Step 4: Preferences ───
|
|
327
|
+
const FONT_SIZE_MIN = 10;
|
|
328
|
+
const FONT_SIZE_MAX = 20;
|
|
329
|
+
|
|
330
|
+
function handleFontSizeChange(e: Event) {
|
|
331
|
+
const value = Number((e.target as HTMLInputElement).value);
|
|
332
|
+
applyFontSize(value);
|
|
333
|
+
updateSettings({ fontSize: value });
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function fontSizePercent() {
|
|
337
|
+
return ((settings.fontSize - FONT_SIZE_MIN) / (FONT_SIZE_MAX - FONT_SIZE_MIN)) * 100;
|
|
338
|
+
}
|
|
339
|
+
</script>
|
|
340
|
+
|
|
341
|
+
<div class="fixed inset-0 z-9999 bg-white dark:bg-slate-950 overflow-y-auto">
|
|
342
|
+
<div class="min-h-full grid place-items-center px-4 py-8">
|
|
343
|
+
<div class="flex flex-col items-center gap-6 text-center max-w-lg w-full">
|
|
344
|
+
<!-- Logo -->
|
|
345
|
+
<div>
|
|
346
|
+
<img src="/favicon.svg" alt="Clopen" class="w-14 h-14 rounded-2xl shadow-xl" />
|
|
347
|
+
</div>
|
|
348
|
+
|
|
349
|
+
<div class="space-y-1">
|
|
350
|
+
<h1 class="text-2xl font-bold text-slate-900 dark:text-slate-100">Welcome to Clopen</h1>
|
|
351
|
+
<p class="text-sm text-slate-500 dark:text-slate-400">Let's set things up in a few quick steps.<br>All of these can be changed later in Settings.</p>
|
|
352
|
+
</div>
|
|
353
|
+
|
|
354
|
+
<!-- Stepper -->
|
|
355
|
+
<div class="flex items-center w-full max-w-sm">
|
|
356
|
+
{#each visibleSteps as step, i (step)}
|
|
357
|
+
{@const isActive = step === currentStep}
|
|
358
|
+
{@const currentIdx = visibleSteps.indexOf(currentStep)}
|
|
359
|
+
{@const isPast = i < currentIdx}
|
|
360
|
+
{@const isCompleted = completedSteps.has(step) && isPast}
|
|
361
|
+
{@const info = stepLabels[step]}
|
|
362
|
+
|
|
363
|
+
{#if i > 0}
|
|
364
|
+
<div class="flex-1 h-0.5 mx-1 rounded-full {i <= currentIdx ? 'bg-violet-400 dark:bg-violet-500' : 'bg-slate-200 dark:bg-slate-700'}"></div>
|
|
365
|
+
{/if}
|
|
366
|
+
|
|
367
|
+
<button
|
|
368
|
+
type="button"
|
|
369
|
+
class="flex items-center justify-center w-8 h-8 rounded-full transition-colors shrink-0
|
|
370
|
+
{isActive
|
|
371
|
+
? 'bg-violet-600 text-white shadow-sm shadow-violet-500/30'
|
|
372
|
+
: isCompleted
|
|
373
|
+
? 'bg-violet-600 text-white'
|
|
374
|
+
: 'bg-slate-200 dark:bg-slate-700 text-slate-400 dark:text-slate-500'}"
|
|
375
|
+
disabled={!isPast}
|
|
376
|
+
onclick={() => { if (isPast) currentStep = step; }}
|
|
377
|
+
title={info.label}
|
|
378
|
+
>
|
|
379
|
+
{#if isCompleted}
|
|
380
|
+
<Icon name="lucide:check" class="w-4 h-4" />
|
|
381
|
+
{:else}
|
|
382
|
+
<span class="text-xs font-bold">{i + 1}</span>
|
|
383
|
+
{/if}
|
|
384
|
+
</button>
|
|
385
|
+
{/each}
|
|
386
|
+
</div>
|
|
387
|
+
|
|
388
|
+
<!-- Step Content -->
|
|
389
|
+
<div class="w-full">
|
|
390
|
+
<!-- ════════ Step 1: Auth Mode ════════ -->
|
|
391
|
+
{#if currentStep === 'auth-mode'}
|
|
392
|
+
<div class="space-y-4">
|
|
393
|
+
<div class="text-center">
|
|
394
|
+
<h2 class="text-base font-semibold text-slate-900 dark:text-slate-100 mb-1">Authentication Mode</h2>
|
|
395
|
+
<p class="text-sm text-slate-500 dark:text-slate-400">
|
|
396
|
+
Choose how users access Clopen.
|
|
397
|
+
</p>
|
|
398
|
+
</div>
|
|
399
|
+
|
|
400
|
+
<div class="grid gap-3">
|
|
401
|
+
<!-- No Login -->
|
|
402
|
+
<button
|
|
403
|
+
type="button"
|
|
404
|
+
class="w-full text-left p-4 rounded-xl border-2 transition-all
|
|
405
|
+
{selectedAuthMode === 'none'
|
|
406
|
+
? 'border-violet-500 bg-violet-50/50 dark:bg-violet-900/10'
|
|
407
|
+
: 'border-slate-200 dark:border-slate-700 hover:border-slate-300 dark:hover:border-slate-600'}"
|
|
408
|
+
onclick={() => { selectedAuthMode = 'none'; }}
|
|
409
|
+
>
|
|
410
|
+
<div class="flex items-start gap-3">
|
|
411
|
+
<div class="flex items-center justify-center w-10 h-10 rounded-lg shrink-0
|
|
412
|
+
{selectedAuthMode === 'none'
|
|
413
|
+
? 'bg-violet-100 dark:bg-violet-900/30 text-violet-600 dark:text-violet-400'
|
|
414
|
+
: 'bg-slate-100 dark:bg-slate-800 text-slate-400'}">
|
|
415
|
+
<Icon name="lucide:lock-open" class="w-5 h-5" />
|
|
416
|
+
</div>
|
|
417
|
+
<div class="flex-1 min-w-0">
|
|
418
|
+
<div class="text-sm font-semibold text-slate-900 dark:text-slate-100">No Login</div>
|
|
419
|
+
<div class="text-xs text-slate-500 dark:text-slate-400 mt-0.5">
|
|
420
|
+
No authentication required. Anyone with access to this URL can use Clopen. Ideal for personal or local use.
|
|
421
|
+
</div>
|
|
422
|
+
</div>
|
|
423
|
+
{#if selectedAuthMode === 'none'}
|
|
424
|
+
<Icon name="lucide:circle-check" class="w-5 h-5 shrink-0 text-violet-500 ml-auto mt-0.5" />
|
|
425
|
+
{/if}
|
|
426
|
+
</div>
|
|
427
|
+
</button>
|
|
428
|
+
|
|
429
|
+
<!-- With Login -->
|
|
430
|
+
<button
|
|
431
|
+
type="button"
|
|
432
|
+
class="w-full text-left p-4 rounded-xl border-2 transition-all
|
|
433
|
+
{selectedAuthMode === 'required'
|
|
434
|
+
? 'border-violet-500 bg-violet-50/50 dark:bg-violet-900/10'
|
|
435
|
+
: 'border-slate-200 dark:border-slate-700 hover:border-slate-300 dark:hover:border-slate-600'}"
|
|
436
|
+
onclick={() => { selectedAuthMode = 'required'; }}
|
|
437
|
+
>
|
|
438
|
+
<div class="flex items-start gap-3">
|
|
439
|
+
<div class="flex items-center justify-center w-10 h-10 rounded-lg shrink-0
|
|
440
|
+
{selectedAuthMode === 'required'
|
|
441
|
+
? 'bg-violet-100 dark:bg-violet-900/30 text-violet-600 dark:text-violet-400'
|
|
442
|
+
: 'bg-slate-100 dark:bg-slate-800 text-slate-400'}">
|
|
443
|
+
<Icon name="lucide:lock" class="w-5 h-5" />
|
|
444
|
+
</div>
|
|
445
|
+
<div class="flex-1 min-w-0">
|
|
446
|
+
<div class="text-sm font-semibold text-slate-900 dark:text-slate-100">With Login</div>
|
|
447
|
+
<div class="text-xs text-slate-500 dark:text-slate-400 mt-0.5">
|
|
448
|
+
Authenticate with a Personal Access Token. Supports multiple users and invite links.
|
|
449
|
+
</div>
|
|
450
|
+
</div>
|
|
451
|
+
{#if selectedAuthMode === 'required'}
|
|
452
|
+
<Icon name="lucide:circle-check" class="w-5 h-5 shrink-0 text-violet-500 ml-auto mt-0.5" />
|
|
453
|
+
{/if}
|
|
454
|
+
</div>
|
|
455
|
+
</button>
|
|
456
|
+
</div>
|
|
457
|
+
|
|
458
|
+
{#if authModeError}
|
|
459
|
+
<p class="text-sm text-red-500">{authModeError}</p>
|
|
460
|
+
{/if}
|
|
461
|
+
|
|
462
|
+
<button
|
|
463
|
+
onclick={confirmAuthMode}
|
|
464
|
+
disabled={authModeLoading}
|
|
465
|
+
class="w-full py-2.5 px-4 rounded-lg bg-violet-600 hover:bg-violet-700 text-white text-sm font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
466
|
+
>
|
|
467
|
+
{#if authModeLoading}
|
|
468
|
+
<span class="inline-flex items-center gap-2">
|
|
469
|
+
<Icon name="lucide:loader" class="w-4 h-4 animate-spin" />
|
|
470
|
+
Setting up...
|
|
471
|
+
</span>
|
|
472
|
+
{:else}
|
|
473
|
+
Continue
|
|
474
|
+
{/if}
|
|
475
|
+
</button>
|
|
476
|
+
</div>
|
|
477
|
+
|
|
478
|
+
<!-- ════════ Step 2: Admin Account ════════ -->
|
|
479
|
+
{:else if currentStep === 'admin-account'}
|
|
480
|
+
<div class="space-y-4">
|
|
481
|
+
{#if !showPAT}
|
|
482
|
+
<div class="text-center">
|
|
483
|
+
<h2 class="text-base font-semibold text-slate-900 dark:text-slate-100 mb-1">
|
|
484
|
+
{isExistingUser ? 'Admin Account' : 'Create Admin Account'}
|
|
485
|
+
</h2>
|
|
486
|
+
<p class="text-sm text-slate-500 dark:text-slate-400">
|
|
487
|
+
{isExistingUser
|
|
488
|
+
? 'Review or update your admin display name.'
|
|
489
|
+
: 'Set a display name for the admin account.'}
|
|
490
|
+
</p>
|
|
491
|
+
</div>
|
|
492
|
+
|
|
493
|
+
<div class="text-left">
|
|
494
|
+
<label for="admin-name" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">
|
|
495
|
+
Display Name
|
|
496
|
+
</label>
|
|
497
|
+
<input
|
|
498
|
+
id="admin-name"
|
|
499
|
+
type="text"
|
|
500
|
+
bind:value={adminName}
|
|
501
|
+
onkeydown={handleAdminKeydown}
|
|
502
|
+
placeholder="Enter your name"
|
|
503
|
+
disabled={adminLoading}
|
|
504
|
+
class="w-full px-3 py-2 rounded-lg border border-slate-300 dark:border-slate-700 bg-white dark:bg-slate-900 text-slate-900 dark:text-slate-100 text-sm focus:outline-none focus:ring-2 focus:ring-violet-500 disabled:opacity-50"
|
|
505
|
+
/>
|
|
506
|
+
</div>
|
|
507
|
+
|
|
508
|
+
{#if adminError}
|
|
509
|
+
<p class="text-sm text-red-500">{adminError}</p>
|
|
510
|
+
{/if}
|
|
511
|
+
|
|
512
|
+
<div class="flex gap-2">
|
|
513
|
+
<button
|
|
514
|
+
onclick={goToPrevStep}
|
|
515
|
+
class="px-4 py-2 rounded-lg border border-slate-300 dark:border-slate-700 text-slate-600 dark:text-slate-400 text-sm font-medium hover:bg-slate-50 dark:hover:bg-slate-800 transition-colors"
|
|
516
|
+
>
|
|
517
|
+
Back
|
|
518
|
+
</button>
|
|
519
|
+
<button
|
|
520
|
+
onclick={handleCreateAdmin}
|
|
521
|
+
disabled={adminLoading}
|
|
522
|
+
class="flex-1 py-2 px-4 rounded-lg bg-violet-600 hover:bg-violet-700 text-white text-sm font-medium transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
523
|
+
>
|
|
524
|
+
{#if adminLoading}
|
|
525
|
+
Saving...
|
|
526
|
+
{:else}
|
|
527
|
+
Continue
|
|
528
|
+
{/if}
|
|
529
|
+
</button>
|
|
530
|
+
</div>
|
|
531
|
+
{:else}
|
|
532
|
+
<!-- PAT Display -->
|
|
533
|
+
<div class="text-left">
|
|
534
|
+
<h2 class="text-base font-semibold text-slate-900 dark:text-slate-100 mb-1">Save Your Token</h2>
|
|
535
|
+
</div>
|
|
536
|
+
|
|
537
|
+
<div class="p-4 rounded-lg bg-amber-50 dark:bg-amber-950/30 border border-amber-200 dark:border-amber-800 text-left">
|
|
538
|
+
<p class="text-sm font-medium text-amber-800 dark:text-amber-200 mb-2">
|
|
539
|
+
Your Personal Access Token
|
|
540
|
+
</p>
|
|
541
|
+
<p class="text-xs text-amber-700 dark:text-amber-300 mb-3">
|
|
542
|
+
Save this token — you'll need it to log in on other devices. It won't be shown again.
|
|
543
|
+
</p>
|
|
544
|
+
<div class="flex items-center gap-2">
|
|
545
|
+
<code class="flex-1 px-3 py-2 rounded bg-white dark:bg-slate-900 border border-amber-300 dark:border-amber-700 text-xs font-mono text-slate-900 dark:text-slate-100 select-all break-all">
|
|
546
|
+
{authStore.personalAccessToken}
|
|
547
|
+
</code>
|
|
548
|
+
<button
|
|
549
|
+
onclick={copyPAT}
|
|
550
|
+
class="shrink-0 px-3 py-2 rounded bg-amber-100 dark:bg-amber-900 hover:bg-amber-200 dark:hover:bg-amber-800 text-amber-800 dark:text-amber-200 text-xs font-medium transition-colors"
|
|
551
|
+
>
|
|
552
|
+
{patCopied ? 'Copied!' : 'Copy'}
|
|
553
|
+
</button>
|
|
554
|
+
</div>
|
|
555
|
+
<p class="text-xs text-amber-600 dark:text-amber-400 mt-3">
|
|
556
|
+
Lost your token? You can reset it anytime by running <code class="font-mono bg-amber-100 dark:bg-amber-900/50 px-1 py-0.5 rounded">clopen reset-pat</code> in your terminal.
|
|
557
|
+
</p>
|
|
558
|
+
</div>
|
|
559
|
+
|
|
560
|
+
<button
|
|
561
|
+
onclick={goToNextStep}
|
|
562
|
+
class="w-full py-2.5 px-4 rounded-lg bg-violet-600 hover:bg-violet-700 text-white text-sm font-medium transition-colors"
|
|
563
|
+
>
|
|
564
|
+
Continue
|
|
565
|
+
</button>
|
|
566
|
+
{/if}
|
|
567
|
+
</div>
|
|
568
|
+
|
|
569
|
+
<!-- ════════ Step 3: AI Engines ════════ -->
|
|
570
|
+
{:else if currentStep === 'engines'}
|
|
571
|
+
<div class="space-y-4">
|
|
572
|
+
<div class="text-center">
|
|
573
|
+
<h2 class="text-base font-semibold text-slate-900 dark:text-slate-100 mb-1">AI Engines</h2>
|
|
574
|
+
<p class="text-sm text-slate-500 dark:text-slate-400">
|
|
575
|
+
Check your AI engine installations.
|
|
576
|
+
</p>
|
|
577
|
+
</div>
|
|
578
|
+
|
|
579
|
+
{#if isLoadingEngines}
|
|
580
|
+
<div class="flex items-center justify-center py-8">
|
|
581
|
+
<Icon name="lucide:loader" class="w-6 h-6 animate-spin text-slate-400" />
|
|
582
|
+
</div>
|
|
583
|
+
{:else}
|
|
584
|
+
<!-- Claude Code -->
|
|
585
|
+
<div class="text-left p-4 rounded-xl border border-slate-200 dark:border-slate-700/50 bg-white dark:bg-slate-800/50">
|
|
586
|
+
<div class="flex items-center justify-between mb-2">
|
|
587
|
+
<div class="flex items-center gap-2.5">
|
|
588
|
+
<div class="flex items-center justify-center [&>svg]:w-5 [&>svg]:h-5">
|
|
589
|
+
{@html isDarkMode() ? claudeEngine.icon.dark : claudeEngine.icon.light}
|
|
590
|
+
</div>
|
|
591
|
+
<span class="text-sm font-semibold text-slate-900 dark:text-slate-100">{claudeEngine.name}</span>
|
|
592
|
+
</div>
|
|
593
|
+
{#if claudeStatus?.installed}
|
|
594
|
+
<span class="inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400">
|
|
595
|
+
<span class="w-1.5 h-1.5 rounded-full bg-green-500"></span>
|
|
596
|
+
{claudeStatus.version || 'Installed'}
|
|
597
|
+
</span>
|
|
598
|
+
{:else}
|
|
599
|
+
<span class="inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-xs font-medium bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-400">
|
|
600
|
+
<span class="w-1.5 h-1.5 rounded-full bg-red-500"></span>
|
|
601
|
+
Not Installed
|
|
602
|
+
</span>
|
|
603
|
+
{/if}
|
|
604
|
+
</div>
|
|
605
|
+
<p class="text-xs text-slate-500 dark:text-slate-400">{claudeEngine.description}</p>
|
|
606
|
+
|
|
607
|
+
{#if claudeStatus?.installed}
|
|
608
|
+
<!-- Account Management -->
|
|
609
|
+
<div class="mt-3 pt-3 border-t border-slate-200 dark:border-slate-700/50 space-y-2.5">
|
|
610
|
+
<div class="flex items-center justify-between">
|
|
611
|
+
<span class="text-xs font-semibold text-slate-600 dark:text-slate-400">Accounts</span>
|
|
612
|
+
<span class="text-2xs text-slate-400">{claudeAccounts.length} account{claudeAccounts.length !== 1 ? 's' : ''}</span>
|
|
613
|
+
</div>
|
|
614
|
+
|
|
615
|
+
{#if claudeAccounts.length > 0}
|
|
616
|
+
<div class="space-y-1.5">
|
|
617
|
+
{#each claudeAccounts as account (account.id)}
|
|
618
|
+
<div class="flex items-center gap-2 px-3 py-2 rounded-lg bg-slate-50 dark:bg-slate-800/80 border border-slate-200 dark:border-slate-700/50 {account.isActive ? 'ring-1 ring-violet-500/30' : ''}">
|
|
619
|
+
<Icon name="lucide:user" class="w-3.5 h-3.5 shrink-0 text-slate-400" />
|
|
620
|
+
<span class="text-xs font-medium text-slate-900 dark:text-slate-100 truncate flex-1">{account.name}</span>
|
|
621
|
+
{#if account.isActive}
|
|
622
|
+
<span class="inline-flex items-center px-1.5 py-0.5 rounded-full text-3xs font-semibold bg-violet-100 dark:bg-violet-900/30 text-violet-700 dark:text-violet-300">Active</span>
|
|
623
|
+
{/if}
|
|
624
|
+
</div>
|
|
625
|
+
{/each}
|
|
626
|
+
</div>
|
|
627
|
+
{:else}
|
|
628
|
+
<p class="text-xs text-slate-400 italic">No accounts configured</p>
|
|
629
|
+
{/if}
|
|
630
|
+
|
|
631
|
+
<!-- Add Account Flow -->
|
|
632
|
+
{#if claudeSetupStep === 'idle'}
|
|
633
|
+
<button
|
|
634
|
+
type="button"
|
|
635
|
+
class="flex items-center gap-1.5 justify-center w-full px-3 py-2 text-xs font-medium rounded-lg border border-dashed border-slate-300 dark:border-slate-600 text-slate-600 dark:text-slate-400 hover:border-violet-400 hover:text-violet-600 dark:hover:text-violet-400 transition-colors"
|
|
636
|
+
onclick={startClaudeSetup}
|
|
637
|
+
>
|
|
638
|
+
<Icon name="lucide:plus" class="w-3.5 h-3.5" />
|
|
639
|
+
Add Account
|
|
640
|
+
</button>
|
|
641
|
+
{:else if claudeSetupStep === 'loading-url'}
|
|
642
|
+
<div class="flex items-center justify-center gap-2 py-3 text-xs text-slate-500">
|
|
643
|
+
<Icon name="lucide:loader" class="w-3.5 h-3.5 animate-spin" />
|
|
644
|
+
Starting authentication...
|
|
645
|
+
</div>
|
|
646
|
+
{:else if claudeSetupStep === 'waiting-code'}
|
|
647
|
+
<div class="space-y-2.5 p-3 rounded-lg border border-violet-200 dark:border-violet-800/50 bg-violet-50/50 dark:bg-violet-900/10">
|
|
648
|
+
<p class="text-xs text-slate-600 dark:text-slate-400">
|
|
649
|
+
Open the URL below, sign in, then paste the code back here.
|
|
650
|
+
</p>
|
|
651
|
+
<div class="bg-white dark:bg-slate-800 rounded-lg px-2.5 py-1.5 text-2xs font-mono text-slate-700 dark:text-slate-300 break-all border border-slate-200 dark:border-slate-700">
|
|
652
|
+
{claudeAuthUrl}
|
|
653
|
+
</div>
|
|
654
|
+
<div class="flex gap-1.5">
|
|
655
|
+
<button
|
|
656
|
+
type="button"
|
|
657
|
+
class="flex items-center gap-1 px-2.5 py-1.5 text-2xs font-medium rounded-md transition-colors
|
|
658
|
+
{claudeUrlCopied
|
|
659
|
+
? 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300'
|
|
660
|
+
: 'bg-slate-100 dark:bg-slate-700 text-slate-700 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-600'}"
|
|
661
|
+
onclick={copyClaudeAuthUrl}
|
|
662
|
+
>
|
|
663
|
+
<Icon name={claudeUrlCopied ? 'lucide:check' : 'lucide:copy'} class="w-3 h-3" />
|
|
664
|
+
{claudeUrlCopied ? 'Copied' : 'Copy URL'}
|
|
665
|
+
</button>
|
|
666
|
+
<a
|
|
667
|
+
href={claudeAuthUrl}
|
|
668
|
+
target="_blank"
|
|
669
|
+
rel="noopener noreferrer"
|
|
670
|
+
class="flex items-center gap-1 px-2.5 py-1.5 text-2xs font-medium rounded-md bg-violet-100 dark:bg-violet-900/30 text-violet-700 dark:text-violet-300 hover:bg-violet-200 dark:hover:bg-violet-800/40 transition-colors"
|
|
671
|
+
>
|
|
672
|
+
<Icon name="lucide:external-link" class="w-3 h-3" />
|
|
673
|
+
Open
|
|
674
|
+
</a>
|
|
675
|
+
</div>
|
|
676
|
+
|
|
677
|
+
<div class="space-y-1.5">
|
|
678
|
+
<input
|
|
679
|
+
type="text"
|
|
680
|
+
bind:value={claudeAuthCode}
|
|
681
|
+
placeholder="Paste authentication code"
|
|
682
|
+
class="w-full px-2.5 py-1.5 text-xs rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-violet-500/40"
|
|
683
|
+
/>
|
|
684
|
+
<input
|
|
685
|
+
type="text"
|
|
686
|
+
bind:value={claudeAccountName}
|
|
687
|
+
placeholder="Account name (e.g. Personal, Work)"
|
|
688
|
+
class="w-full px-2.5 py-1.5 text-xs rounded-lg border border-slate-300 dark:border-slate-600 bg-white dark:bg-slate-800 text-slate-900 dark:text-slate-100 placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-violet-500/40"
|
|
689
|
+
/>
|
|
690
|
+
</div>
|
|
691
|
+
|
|
692
|
+
<div class="flex gap-1.5">
|
|
693
|
+
<button
|
|
694
|
+
type="button"
|
|
695
|
+
class="flex-1 flex items-center justify-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-lg bg-violet-600 text-white hover:bg-violet-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
696
|
+
onclick={submitClaudeAuth}
|
|
697
|
+
disabled={!claudeAuthCode.trim() || !claudeAccountName.trim()}
|
|
698
|
+
>
|
|
699
|
+
<Icon name="lucide:send" class="w-3.5 h-3.5" />
|
|
700
|
+
Submit
|
|
701
|
+
</button>
|
|
702
|
+
<button
|
|
703
|
+
type="button"
|
|
704
|
+
class="px-3 py-1.5 text-xs font-medium rounded-lg border border-slate-300 dark:border-slate-600 text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-700 transition-colors"
|
|
705
|
+
onclick={cancelClaudeSetup}
|
|
706
|
+
>
|
|
707
|
+
Cancel
|
|
708
|
+
</button>
|
|
709
|
+
</div>
|
|
710
|
+
</div>
|
|
711
|
+
{:else if claudeSetupStep === 'submitting'}
|
|
712
|
+
<div class="flex items-center justify-center gap-2 py-3 text-xs text-slate-500">
|
|
713
|
+
<Icon name="lucide:loader" class="w-3.5 h-3.5 animate-spin" />
|
|
714
|
+
Verifying...
|
|
715
|
+
</div>
|
|
716
|
+
{:else if claudeSetupStep === 'success'}
|
|
717
|
+
<div class="flex items-center gap-2 p-2.5 rounded-lg bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800/50">
|
|
718
|
+
<Icon name="lucide:circle-check" class="w-4 h-4 text-green-600 dark:text-green-400" />
|
|
719
|
+
<span class="text-xs text-green-700 dark:text-green-300">Account added!</span>
|
|
720
|
+
<button type="button" class="ml-auto text-2xs text-green-600 dark:text-green-400 hover:underline" onclick={resetClaudeSetup}>Dismiss</button>
|
|
721
|
+
</div>
|
|
722
|
+
{:else if claudeSetupStep === 'error'}
|
|
723
|
+
<div class="space-y-2">
|
|
724
|
+
<div class="flex items-center gap-2 p-2.5 rounded-lg bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800/50">
|
|
725
|
+
<Icon name="lucide:circle-alert" class="w-4 h-4 shrink-0 text-red-600 dark:text-red-400" />
|
|
726
|
+
<span class="text-xs text-red-700 dark:text-red-300">{claudeSetupError}</span>
|
|
727
|
+
</div>
|
|
728
|
+
<button type="button" class="flex items-center justify-center gap-1.5 w-full px-3 py-1.5 text-xs font-medium rounded-lg border border-slate-300 dark:border-slate-700 text-slate-600 dark:text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors" onclick={resetClaudeSetup}>
|
|
729
|
+
<Icon name="lucide:rotate-ccw" class="w-3.5 h-3.5" />
|
|
730
|
+
Try Again
|
|
731
|
+
</button>
|
|
732
|
+
</div>
|
|
733
|
+
{/if}
|
|
734
|
+
</div>
|
|
735
|
+
{:else if claudeStatus}
|
|
736
|
+
<!-- Install Guide -->
|
|
737
|
+
<div class="mt-3 pt-3 border-t border-slate-200 dark:border-slate-700/50 space-y-3">
|
|
738
|
+
<p class="text-xs text-slate-600 dark:text-slate-300">Install using one of the methods below:</p>
|
|
739
|
+
|
|
740
|
+
<div class="flex flex-wrap gap-1.5">
|
|
741
|
+
{#each Object.entries(claudeInstallCommands) as [key, { label }]}
|
|
742
|
+
<button
|
|
743
|
+
type="button"
|
|
744
|
+
class="px-2.5 py-1 rounded-lg text-2xs font-medium transition-colors
|
|
745
|
+
{activeClaudeInstallTab === key
|
|
746
|
+
? 'bg-violet-500/15 text-violet-700 dark:text-violet-300'
|
|
747
|
+
: 'bg-slate-100 dark:bg-slate-700/50 text-slate-600 dark:text-slate-400 hover:bg-slate-200 dark:hover:bg-slate-700'}"
|
|
748
|
+
onclick={() => (activeClaudeInstallTab = key as ClaudeInstallTab)}
|
|
749
|
+
>
|
|
750
|
+
{label}
|
|
751
|
+
</button>
|
|
752
|
+
{/each}
|
|
753
|
+
</div>
|
|
754
|
+
|
|
755
|
+
<div class="relative group">
|
|
756
|
+
<pre class="bg-slate-100 dark:bg-slate-950 text-slate-800 dark:text-slate-200 rounded-lg px-3 py-2 text-xs font-mono overflow-x-auto">{claudeInstallCommands[activeClaudeInstallTab].command}</pre>
|
|
757
|
+
<button
|
|
758
|
+
type="button"
|
|
759
|
+
class="flex absolute top-1.5 right-1.5 p-1 rounded-md transition-colors {claudeCommandCopied ? 'bg-violet-600/80 text-white' : 'bg-slate-300/80 dark:bg-slate-700/80 text-slate-600 dark:text-slate-300 hover:bg-slate-400/80 dark:hover:bg-slate-600'}"
|
|
760
|
+
onclick={copyClaudeCommand}
|
|
761
|
+
aria-label="Copy command"
|
|
762
|
+
>
|
|
763
|
+
<Icon name={claudeCommandCopied ? 'lucide:check' : 'lucide:copy'} class="w-3 h-3" />
|
|
764
|
+
</button>
|
|
765
|
+
</div>
|
|
766
|
+
|
|
767
|
+
{#if claudeStatus.backendOS === 'windows'}
|
|
768
|
+
<div class="flex gap-2 p-2.5 rounded-lg bg-amber-50 dark:bg-amber-900/15 border border-amber-200 dark:border-amber-700/50">
|
|
769
|
+
<Icon name="lucide:info" class="w-3.5 h-3.5 shrink-0 mt-0.5 text-amber-600 dark:text-amber-400" />
|
|
770
|
+
<div class="text-2xs text-amber-800 dark:text-amber-300 space-y-0.5">
|
|
771
|
+
<p class="font-medium">Git Bash is required</p>
|
|
772
|
+
<p class="text-amber-700 dark:text-amber-400">
|
|
773
|
+
Claude Code requires Git Bash on Windows.
|
|
774
|
+
</p>
|
|
775
|
+
</div>
|
|
776
|
+
</div>
|
|
777
|
+
{/if}
|
|
778
|
+
|
|
779
|
+
<div class="flex items-center gap-1.5 text-2xs text-slate-500 dark:text-slate-400">
|
|
780
|
+
<Icon name="lucide:book-open" class="w-3 h-3 shrink-0" />
|
|
781
|
+
<a
|
|
782
|
+
href="https://code.claude.com/docs/en/quickstart"
|
|
783
|
+
target="_blank"
|
|
784
|
+
rel="noopener noreferrer"
|
|
785
|
+
class="font-medium text-violet-600 dark:text-violet-400 hover:text-violet-800 dark:hover:text-violet-200 underline underline-offset-2"
|
|
786
|
+
>
|
|
787
|
+
Official documentation
|
|
788
|
+
</a>
|
|
789
|
+
</div>
|
|
790
|
+
</div>
|
|
791
|
+
{/if}
|
|
792
|
+
</div>
|
|
793
|
+
|
|
794
|
+
<!-- OpenCode -->
|
|
795
|
+
<div class="text-left p-4 rounded-xl border border-slate-200 dark:border-slate-700/50 bg-white dark:bg-slate-800/50">
|
|
796
|
+
<div class="flex items-center justify-between mb-2">
|
|
797
|
+
<div class="flex items-center gap-2.5">
|
|
798
|
+
<div class="flex items-center justify-center [&>svg]:w-5 [&>svg]:h-5">
|
|
799
|
+
{@html isDarkMode() ? openCodeEngine.icon.dark : openCodeEngine.icon.light}
|
|
800
|
+
</div>
|
|
801
|
+
<span class="text-sm font-semibold text-slate-900 dark:text-slate-100">{openCodeEngine.name}</span>
|
|
802
|
+
</div>
|
|
803
|
+
{#if openCodeStatus?.installed}
|
|
804
|
+
<span class="inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400">
|
|
805
|
+
<span class="w-1.5 h-1.5 rounded-full bg-green-500"></span>
|
|
806
|
+
{openCodeStatus.version || 'Installed'}
|
|
807
|
+
</span>
|
|
808
|
+
{:else}
|
|
809
|
+
<span class="inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-xs font-medium bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-400">
|
|
810
|
+
<span class="w-1.5 h-1.5 rounded-full bg-red-500"></span>
|
|
811
|
+
Not Installed
|
|
812
|
+
</span>
|
|
813
|
+
{/if}
|
|
814
|
+
</div>
|
|
815
|
+
<p class="text-xs text-slate-500 dark:text-slate-400">{openCodeEngine.description}</p>
|
|
816
|
+
|
|
817
|
+
{#if openCodeStatus && !openCodeStatus.installed}
|
|
818
|
+
<!-- Install Guide -->
|
|
819
|
+
<div class="mt-3 pt-3 border-t border-slate-200 dark:border-slate-700/50 space-y-3">
|
|
820
|
+
<p class="text-xs text-slate-600 dark:text-slate-300">Install using one of the methods below:</p>
|
|
821
|
+
|
|
822
|
+
<div class="flex flex-wrap gap-1.5">
|
|
823
|
+
{#each Object.entries(openCodeInstallCommands) as [key, { label }]}
|
|
824
|
+
<button
|
|
825
|
+
type="button"
|
|
826
|
+
class="px-2.5 py-1 rounded-lg text-2xs font-medium transition-colors
|
|
827
|
+
{activeOpenCodeInstallTab === key
|
|
828
|
+
? 'bg-violet-500/15 text-violet-700 dark:text-violet-300'
|
|
829
|
+
: 'bg-slate-100 dark:bg-slate-700/50 text-slate-600 dark:text-slate-400 hover:bg-slate-200 dark:hover:bg-slate-700'}"
|
|
830
|
+
onclick={() => (activeOpenCodeInstallTab = key as OpenCodeInstallTab)}
|
|
831
|
+
>
|
|
832
|
+
{label}
|
|
833
|
+
</button>
|
|
834
|
+
{/each}
|
|
835
|
+
</div>
|
|
836
|
+
|
|
837
|
+
<div class="relative group">
|
|
838
|
+
<pre class="bg-slate-100 dark:bg-slate-950 text-slate-800 dark:text-slate-200 rounded-lg px-3 py-2 text-xs font-mono overflow-x-auto">{openCodeInstallCommands[activeOpenCodeInstallTab].command}</pre>
|
|
839
|
+
<button
|
|
840
|
+
type="button"
|
|
841
|
+
class="flex absolute top-1.5 right-1.5 p-1 rounded-md transition-colors {openCodeCommandCopied ? 'bg-violet-600/80 text-white' : 'bg-slate-300/80 dark:bg-slate-700/80 text-slate-600 dark:text-slate-300 hover:bg-slate-400/80 dark:hover:bg-slate-600'}"
|
|
842
|
+
onclick={copyOpenCodeCommand}
|
|
843
|
+
aria-label="Copy command"
|
|
844
|
+
>
|
|
845
|
+
<Icon name={openCodeCommandCopied ? 'lucide:check' : 'lucide:copy'} class="w-3 h-3" />
|
|
846
|
+
</button>
|
|
847
|
+
</div>
|
|
848
|
+
|
|
849
|
+
<div class="flex items-center gap-1.5 text-2xs text-slate-500 dark:text-slate-400">
|
|
850
|
+
<Icon name="lucide:book-open" class="w-3 h-3 shrink-0" />
|
|
851
|
+
<a
|
|
852
|
+
href="https://opencode.ai/docs"
|
|
853
|
+
target="_blank"
|
|
854
|
+
rel="noopener noreferrer"
|
|
855
|
+
class="font-medium text-violet-600 dark:text-violet-400 hover:text-violet-800 dark:hover:text-violet-200 underline underline-offset-2"
|
|
856
|
+
>
|
|
857
|
+
Official documentation
|
|
858
|
+
</a>
|
|
859
|
+
</div>
|
|
860
|
+
</div>
|
|
861
|
+
{/if}
|
|
862
|
+
</div>
|
|
863
|
+
|
|
864
|
+
<!-- Recheck button -->
|
|
865
|
+
<button
|
|
866
|
+
type="button"
|
|
867
|
+
onclick={checkEngines}
|
|
868
|
+
class="flex items-center justify-center gap-2 w-full py-2 px-4 text-xs font-medium rounded-lg border border-slate-200 dark:border-slate-700 text-slate-600 dark:text-slate-400 hover:bg-slate-50 dark:hover:bg-slate-800 transition-colors"
|
|
869
|
+
>
|
|
870
|
+
<Icon name="lucide:refresh-cw" class="w-3.5 h-3.5" />
|
|
871
|
+
Recheck Installation
|
|
872
|
+
</button>
|
|
873
|
+
{/if}
|
|
874
|
+
|
|
875
|
+
<div class="flex gap-2">
|
|
876
|
+
<button
|
|
877
|
+
onclick={goToPrevStep}
|
|
878
|
+
class="px-4 py-2 rounded-lg border border-slate-300 dark:border-slate-700 text-slate-600 dark:text-slate-400 text-sm font-medium hover:bg-slate-50 dark:hover:bg-slate-800 transition-colors"
|
|
879
|
+
>
|
|
880
|
+
Back
|
|
881
|
+
</button>
|
|
882
|
+
<button
|
|
883
|
+
onclick={goToNextStep}
|
|
884
|
+
class="flex-1 py-2.5 px-4 rounded-lg bg-violet-600 hover:bg-violet-700 text-white text-sm font-medium transition-colors"
|
|
885
|
+
>
|
|
886
|
+
Continue
|
|
887
|
+
</button>
|
|
888
|
+
</div>
|
|
889
|
+
</div>
|
|
890
|
+
|
|
891
|
+
<!-- ════════ Step 4: Preferences ════════ -->
|
|
892
|
+
{:else if currentStep === 'preferences'}
|
|
893
|
+
<div class="space-y-4">
|
|
894
|
+
<div class="text-center">
|
|
895
|
+
<h2 class="text-base font-semibold text-slate-900 dark:text-slate-100 mb-1">Preferences</h2>
|
|
896
|
+
<p class="text-sm text-slate-500 dark:text-slate-400">
|
|
897
|
+
Customize your experience.
|
|
898
|
+
</p>
|
|
899
|
+
</div>
|
|
900
|
+
|
|
901
|
+
<!-- Theme -->
|
|
902
|
+
<div class="text-left p-4 rounded-xl border border-slate-200 dark:border-slate-700/50 bg-white dark:bg-slate-800/50">
|
|
903
|
+
<div class="flex items-center justify-between">
|
|
904
|
+
<div class="flex items-center gap-3">
|
|
905
|
+
<div class="flex items-center justify-center w-9 h-9 rounded-lg bg-violet-500/10 text-violet-600 dark:text-violet-400">
|
|
906
|
+
<Icon name={themeStore.isDark ? 'lucide:moon' : 'lucide:sun'} class="w-4.5 h-4.5" />
|
|
907
|
+
</div>
|
|
908
|
+
<div>
|
|
909
|
+
<div class="text-sm font-semibold text-slate-900 dark:text-slate-100">Dark Mode</div>
|
|
910
|
+
<div class="text-xs text-slate-500 dark:text-slate-400">
|
|
911
|
+
Currently: <span class="font-medium">{themeStore.isDark ? 'Dark' : 'Light'}</span>
|
|
912
|
+
</div>
|
|
913
|
+
</div>
|
|
914
|
+
</div>
|
|
915
|
+
<label class="relative inline-block w-12 h-6.5 shrink-0">
|
|
916
|
+
<input
|
|
917
|
+
type="checkbox"
|
|
918
|
+
checked={themeStore.isDark}
|
|
919
|
+
onchange={toggleDarkMode}
|
|
920
|
+
class="opacity-0 w-0 h-0"
|
|
921
|
+
/>
|
|
922
|
+
<span
|
|
923
|
+
class="absolute cursor-pointer inset-0 bg-slate-600/40 rounded-3xl transition-all duration-200
|
|
924
|
+
before:absolute before:content-[''] before:h-5 before:w-5 before:left-0.75 before:bottom-0.75 before:bg-white before:rounded-full before:transition-all before:duration-200
|
|
925
|
+
{themeStore.isDark
|
|
926
|
+
? 'bg-gradient-to-br from-violet-600 to-purple-600 before:translate-x-5.5'
|
|
927
|
+
: ''}"
|
|
928
|
+
></span>
|
|
929
|
+
</label>
|
|
930
|
+
</div>
|
|
931
|
+
</div>
|
|
932
|
+
|
|
933
|
+
<!-- Sound Notifications -->
|
|
934
|
+
<div class="text-left p-4 rounded-xl border border-slate-200 dark:border-slate-700/50 bg-white dark:bg-slate-800/50">
|
|
935
|
+
<div class="flex items-center justify-between">
|
|
936
|
+
<div class="flex items-center gap-3">
|
|
937
|
+
<div class="flex items-center justify-center w-9 h-9 rounded-lg bg-violet-500/10 text-violet-600 dark:text-violet-400">
|
|
938
|
+
<Icon name="lucide:volume-2" class="w-4.5 h-4.5" />
|
|
939
|
+
</div>
|
|
940
|
+
<div>
|
|
941
|
+
<div class="text-sm font-semibold text-slate-900 dark:text-slate-100">Sound Notifications</div>
|
|
942
|
+
<div class="text-xs text-slate-500 dark:text-slate-400">Play sound when response completes</div>
|
|
943
|
+
</div>
|
|
944
|
+
</div>
|
|
945
|
+
<label class="relative inline-block w-12 h-6.5 shrink-0">
|
|
946
|
+
<input
|
|
947
|
+
type="checkbox"
|
|
948
|
+
checked={settings.soundNotifications}
|
|
949
|
+
onchange={() => updateSettings({ soundNotifications: !settings.soundNotifications })}
|
|
950
|
+
class="opacity-0 w-0 h-0"
|
|
951
|
+
/>
|
|
952
|
+
<span
|
|
953
|
+
class="absolute cursor-pointer inset-0 bg-slate-600/40 rounded-3xl transition-all duration-200
|
|
954
|
+
before:absolute before:content-[''] before:h-5 before:w-5 before:left-0.75 before:bottom-0.75 before:bg-white before:rounded-full before:transition-all before:duration-200
|
|
955
|
+
{settings.soundNotifications
|
|
956
|
+
? 'bg-gradient-to-br from-violet-600 to-purple-600 before:translate-x-5.5'
|
|
957
|
+
: ''}"
|
|
958
|
+
></span>
|
|
959
|
+
</label>
|
|
960
|
+
</div>
|
|
961
|
+
</div>
|
|
962
|
+
|
|
963
|
+
<!-- Font Size -->
|
|
964
|
+
<div class="text-left p-4 rounded-xl border border-slate-200 dark:border-slate-700/50 bg-white dark:bg-slate-800/50">
|
|
965
|
+
<div class="flex items-center gap-3 mb-3">
|
|
966
|
+
<div class="flex items-center justify-center w-9 h-9 rounded-lg bg-violet-500/10 text-violet-600 dark:text-violet-400">
|
|
967
|
+
<Icon name="lucide:type" class="w-4.5 h-4.5" />
|
|
968
|
+
</div>
|
|
969
|
+
<div class="flex-1">
|
|
970
|
+
<div class="text-sm font-semibold text-slate-900 dark:text-slate-100">Font Size</div>
|
|
971
|
+
<div class="text-xs text-slate-500 dark:text-slate-400">Adjust the base font size</div>
|
|
972
|
+
</div>
|
|
973
|
+
<div class="text-sm font-semibold text-violet-600 dark:text-violet-400">
|
|
974
|
+
{settings.fontSize}px
|
|
975
|
+
</div>
|
|
976
|
+
</div>
|
|
977
|
+
<div class="flex items-center gap-2.5 px-0.5">
|
|
978
|
+
<span class="text-xs text-slate-500 shrink-0">A</span>
|
|
979
|
+
<div class="relative flex-1 h-1.5">
|
|
980
|
+
<div class="absolute inset-0 bg-slate-300 dark:bg-slate-700 rounded-full"></div>
|
|
981
|
+
<div
|
|
982
|
+
class="absolute inset-y-0 left-0 bg-gradient-to-r from-violet-500 to-purple-500 rounded-full"
|
|
983
|
+
style="width: {fontSizePercent()}%"
|
|
984
|
+
></div>
|
|
985
|
+
<input
|
|
986
|
+
type="range"
|
|
987
|
+
min={FONT_SIZE_MIN}
|
|
988
|
+
max={FONT_SIZE_MAX}
|
|
989
|
+
step="1"
|
|
990
|
+
value={settings.fontSize}
|
|
991
|
+
oninput={handleFontSizeChange}
|
|
992
|
+
class="absolute inset-0 w-full opacity-0 cursor-pointer h-full"
|
|
993
|
+
/>
|
|
994
|
+
<div
|
|
995
|
+
class="absolute top-1/2 -translate-y-1/2 w-4 h-4 bg-white border-2 border-violet-500 rounded-full shadow-sm pointer-events-none"
|
|
996
|
+
style="left: calc({fontSizePercent()}% - {fontSizePercent() / 100 * 16}px)"
|
|
997
|
+
></div>
|
|
998
|
+
</div>
|
|
999
|
+
<span class="text-base text-slate-500 shrink-0">A</span>
|
|
1000
|
+
</div>
|
|
1001
|
+
</div>
|
|
1002
|
+
|
|
1003
|
+
<div class="flex gap-2">
|
|
1004
|
+
<button
|
|
1005
|
+
onclick={goToPrevStep}
|
|
1006
|
+
class="px-4 py-2 rounded-lg border border-slate-300 dark:border-slate-700 text-slate-600 dark:text-slate-400 text-sm font-medium hover:bg-slate-50 dark:hover:bg-slate-800 transition-colors"
|
|
1007
|
+
>
|
|
1008
|
+
Back
|
|
1009
|
+
</button>
|
|
1010
|
+
<button
|
|
1011
|
+
onclick={finishWizard}
|
|
1012
|
+
class="flex-1 py-2.5 px-4 rounded-lg bg-violet-600 hover:bg-violet-700 text-white text-sm font-medium transition-colors"
|
|
1013
|
+
>
|
|
1014
|
+
Finish Setup
|
|
1015
|
+
</button>
|
|
1016
|
+
</div>
|
|
1017
|
+
</div>
|
|
1018
|
+
{/if}
|
|
1019
|
+
</div>
|
|
1020
|
+
</div>
|
|
1021
|
+
</div>
|
|
1022
|
+
</div>
|