@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.
Files changed (94) hide show
  1. package/README.md +23 -1
  2. package/backend/index.ts +25 -1
  3. package/backend/lib/auth/auth-service.ts +484 -0
  4. package/backend/lib/auth/index.ts +4 -0
  5. package/backend/lib/auth/permissions.ts +63 -0
  6. package/backend/lib/auth/rate-limiter.ts +145 -0
  7. package/backend/lib/auth/tokens.ts +53 -0
  8. package/backend/lib/chat/stream-manager.ts +4 -1
  9. package/backend/lib/database/migrations/024_create_users_table.ts +29 -0
  10. package/backend/lib/database/migrations/025_create_auth_sessions_table.ts +38 -0
  11. package/backend/lib/database/migrations/026_create_invite_tokens_table.ts +31 -0
  12. package/backend/lib/database/migrations/index.ts +21 -0
  13. package/backend/lib/database/queries/auth-queries.ts +201 -0
  14. package/backend/lib/database/queries/index.ts +2 -1
  15. package/backend/lib/database/queries/session-queries.ts +13 -0
  16. package/backend/lib/database/queries/snapshot-queries.ts +1 -1
  17. package/backend/lib/engine/adapters/opencode/server.ts +9 -1
  18. package/backend/lib/engine/adapters/opencode/stream.ts +175 -1
  19. package/backend/lib/mcp/config.ts +13 -18
  20. package/backend/lib/mcp/index.ts +9 -0
  21. package/backend/lib/mcp/remote-server.ts +132 -0
  22. package/backend/lib/mcp/servers/helper.ts +49 -3
  23. package/backend/lib/mcp/servers/index.ts +3 -2
  24. package/backend/lib/preview/browser/browser-audio-capture.ts +20 -3
  25. package/backend/lib/preview/browser/browser-navigation-tracker.ts +3 -0
  26. package/backend/lib/preview/browser/browser-pool.ts +73 -176
  27. package/backend/lib/preview/browser/browser-preview-service.ts +3 -2
  28. package/backend/lib/preview/browser/browser-tab-manager.ts +261 -23
  29. package/backend/lib/preview/browser/browser-video-capture.ts +36 -1
  30. package/backend/lib/snapshot/helpers.ts +22 -49
  31. package/backend/lib/snapshot/snapshot-service.ts +148 -83
  32. package/backend/lib/utils/ws.ts +65 -1
  33. package/backend/ws/auth/index.ts +17 -0
  34. package/backend/ws/auth/invites.ts +84 -0
  35. package/backend/ws/auth/login.ts +269 -0
  36. package/backend/ws/auth/status.ts +41 -0
  37. package/backend/ws/auth/users.ts +32 -0
  38. package/backend/ws/chat/stream.ts +13 -0
  39. package/backend/ws/engine/claude/accounts.ts +3 -1
  40. package/backend/ws/engine/utils.ts +38 -6
  41. package/backend/ws/index.ts +4 -4
  42. package/backend/ws/preview/browser/interact.ts +27 -5
  43. package/backend/ws/snapshot/restore.ts +111 -12
  44. package/backend/ws/snapshot/timeline.ts +56 -29
  45. package/bin/clopen.ts +56 -1
  46. package/bun.lock +113 -51
  47. package/frontend/App.svelte +47 -29
  48. package/frontend/lib/components/auth/InvitePage.svelte +215 -0
  49. package/frontend/lib/components/auth/LoginPage.svelte +129 -0
  50. package/frontend/lib/components/auth/SetupPage.svelte +1022 -0
  51. package/frontend/lib/components/chat/input/ChatInput.svelte +1 -2
  52. package/frontend/lib/components/chat/input/components/EngineModelPicker.svelte +2 -2
  53. package/frontend/lib/components/chat/input/composables/use-chat-actions.svelte.ts +4 -4
  54. package/frontend/lib/components/chat/input/composables/use-textarea-resize.svelte.ts +11 -19
  55. package/frontend/lib/components/checkpoint/TimelineModal.svelte +15 -3
  56. package/frontend/lib/components/checkpoint/timeline/TimelineNode.svelte +30 -19
  57. package/frontend/lib/components/checkpoint/timeline/types.ts +4 -0
  58. package/frontend/lib/components/common/FolderBrowser.svelte +9 -9
  59. package/frontend/lib/components/common/UpdateBanner.svelte +2 -2
  60. package/frontend/lib/components/git/CommitForm.svelte +6 -4
  61. package/frontend/lib/components/history/HistoryModal.svelte +1 -1
  62. package/frontend/lib/components/history/HistoryView.svelte +1 -1
  63. package/frontend/lib/components/preview/browser/BrowserPreview.svelte +1 -1
  64. package/frontend/lib/components/preview/browser/core/mcp-handlers.svelte.ts +12 -4
  65. package/frontend/lib/components/settings/SettingsModal.svelte +50 -15
  66. package/frontend/lib/components/settings/SettingsView.svelte +21 -7
  67. package/frontend/lib/components/settings/account/AccountSettings.svelte +5 -0
  68. package/frontend/lib/components/settings/admin/InviteManagement.svelte +239 -0
  69. package/frontend/lib/components/settings/admin/UserManagement.svelte +127 -0
  70. package/frontend/lib/components/settings/general/AdvancedSettings.svelte +10 -4
  71. package/frontend/lib/components/settings/general/AuthModeSettings.svelte +229 -0
  72. package/frontend/lib/components/settings/general/GeneralSettings.svelte +6 -1
  73. package/frontend/lib/components/settings/general/UpdateSettings.svelte +5 -5
  74. package/frontend/lib/components/settings/security/SecuritySettings.svelte +10 -0
  75. package/frontend/lib/components/settings/system/SystemSettings.svelte +10 -0
  76. package/frontend/lib/components/settings/user/UserSettings.svelte +147 -74
  77. package/frontend/lib/components/workspace/PanelHeader.svelte +1 -1
  78. package/frontend/lib/components/workspace/WorkspaceLayout.svelte +5 -10
  79. package/frontend/lib/components/workspace/panels/ChatPanel.svelte +3 -2
  80. package/frontend/lib/services/preview/browser/browser-webcodecs.service.ts +31 -8
  81. package/frontend/lib/stores/core/sessions.svelte.ts +15 -1
  82. package/frontend/lib/stores/features/auth.svelte.ts +296 -0
  83. package/frontend/lib/stores/features/settings.svelte.ts +53 -9
  84. package/frontend/lib/stores/features/user.svelte.ts +26 -68
  85. package/frontend/lib/stores/ui/settings-modal.svelte.ts +42 -21
  86. package/frontend/lib/stores/ui/update.svelte.ts +2 -14
  87. package/frontend/lib/stores/ui/workspace.svelte.ts +4 -4
  88. package/package.json +8 -6
  89. package/shared/types/stores/settings.ts +16 -2
  90. package/shared/utils/logger.ts +1 -0
  91. package/shared/utils/ws-client.ts +30 -13
  92. package/shared/utils/ws-server.ts +42 -4
  93. package/backend/lib/mcp/stdio-server.ts +0 -103
  94. 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-2 focus-within:ring-violet-500 {fileHandling.isDragging && 'ring-2 ring-violet-500'}"
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
- // Restore to parent of edited message (if exists)
49
- const restoreTargetId = editModeState.parentMessageId || editModeState.messageId;
50
-
51
- if (restoreTargetId && sessionState.currentSession?.id) {
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
- // Reset height to auto first to get accurate scrollHeight
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
- // Calculate required height based on content
22
- const scrollHeight = textareaElement.scrollHeight + 7;
23
- const lineHeight = parseInt(getComputedStyle(textareaElement).lineHeight) || 24;
24
- const paddingTop = parseInt(getComputedStyle(textareaElement).paddingTop) || 0;
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
- const newHeight = Math.max(minHeight, scrollHeight);
29
-
30
- if (newHeight >= MAX_HEIGHT_PX) {
31
- textareaElement.style.height = '22.5rem';
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
- <!-- Timestamp and file stats in one line -->
88
- <div class="flex items-center gap-2 text-xs text-slate-500 dark:text-slate-400 leading-tight">
89
- <span>{formatTime(node.checkpoint.timestamp)}</span>
90
- <span class="w-px h-3 bg-slate-300 dark:bg-slate-600"></span>
91
- <span class="flex items-center gap-0.5">
92
- <Icon name="lucide:file-text" class="w-2.5 h-2.5" />
93
- {node.checkpoint.filesChanged ?? 0}
94
- </span>
95
- <span class="text-green-600 dark:text-green-400">
96
- +{node.checkpoint.insertions ?? 0}
97
- </span>
98
- <span class="text-red-600 dark:text-red-400">
99
- -{node.checkpoint.deletions ?? 0}
100
- </span>
101
- </div>
102
- <!-- Message text below timestamp with auto truncation -->
103
- <div class="text-sm text-slate-900 dark:text-slate-100 leading-tight truncate mt-0.5">
104
- {node.checkpoint.messageText}
105
- </div>
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 { settings } from '$frontend/lib/stores/features/settings.svelte';
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(settings.allowedBasePaths && settings.allowedBasePaths.length > 0);
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 (!settings.allowedBasePaths || settings.allowedBasePaths.length === 0) return true;
93
- return settings.allowedBasePaths.some(base => isWithinBase(path, base));
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 && settings.allowedBasePaths.some(base => pathsEqual(currentPath, base))
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 (settings.allowedBasePaths && settings.allowedBasePaths.length > 0) {
132
- return settings.allowedBasePaths[0];
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: ${settings.allowedBasePaths.join(', ')}`;
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 settings.allowedBasePaths as basePath (basePath)}
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 { settings, updateSettings } from '$frontend/lib/stores/features/settings.svelte';
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
- updateSettings({ autoUpdate: !settings.autoUpdate });
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-xs, so 5 lines max = 100px
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
- textareaEl.style.height = Math.min(scrollHeight, maxHeight) + 'px';
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:border-violet-500/40 focus:ring-1 focus:ring-violet-500/20 transition-colors overflow-hidden"
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="height: 27px"
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
- // Cursor stays visible between automated steps until user interacts manually
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(data: { sessionId: string; timestamp: number; source: 'mcp' }) {
158
- if (mcpControlState.browserSessionId === data.sessionId && onCursorHide) {
159
- onCursorHide();
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 UserSettings from './user/UserSettings.svelte';
19
+ import AccountSettings from './account/AccountSettings.svelte';
18
20
  import NotificationSettings from './notifications/NotificationSettings.svelte';
19
- import GeneralSettings from './general/GeneralSettings.svelte';
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-68 shrink-0 bg-white dark:bg-slate-900/98 border-r border-slate-200 dark:border-slate-800
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-[250ms] ease-out'
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 settingsSections as section (section.id)}
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 === 'general'}
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
- <GeneralSettings />
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 UserSettings from './user/UserSettings.svelte';
8
+ import AccountSettings from './account/AccountSettings.svelte';
8
9
  import NotificationSettings from './notifications/NotificationSettings.svelte';
9
- import GeneralSettings from './general/GeneralSettings.svelte';
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
- <!-- General Settings -->
32
- <GeneralSettings />
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>
@@ -0,0 +1,5 @@
1
+ <script lang="ts">
2
+ import UserSettings from '../user/UserSettings.svelte';
3
+ </script>
4
+
5
+ <UserSettings />