@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
|
@@ -407,7 +407,7 @@
|
|
|
407
407
|
class="
|
|
408
408
|
relative z-10 flex items-end gap-3 lg:gap-4 overflow-hidden bg-white dark:bg-slate-900
|
|
409
409
|
border border-slate-200 dark:border-slate-700 rounded-xl transition-all duration-200
|
|
410
|
-
focus-within:ring-
|
|
410
|
+
focus-within:ring-1 focus-within:ring-violet-500 {fileHandling.isDragging && 'ring-1 ring-violet-500'}"
|
|
411
411
|
role="region"
|
|
412
412
|
aria-label="Message input with file drop zone"
|
|
413
413
|
ondragover={fileHandling.handleDragOver}
|
|
@@ -428,7 +428,6 @@
|
|
|
428
428
|
placeholder={chatPlaceholder}
|
|
429
429
|
class="flex-1 w-full px-4 pt-2 pb-4 border-0 bg-transparent resize-none focus:outline-none text-slate-900 dark:text-slate-100 placeholder-slate-500 dark:placeholder-slate-400 text-base leading-relaxed disabled:opacity-50 disabled:cursor-not-allowed"
|
|
430
430
|
rows="1"
|
|
431
|
-
style="max-height: 22.5rem; overflow-y: hidden;"
|
|
432
431
|
disabled={isInputDisabled}
|
|
433
432
|
oninput={handleTextareaInput}
|
|
434
433
|
onkeydown={handleKeyDown}
|
|
@@ -122,9 +122,9 @@
|
|
|
122
122
|
// Model Picker (existing logic)
|
|
123
123
|
// ════════════════════════════════════════════
|
|
124
124
|
|
|
125
|
-
// Track whether a chat has started (any user message in current session)
|
|
125
|
+
// Track whether a chat has started (any user message in current session, or session has history e.g. restored to initial)
|
|
126
126
|
const hasStartedChat = $derived(
|
|
127
|
-
sessionState.messages.some(m => m.type === 'user')
|
|
127
|
+
sessionState.messages.some(m => m.type === 'user') || sessionState.hasMessageHistory
|
|
128
128
|
);
|
|
129
129
|
|
|
130
130
|
// Engine lock: once chat starts, the engine is locked for this session.
|
|
@@ -45,10 +45,10 @@ export function useChatActions(params: ChatActionsParams) {
|
|
|
45
45
|
// If in edit mode, restore to parent of edited message first
|
|
46
46
|
if (editModeState.isEditing) {
|
|
47
47
|
try {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
if (sessionState.currentSession?.id) {
|
|
49
|
+
// Restore to parent of edited message (state before the edited message)
|
|
50
|
+
// When parentMessageId is null (editing first message), restore to initial state
|
|
51
|
+
const restoreTargetId = editModeState.parentMessageId || '__initial__';
|
|
52
52
|
await snapshotService.restore(restoreTargetId, sessionState.currentSession.id);
|
|
53
53
|
}
|
|
54
54
|
|
|
@@ -7,33 +7,25 @@ export function useTextareaResize() {
|
|
|
7
7
|
messageText: string
|
|
8
8
|
) {
|
|
9
9
|
if (textareaElement) {
|
|
10
|
-
//
|
|
10
|
+
// Hide overflow during measurement to prevent scrollbar from affecting width
|
|
11
|
+
textareaElement.style.overflowY = 'hidden';
|
|
12
|
+
// Reset height to auto to get accurate scrollHeight
|
|
11
13
|
textareaElement.style.height = 'auto';
|
|
12
14
|
|
|
13
15
|
// If content is empty or only whitespace, keep at minimum height
|
|
14
16
|
if (!messageText || !messageText.trim()) {
|
|
15
|
-
// Force single line height
|
|
16
|
-
textareaElement.style.height = 'auto';
|
|
17
|
-
textareaElement.style.overflowY = 'hidden';
|
|
18
17
|
return;
|
|
19
18
|
}
|
|
20
19
|
|
|
21
|
-
//
|
|
22
|
-
const scrollHeight = textareaElement.scrollHeight
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
const paddingBottom = parseInt(getComputedStyle(textareaElement).paddingBottom) || 0;
|
|
26
|
-
const minHeight = lineHeight + paddingTop + paddingBottom;
|
|
20
|
+
// Measure content height and cap at max
|
|
21
|
+
const scrollHeight = textareaElement.scrollHeight;
|
|
22
|
+
const newHeight = Math.min(scrollHeight, MAX_HEIGHT_PX);
|
|
23
|
+
textareaElement.style.height = newHeight + 'px';
|
|
27
24
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
textareaElement.
|
|
32
|
-
textareaElement.style.overflowY = 'auto';
|
|
33
|
-
} else {
|
|
34
|
-
textareaElement.style.height = newHeight / 16 + 'rem';
|
|
35
|
-
textareaElement.style.overflowY = 'hidden';
|
|
36
|
-
}
|
|
25
|
+
// Check actual overflow AFTER setting height to handle edge cases
|
|
26
|
+
// where collapsed measurement differs from rendered content height
|
|
27
|
+
textareaElement.style.overflowY =
|
|
28
|
+
textareaElement.scrollHeight > textareaElement.clientHeight ? 'auto' : 'hidden';
|
|
37
29
|
}
|
|
38
30
|
}
|
|
39
31
|
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { snapshotService } from '$frontend/lib/services/snapshot/snapshot.service';
|
|
14
14
|
import type { RestoreConflict, ConflictResolution } from '$frontend/lib/services/snapshot/snapshot.service';
|
|
15
15
|
import type { TimelineResponse, GraphNode, GraphEdge, VersionGroup, AnimationState } from './timeline/types';
|
|
16
|
+
import ws from '$frontend/lib/utils/ws';
|
|
16
17
|
|
|
17
18
|
let {
|
|
18
19
|
isOpen = $bindable(false),
|
|
@@ -107,6 +108,19 @@
|
|
|
107
108
|
}
|
|
108
109
|
});
|
|
109
110
|
|
|
111
|
+
// Reload timeline when a snapshot is captured (stats become available after stream ends)
|
|
112
|
+
$effect(() => {
|
|
113
|
+
if (!isOpen) return;
|
|
114
|
+
|
|
115
|
+
const unsub = ws.on('snapshot:captured', (data: { chatSessionId: string }) => {
|
|
116
|
+
if (data.chatSessionId === sessionId && !processingAction && !animationState.isAnimating) {
|
|
117
|
+
loadTimeline();
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
return unsub;
|
|
122
|
+
});
|
|
123
|
+
|
|
110
124
|
// Scroll to bottom on initial load
|
|
111
125
|
$effect(() => {
|
|
112
126
|
if (timelineData && graphNodes.length > 0 && scrollContainer && !hasScrolledToBottom) {
|
|
@@ -445,9 +459,7 @@
|
|
|
445
459
|
type="warning"
|
|
446
460
|
title="Restore Checkpoint"
|
|
447
461
|
message={pendingNode
|
|
448
|
-
? `Are you sure you want to restore to this checkpoint
|
|
449
|
-
"${getTruncatedMessage(pendingNode.checkpoint.messageText)}"
|
|
450
|
-
This will restore your files to this point within this session.`
|
|
462
|
+
? `Are you sure you want to restore to this checkpoint?\n"${getTruncatedMessage(pendingNode.checkpoint.messageText)}"\nThis will restore your files to this point within this session.`
|
|
451
463
|
: ''}
|
|
452
464
|
confirmText="Restore"
|
|
453
465
|
cancelText="Cancel"
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
const pos = $derived(getInterpolatedPosition(node, animationState));
|
|
23
23
|
const nodeClass = $derived(getInterpolatedNodeClass(node));
|
|
24
|
+
const isInitial = $derived(!!node.checkpoint.isInitial);
|
|
24
25
|
</script>
|
|
25
26
|
|
|
26
27
|
<!-- Node group -->
|
|
@@ -33,6 +34,7 @@
|
|
|
33
34
|
aria-label={`${node.type === 'main' ? 'Checkpoint' : 'Version'} - ${node.checkpoint.messageText}`}
|
|
34
35
|
>
|
|
35
36
|
<title>{node.checkpoint.messageText}</title>
|
|
37
|
+
|
|
36
38
|
<!-- Node circle -->
|
|
37
39
|
<circle
|
|
38
40
|
cx={pos.x}
|
|
@@ -84,25 +86,34 @@
|
|
|
84
86
|
height={SIZE.labelHeight - 8}
|
|
85
87
|
>
|
|
86
88
|
<div class="flex flex-col h-full justify-center pointer-events-none">
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
<
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
89
|
+
{#if isInitial}
|
|
90
|
+
<div class="flex items-center gap-2 text-xs text-slate-500 dark:text-slate-400 leading-tight">
|
|
91
|
+
<span>{formatTime(node.checkpoint.timestamp)}</span>
|
|
92
|
+
</div>
|
|
93
|
+
<div class="text-sm text-slate-900 dark:text-slate-100 leading-tight truncate mt-0.5">
|
|
94
|
+
Session Start
|
|
95
|
+
</div>
|
|
96
|
+
{:else}
|
|
97
|
+
<!-- Timestamp and file stats in one line -->
|
|
98
|
+
<div class="flex items-center gap-2 text-xs text-slate-500 dark:text-slate-400 leading-tight">
|
|
99
|
+
<span>{formatTime(node.checkpoint.timestamp)}</span>
|
|
100
|
+
<span class="w-px h-3 bg-slate-300 dark:bg-slate-600"></span>
|
|
101
|
+
<span class="flex items-center gap-0.5">
|
|
102
|
+
<Icon name="lucide:file-text" class="w-2.5 h-2.5" />
|
|
103
|
+
{node.checkpoint.filesChanged ?? 0}
|
|
104
|
+
</span>
|
|
105
|
+
<span class="text-green-600 dark:text-green-400">
|
|
106
|
+
+{node.checkpoint.insertions ?? 0}
|
|
107
|
+
</span>
|
|
108
|
+
<span class="text-red-600 dark:text-red-400">
|
|
109
|
+
-{node.checkpoint.deletions ?? 0}
|
|
110
|
+
</span>
|
|
111
|
+
</div>
|
|
112
|
+
<!-- Message text below timestamp with auto truncation -->
|
|
113
|
+
<div class="text-sm text-slate-900 dark:text-slate-100 leading-tight truncate mt-0.5">
|
|
114
|
+
{node.checkpoint.messageText}
|
|
115
|
+
</div>
|
|
116
|
+
{/if}
|
|
106
117
|
</div>
|
|
107
118
|
</foreignObject>
|
|
108
119
|
</g>
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
* Timeline data structures and type definitions
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
/** Sentinel ID for the "initial state" node (before any chat messages) */
|
|
6
|
+
export const INITIAL_NODE_ID = '__initial__';
|
|
7
|
+
|
|
5
8
|
export interface CheckpointNode {
|
|
6
9
|
id: string;
|
|
7
10
|
messageId: string;
|
|
@@ -13,6 +16,7 @@ export interface CheckpointNode {
|
|
|
13
16
|
isOrphaned: boolean;
|
|
14
17
|
isCurrent: boolean;
|
|
15
18
|
hasSnapshot: boolean;
|
|
19
|
+
isInitial?: boolean; // true for the "initial state" node
|
|
16
20
|
senderName?: string | null;
|
|
17
21
|
// File change statistics (git-like)
|
|
18
22
|
filesChanged?: number;
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
import Dialog from './Dialog.svelte';
|
|
7
7
|
import { debug } from '$shared/utils/logger';
|
|
8
8
|
import ws from '$frontend/lib/utils/ws';
|
|
9
|
-
import {
|
|
9
|
+
import { systemSettings } from '$frontend/lib/stores/features/settings.svelte';
|
|
10
10
|
|
|
11
11
|
interface FileItem {
|
|
12
12
|
name: string;
|
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
let deleteFolderConfirmName = $state('');
|
|
44
44
|
|
|
45
45
|
// Derived: whether directory access is restricted
|
|
46
|
-
const hasRestrictions = $derived(
|
|
46
|
+
const hasRestrictions = $derived(systemSettings.allowedBasePaths && systemSettings.allowedBasePaths.length > 0);
|
|
47
47
|
|
|
48
48
|
// Detect backend OS from current path (drive letter = Windows)
|
|
49
49
|
const isWindows = $derived(/^[A-Za-z]:/.test(currentPath));
|
|
@@ -89,13 +89,13 @@
|
|
|
89
89
|
|
|
90
90
|
// Check if a path is accessible (within allowed base paths)
|
|
91
91
|
function isPathAllowed(path: string): boolean {
|
|
92
|
-
if (!
|
|
93
|
-
return
|
|
92
|
+
if (!systemSettings.allowedBasePaths || systemSettings.allowedBasePaths.length === 0) return true;
|
|
93
|
+
return systemSettings.allowedBasePaths.some(base => isWithinBase(path, base));
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
// Check if current path is at the restriction boundary (cannot go up)
|
|
97
97
|
const atRestrictionBoundary = $derived(
|
|
98
|
-
hasRestrictions &&
|
|
98
|
+
hasRestrictions && systemSettings.allowedBasePaths.some(base => pathsEqual(currentPath, base))
|
|
99
99
|
);
|
|
100
100
|
|
|
101
101
|
// Get available drives/mount points for all platforms
|
|
@@ -128,8 +128,8 @@
|
|
|
128
128
|
// Get user's home directory or current working directory
|
|
129
129
|
async function getInitialPath(): Promise<string> {
|
|
130
130
|
// If restrictions are set, start at the first allowed base path
|
|
131
|
-
if (
|
|
132
|
-
return
|
|
131
|
+
if (systemSettings.allowedBasePaths && systemSettings.allowedBasePaths.length > 0) {
|
|
132
|
+
return systemSettings.allowedBasePaths[0];
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
try {
|
|
@@ -225,7 +225,7 @@
|
|
|
225
225
|
|
|
226
226
|
// Enforce access restrictions
|
|
227
227
|
if (!isPathAllowed(currentPath)) {
|
|
228
|
-
error = `Access restricted. Allowed paths: ${
|
|
228
|
+
error = `Access restricted. Allowed paths: ${systemSettings.allowedBasePaths.join(', ')}`;
|
|
229
229
|
items = [];
|
|
230
230
|
return;
|
|
231
231
|
}
|
|
@@ -546,7 +546,7 @@
|
|
|
546
546
|
<div class="flex items-center gap-2 flex-wrap">
|
|
547
547
|
{#if hasRestrictions}
|
|
548
548
|
<!-- Restricted mode: show allowed base paths as quick access -->
|
|
549
|
-
{#each
|
|
549
|
+
{#each systemSettings.allowedBasePaths as basePath (basePath)}
|
|
550
550
|
<button
|
|
551
551
|
onclick={() => navigateToLocation(basePath)}
|
|
552
552
|
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"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { updateState, runUpdate, dismissUpdate, checkForUpdate } from '$frontend/lib/stores/ui/update.svelte';
|
|
3
|
-
import {
|
|
3
|
+
import { systemSettings, updateSystemSettings } from '$frontend/lib/stores/features/settings.svelte';
|
|
4
4
|
import Icon from '$frontend/lib/components/common/Icon.svelte';
|
|
5
5
|
import { slide } from 'svelte/transition';
|
|
6
6
|
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
function toggleAutoUpdate() {
|
|
25
|
-
|
|
25
|
+
updateSystemSettings({ autoUpdate: !systemSettings.autoUpdate });
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
function handleRetry() {
|
|
@@ -30,11 +30,13 @@
|
|
|
30
30
|
if (!textareaEl) return;
|
|
31
31
|
// Reset to single line to measure content
|
|
32
32
|
textareaEl.style.height = 'auto';
|
|
33
|
-
// Line height is ~20px for text-
|
|
33
|
+
// Line height is ~20px for text-sm, so 5 lines max = 100px
|
|
34
34
|
const lineHeight = 20;
|
|
35
35
|
const maxHeight = lineHeight * 5;
|
|
36
36
|
const scrollHeight = textareaEl.scrollHeight;
|
|
37
|
-
|
|
37
|
+
const newHeight = Math.min(scrollHeight, maxHeight);
|
|
38
|
+
textareaEl.style.height = newHeight + 'px';
|
|
39
|
+
textareaEl.style.overflowY = scrollHeight > maxHeight ? 'auto' : 'hidden';
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
function handleInput() {
|
|
@@ -48,9 +50,9 @@
|
|
|
48
50
|
bind:this={textareaEl}
|
|
49
51
|
bind:value={commitMessage}
|
|
50
52
|
placeholder="Commit message..."
|
|
51
|
-
class="w-full px-2.5 py-2 text-sm bg-white dark:bg-slate-800/80 border border-slate-200 dark:border-slate-700 rounded-md text-slate-900 dark:text-slate-100 placeholder:text-slate-400 dark:placeholder:text-slate-600 resize-none outline-none focus:
|
|
53
|
+
class="w-full px-2.5 py-2 text-sm bg-white dark:bg-slate-800/80 border border-slate-200 dark:border-slate-700 rounded-md text-slate-900 dark:text-slate-100 placeholder:text-slate-400 dark:placeholder:text-slate-600 resize-none outline-none focus:ring-1 focus:ring-violet-500 transition-colors"
|
|
52
54
|
rows="1"
|
|
53
|
-
style="
|
|
55
|
+
style="overflow-y: hidden;"
|
|
54
56
|
onkeydown={handleKeydown}
|
|
55
57
|
oninput={handleInput}
|
|
56
58
|
disabled={isCommitting}
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
try {
|
|
74
|
-
const messages = await ws.http('messages:list', { session_id: sessionId });
|
|
74
|
+
const messages = await ws.http('messages:list', { session_id: sessionId, include_all: true });
|
|
75
75
|
|
|
76
76
|
const firstUserMessage = messages.find((m: SDKMessage) => m.type === 'user');
|
|
77
77
|
let title = 'New Conversation';
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
|
|
80
80
|
try {
|
|
81
81
|
// Get messages from current HEAD checkpoint (active branch only)
|
|
82
|
-
const messages = await ws.http('messages:list', { session_id: sessionId });
|
|
82
|
+
const messages = await ws.http('messages:list', { session_id: sessionId, include_all: true });
|
|
83
83
|
|
|
84
84
|
// Get title from first user message in current HEAD
|
|
85
85
|
const firstUserMessage = messages.find((m: SDKMessage) => m.type === 'user');
|
|
@@ -112,7 +112,7 @@
|
|
|
112
112
|
}
|
|
113
113
|
},
|
|
114
114
|
onMcpCursorHide: () => {
|
|
115
|
-
|
|
115
|
+
mcpVirtualCursor = { ...mcpVirtualCursor, visible: false };
|
|
116
116
|
},
|
|
117
117
|
transformBrowserToDisplayCoordinates: (browserX, browserY) => {
|
|
118
118
|
return transformBrowserToDisplayCoordinates(browserX, browserY);
|
|
@@ -68,6 +68,15 @@ export function createMcpHandler(config: McpHandlerConfig) {
|
|
|
68
68
|
handleTestCompleted(data);
|
|
69
69
|
});
|
|
70
70
|
|
|
71
|
+
// Hide cursor when the entire Claude request finishes or is stopped
|
|
72
|
+
ws.on('chat:complete', () => {
|
|
73
|
+
if (onCursorHide) onCursorHide();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
ws.on('chat:cancelled', () => {
|
|
77
|
+
if (onCursorHide) onCursorHide();
|
|
78
|
+
});
|
|
79
|
+
|
|
71
80
|
// MCP Tab Management - Request/Response handlers
|
|
72
81
|
setupTabManagementListeners();
|
|
73
82
|
|
|
@@ -154,10 +163,9 @@ export function createMcpHandler(config: McpHandlerConfig) {
|
|
|
154
163
|
}
|
|
155
164
|
}
|
|
156
165
|
|
|
157
|
-
function handleTestCompleted(
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
166
|
+
function handleTestCompleted(_data: { sessionId: string; timestamp: number; source: 'mcp' }) {
|
|
167
|
+
// Cursor is hidden via chat:complete / chat:cancelled listeners instead,
|
|
168
|
+
// because test-completed fires per-tool-call, not at end of full request.
|
|
161
169
|
}
|
|
162
170
|
|
|
163
171
|
function handleTabsListRequest(data: { requestId: string }) {
|
|
@@ -9,14 +9,19 @@
|
|
|
9
9
|
settingsSections,
|
|
10
10
|
type SettingsSection
|
|
11
11
|
} from '$frontend/lib/stores/ui/settings-modal.svelte';
|
|
12
|
+
import { authStore } from '$frontend/lib/stores/features/auth.svelte';
|
|
13
|
+
import { systemSettings } from '$frontend/lib/stores/features/settings.svelte';
|
|
12
14
|
|
|
13
15
|
// Import settings components
|
|
14
16
|
import ModelSettings from './model/ModelSettings.svelte';
|
|
15
17
|
import AIEnginesSettings from './engines/AIEnginesSettings.svelte';
|
|
16
18
|
import AppearanceSettings from './appearance/AppearanceSettings.svelte';
|
|
17
|
-
import
|
|
19
|
+
import AccountSettings from './account/AccountSettings.svelte';
|
|
18
20
|
import NotificationSettings from './notifications/NotificationSettings.svelte';
|
|
19
|
-
import
|
|
21
|
+
import TeamSettings from './admin/UserManagement.svelte';
|
|
22
|
+
import InviteManagement from './admin/InviteManagement.svelte';
|
|
23
|
+
import SecuritySettings from './security/SecuritySettings.svelte';
|
|
24
|
+
import SystemSettings from './system/SystemSettings.svelte';
|
|
20
25
|
|
|
21
26
|
// Responsive state
|
|
22
27
|
let isMobileMenuOpen = $state(false);
|
|
@@ -25,6 +30,17 @@
|
|
|
25
30
|
const isMobile = $derived(windowWidth < 768);
|
|
26
31
|
const isOpen = $derived(settingsModalState.isOpen);
|
|
27
32
|
const activeSection = $derived(settingsModalState.activeSection);
|
|
33
|
+
const isAdmin = $derived(authStore.isAdmin);
|
|
34
|
+
const isNoAuth = $derived(systemSettings.authMode === 'none');
|
|
35
|
+
|
|
36
|
+
// Filter sections: hide admin-only tabs for non-admins, hide team in no-auth mode
|
|
37
|
+
const visibleSections = $derived(
|
|
38
|
+
settingsSections.filter(s => {
|
|
39
|
+
if (s.adminOnly && !isAdmin) return false;
|
|
40
|
+
if (s.id === 'team' && isNoAuth) return false;
|
|
41
|
+
return true;
|
|
42
|
+
})
|
|
43
|
+
);
|
|
28
44
|
|
|
29
45
|
// Handle section change
|
|
30
46
|
function handleSectionChange(section: SettingsSection) {
|
|
@@ -48,6 +64,14 @@
|
|
|
48
64
|
}
|
|
49
65
|
}
|
|
50
66
|
|
|
67
|
+
// Auto-redirect when current section becomes hidden
|
|
68
|
+
$effect(() => {
|
|
69
|
+
const isVisible = visibleSections.some(s => s.id === activeSection);
|
|
70
|
+
if (!isVisible && visibleSections.length > 0) {
|
|
71
|
+
setActiveSection(visibleSections[0].id);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
51
75
|
// Get current section info
|
|
52
76
|
const currentSectionInfo = $derived(
|
|
53
77
|
settingsSections.find((s) => s.id === activeSection) || settingsSections[0]
|
|
@@ -122,9 +146,9 @@
|
|
|
122
146
|
<div class="flex flex-1 min-h-0 relative">
|
|
123
147
|
<!-- Sidebar -->
|
|
124
148
|
<aside
|
|
125
|
-
class="flex flex-col w-
|
|
149
|
+
class="flex flex-col w-65 shrink-0 bg-white dark:bg-slate-900/98 border-r border-slate-200 dark:border-slate-800
|
|
126
150
|
{isMobile
|
|
127
|
-
? 'absolute left-0 top-0 bottom-0 z-10 w-70 shadow-[4px_0_20px_rgba(0,0,0,0.15)] dark:shadow-[4px_0_20px_rgba(0,0,0,0.3)] transition-transform duration-
|
|
151
|
+
? 'absolute left-0 top-0 bottom-0 z-10 w-70 shadow-[4px_0_20px_rgba(0,0,0,0.15)] dark:shadow-[4px_0_20px_rgba(0,0,0,0.3)] transition-transform duration-250 ease-out'
|
|
128
152
|
: ''}
|
|
129
153
|
{isMobile && !isMobileMenuOpen ? '-translate-x-full' : 'translate-x-0'}"
|
|
130
154
|
>
|
|
@@ -149,7 +173,7 @@
|
|
|
149
173
|
{/if}
|
|
150
174
|
|
|
151
175
|
<nav class="flex-1 overflow-y-auto p-3">
|
|
152
|
-
{#each
|
|
176
|
+
{#each visibleSections as section (section.id)}
|
|
153
177
|
<button
|
|
154
178
|
type="button"
|
|
155
179
|
class="flex items-start gap-3 w-full py-3 px-3.5 bg-transparent border-none rounded-lg text-slate-500 text-sm text-left cursor-pointer transition-all duration-150 mb-1
|
|
@@ -199,25 +223,36 @@
|
|
|
199
223
|
<div in:fly={{ x: 20, duration: 200 }}>
|
|
200
224
|
<ModelSettings />
|
|
201
225
|
</div>
|
|
202
|
-
{:else if activeSection === 'engines'}
|
|
203
|
-
<div in:fly={{ x: 20, duration: 200 }}>
|
|
204
|
-
<AIEnginesSettings />
|
|
205
|
-
</div>
|
|
206
226
|
{:else if activeSection === 'appearance'}
|
|
207
227
|
<div in:fly={{ x: 20, duration: 200 }}>
|
|
208
228
|
<AppearanceSettings />
|
|
209
229
|
</div>
|
|
210
|
-
{:else if activeSection === 'user'}
|
|
211
|
-
<div in:fly={{ x: 20, duration: 200 }}>
|
|
212
|
-
<UserSettings />
|
|
213
|
-
</div>
|
|
214
230
|
{:else if activeSection === 'notifications'}
|
|
215
231
|
<div in:fly={{ x: 20, duration: 200 }}>
|
|
216
232
|
<NotificationSettings />
|
|
217
233
|
</div>
|
|
218
|
-
{:else if activeSection === '
|
|
234
|
+
{:else if activeSection === 'account'}
|
|
235
|
+
<div in:fly={{ x: 20, duration: 200 }}>
|
|
236
|
+
<AccountSettings />
|
|
237
|
+
</div>
|
|
238
|
+
{:else if activeSection === 'engines' && isAdmin}
|
|
239
|
+
<div in:fly={{ x: 20, duration: 200 }}>
|
|
240
|
+
<AIEnginesSettings />
|
|
241
|
+
</div>
|
|
242
|
+
{:else if activeSection === 'team' && isAdmin && !isNoAuth}
|
|
243
|
+
<div in:fly={{ x: 20, duration: 200 }}>
|
|
244
|
+
<TeamSettings />
|
|
245
|
+
<div class="mt-6">
|
|
246
|
+
<InviteManagement />
|
|
247
|
+
</div>
|
|
248
|
+
</div>
|
|
249
|
+
{:else if activeSection === 'security' && isAdmin}
|
|
250
|
+
<div in:fly={{ x: 20, duration: 200 }}>
|
|
251
|
+
<SecuritySettings />
|
|
252
|
+
</div>
|
|
253
|
+
{:else if activeSection === 'system' && isAdmin}
|
|
219
254
|
<div in:fly={{ x: 20, duration: 200 }}>
|
|
220
|
-
<
|
|
255
|
+
<SystemSettings />
|
|
221
256
|
</div>
|
|
222
257
|
{/if}
|
|
223
258
|
</div>
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { authStore } from '$frontend/lib/stores/features/auth.svelte';
|
|
2
3
|
import PageTemplate from '../common/PageTemplate.svelte';
|
|
3
4
|
|
|
4
5
|
// Import modular components
|
|
5
6
|
import ModelSettings from './model/ModelSettings.svelte';
|
|
6
7
|
import AppearanceSettings from './appearance/AppearanceSettings.svelte';
|
|
7
|
-
import
|
|
8
|
+
import AccountSettings from './account/AccountSettings.svelte';
|
|
8
9
|
import NotificationSettings from './notifications/NotificationSettings.svelte';
|
|
9
|
-
import
|
|
10
|
+
import SecuritySettings from './security/SecuritySettings.svelte';
|
|
11
|
+
import SystemSettings from './system/SystemSettings.svelte';
|
|
12
|
+
import UserManagement from './admin/UserManagement.svelte';
|
|
13
|
+
import InviteManagement from './admin/InviteManagement.svelte';
|
|
14
|
+
|
|
15
|
+
const isAdmin = $derived(authStore.isAdmin);
|
|
16
|
+
const isNoAuth = $derived(authStore.isNoAuth);
|
|
10
17
|
</script>
|
|
11
18
|
|
|
12
19
|
<PageTemplate
|
|
@@ -22,14 +29,21 @@
|
|
|
22
29
|
<!-- Appearance Configuration -->
|
|
23
30
|
<AppearanceSettings />
|
|
24
31
|
|
|
25
|
-
<!-- User Settings -->
|
|
26
|
-
<UserSettings />
|
|
27
|
-
|
|
28
32
|
<!-- Notification Settings -->
|
|
29
33
|
<NotificationSettings />
|
|
30
34
|
|
|
31
|
-
<!--
|
|
32
|
-
|
|
35
|
+
<!-- Account (hidden in no-auth mode) -->
|
|
36
|
+
{#if !isNoAuth}
|
|
37
|
+
<AccountSettings />
|
|
38
|
+
{/if}
|
|
39
|
+
|
|
40
|
+
<!-- Admin-only sections -->
|
|
41
|
+
{#if isAdmin}
|
|
42
|
+
<UserManagement />
|
|
43
|
+
<InviteManagement />
|
|
44
|
+
<SecuritySettings />
|
|
45
|
+
<SystemSettings />
|
|
46
|
+
{/if}
|
|
33
47
|
|
|
34
48
|
</div>
|
|
35
49
|
</div>
|