@nocturnium/svelte-ide 1.0.2 → 1.0.3

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 (74) hide show
  1. package/README.md +5 -3
  2. package/dist/components/ai/AIMessageContent.svelte +24 -14
  3. package/dist/components/ai/AIPanel.svelte +22 -0
  4. package/dist/components/editor/CollaborativeEditor.svelte +68 -5
  5. package/dist/components/editor/CollaborativeEditor.svelte.d.ts +14 -0
  6. package/dist/components/editor/CustomEditor.svelte +52 -33
  7. package/dist/components/editor/CustomEditor.svelte.d.ts +2 -2
  8. package/dist/components/editor/Editor.svelte +17 -0
  9. package/dist/components/editor/Editor.svelte.d.ts +9 -0
  10. package/dist/components/editor/EditorPane.svelte +18 -1
  11. package/dist/components/editor/EditorPane.svelte.d.ts +5 -0
  12. package/dist/components/editor/EditorSelections.svelte +27 -11
  13. package/dist/components/editor/EditorSelections.svelte.d.ts +1 -0
  14. package/dist/components/editor/core/folding.d.ts +11 -0
  15. package/dist/components/editor/core/folding.js +41 -0
  16. package/dist/components/editor/core/index.d.ts +0 -5
  17. package/dist/components/editor/core/index.js +4 -5
  18. package/dist/components/editor/core/state.d.ts +5 -0
  19. package/dist/components/editor/core/state.js +131 -12
  20. package/dist/components/editor/editor-find.d.ts +1 -0
  21. package/dist/components/editor/editor-find.js +6 -5
  22. package/dist/components/editor/editor-input.d.ts +1 -0
  23. package/dist/components/editor/editor-input.js +4 -1
  24. package/dist/components/editor/editor-scroll.d.ts +1 -0
  25. package/dist/components/editor/editor-scroll.js +2 -1
  26. package/dist/components/editor/index.d.ts +19 -3
  27. package/dist/components/editor/index.js +18 -4
  28. package/dist/components/editor/tokenizer/base.d.ts +1 -25
  29. package/dist/components/editor/tokenizer/base.js +0 -172
  30. package/dist/components/editor/tokenizer/index.d.ts +4 -0
  31. package/dist/components/editor/tokenizer/index.js +1 -1
  32. package/dist/components/editor/tokenizer/languages/html.d.ts +3 -2
  33. package/dist/components/editor/tokenizer/languages/html.js +64 -6
  34. package/dist/components/editor/tokenizer/languages/javascript.d.ts +0 -3
  35. package/dist/components/editor/tokenizer/languages/javascript.js +1 -2
  36. package/dist/components/editor/tokenizer/languages/svelte.d.ts +1 -1
  37. package/dist/components/editor/tokenizer/languages/svelte.js +6 -1
  38. package/dist/components/editor/tokenizer/types.d.ts +0 -28
  39. package/dist/crdt/awareness.d.ts +8 -2
  40. package/dist/crdt/awareness.js +11 -4
  41. package/dist/crdt/document.d.ts +10 -1
  42. package/dist/crdt/document.js +15 -7
  43. package/dist/crdt/index.d.ts +8 -2
  44. package/dist/crdt/index.js +5 -2
  45. package/dist/crdt/undo.d.ts +2 -7
  46. package/dist/crdt/undo.js +1 -8
  47. package/dist/index.d.ts +7 -9
  48. package/dist/index.js +7 -9
  49. package/dist/services/error-handling.d.ts +2 -11
  50. package/dist/services/error-handling.js +15 -4
  51. package/dist/services/lsp-client.d.ts +3 -0
  52. package/dist/services/lsp-client.js +55 -10
  53. package/dist/services/optimistic.d.ts +8 -5
  54. package/dist/services/optimistic.js +36 -10
  55. package/dist/services/vfs-client.js +11 -3
  56. package/dist/stores/agents.svelte.js +3 -2
  57. package/dist/stores/ai-persistence.svelte.js +7 -2
  58. package/dist/stores/ai.svelte.js +2 -1
  59. package/dist/stores/collaboration.svelte.d.ts +1 -1
  60. package/dist/stores/collaboration.svelte.js +3 -2
  61. package/dist/stores/editor.svelte.js +29 -5
  62. package/dist/stores/layout.svelte.js +3 -0
  63. package/dist/stores/plugin.svelte.js +9 -3
  64. package/dist/stores/vfs.svelte.js +26 -9
  65. package/dist/styles/theme.css +43 -0
  66. package/dist/types/vfs.d.ts +15 -1
  67. package/dist/types/vfs.js +9 -0
  68. package/dist/utils/language.d.ts +4 -3
  69. package/dist/utils/language.js +8 -18
  70. package/package.json +1 -1
  71. package/dist/components/editor/MinimalEditor.svelte +0 -75
  72. package/dist/components/editor/MinimalEditor.svelte.d.ts +0 -6
  73. package/dist/components/editor/MinimalEditor2.svelte +0 -84
  74. package/dist/components/editor/MinimalEditor2.svelte.d.ts +0 -6
package/dist/index.d.ts CHANGED
@@ -8,7 +8,7 @@
8
8
  * - All UI components built from scratch (no UI libraries)
9
9
  * - Native Svelte 5 runes for state management
10
10
  * - Custom code editor (no CodeMirror dependency)
11
- * - Yjs for CRDT collaboration (optional, tree-shakeable)
11
+ * - Yjs for CRDT collaboration (optional via the ./crdt entry)
12
12
  *
13
13
  * ============================================================================
14
14
  * PUBLIC API SURFACE — what belongs in this root barrel
@@ -111,11 +111,6 @@ export { default as Editor } from './components/editor/Editor.svelte';
111
111
  * @public - Stable API
112
112
  */
113
113
  export { default as CustomEditor } from './components/editor/CustomEditor.svelte';
114
- /**
115
- * Collaborative editor with CRDT support
116
- * @experimental - requires the optional yjs peers; API may change in future versions
117
- */
118
- export { default as CollaborativeEditor } from './components/editor/CollaborativeEditor.svelte';
119
114
  /**
120
115
  * Editor tab bar component
121
116
  * @public - Stable API
@@ -142,14 +137,17 @@ export { default as FileExplorer, type FileChangeStatus, type GitFileStatus } fr
142
137
  *
143
138
  * Functions and types marked below:
144
139
  * createEditorState, createNavigation, createKeyboardHandler,
145
- * createDefaultKeybindings, createCRDTBinding,
140
+ * createDefaultKeybindings,
146
141
  * EditorState, Position, Selection, Line, ChangeEvent,
147
- * Navigation, KeyboardHandler, Keybinding, CRDTBinding,
142
+ * Navigation, KeyboardHandler, Keybinding,
148
143
  * Cursor, CursorManager, CursorManagerConfig
149
144
  *
150
145
  * @public - Stable API
151
146
  */
152
- export { createEditorState, createNavigation, createKeyboardHandler, createDefaultKeybindings, createCRDTBinding, type EditorState, type Position, type Selection, type Line, type ChangeEvent, type Navigation, type KeyboardHandler, type Keybinding, type CRDTBinding, type Cursor, type CursorManager, type CursorManagerConfig } from './components/editor/core';
147
+ export { createEditorState, type EditorState, type Position, type Selection, type Line, type ChangeEvent } from './components/editor/core/state';
148
+ export { createNavigation, type Navigation } from './components/editor/core/navigation';
149
+ export { createKeyboardHandler, createDefaultKeybindings, type KeyboardHandler, type Keybinding } from './components/editor/core/keybindings';
150
+ export { type Cursor, type CursorManager, type CursorManagerConfig } from './components/editor/core/multi-cursor';
153
151
  /**
154
152
  * Editor theme utilities and types
155
153
  * @public - Stable API
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@
8
8
  * - All UI components built from scratch (no UI libraries)
9
9
  * - Native Svelte 5 runes for state management
10
10
  * - Custom code editor (no CodeMirror dependency)
11
- * - Yjs for CRDT collaboration (optional, tree-shakeable)
11
+ * - Yjs for CRDT collaboration (optional via the ./crdt entry)
12
12
  *
13
13
  * ============================================================================
14
14
  * PUBLIC API SURFACE — what belongs in this root barrel
@@ -117,11 +117,6 @@ export { default as Editor } from './components/editor/Editor.svelte';
117
117
  * @public - Stable API
118
118
  */
119
119
  export { default as CustomEditor } from './components/editor/CustomEditor.svelte';
120
- /**
121
- * Collaborative editor with CRDT support
122
- * @experimental - requires the optional yjs peers; API may change in future versions
123
- */
124
- export { default as CollaborativeEditor } from './components/editor/CollaborativeEditor.svelte';
125
120
  /**
126
121
  * Editor tab bar component
127
122
  * @public - Stable API
@@ -148,14 +143,17 @@ export { default as FileExplorer } from './components/editor/FileExplorer.svelte
148
143
  *
149
144
  * Functions and types marked below:
150
145
  * createEditorState, createNavigation, createKeyboardHandler,
151
- * createDefaultKeybindings, createCRDTBinding,
146
+ * createDefaultKeybindings,
152
147
  * EditorState, Position, Selection, Line, ChangeEvent,
153
- * Navigation, KeyboardHandler, Keybinding, CRDTBinding,
148
+ * Navigation, KeyboardHandler, Keybinding,
154
149
  * Cursor, CursorManager, CursorManagerConfig
155
150
  *
156
151
  * @public - Stable API
157
152
  */
158
- export { createEditorState, createNavigation, createKeyboardHandler, createDefaultKeybindings, createCRDTBinding } from './components/editor/core';
153
+ export { createEditorState } from './components/editor/core/state';
154
+ export { createNavigation } from './components/editor/core/navigation';
155
+ export { createKeyboardHandler, createDefaultKeybindings } from './components/editor/core/keybindings';
156
+ export {} from './components/editor/core/multi-cursor';
159
157
  /**
160
158
  * Editor theme utilities and types
161
159
  * @public - Stable API
@@ -3,17 +3,8 @@
3
3
  *
4
4
  * Provides structured error types, recovery strategies, and user-friendly error messages.
5
5
  */
6
- export type VFSErrorCode = 'NETWORK_ERROR' | 'CONNECTION_LOST' | 'TIMEOUT' | 'FILE_LOCKED' | 'LOCK_EXPIRED' | 'LOCK_CONFLICT' | 'VERSION_CONFLICT' | 'FILE_NOT_FOUND' | 'PERMISSION_DENIED' | 'WORKSPACE_NOT_FOUND' | 'INVALID_OPERATION' | 'SERVER_ERROR' | 'RATE_LIMITED' | 'UNKNOWN';
7
- export interface VFSError extends Error {
8
- code: VFSErrorCode;
9
- statusCode?: number;
10
- path?: string;
11
- workspaceId?: string;
12
- retryable: boolean;
13
- userMessage: string;
14
- technicalDetails?: string;
15
- recoveryOptions: RecoveryOption[];
16
- }
6
+ import { VFSError, type VFSErrorCode } from '../types/vfs';
7
+ export { VFSError, type VFSErrorCode };
17
8
  export interface RecoveryOption {
18
9
  id: string;
19
10
  label: string;
@@ -3,6 +3,11 @@
3
3
  *
4
4
  * Provides structured error types, recovery strategies, and user-friendly error messages.
5
5
  */
6
+ import { VFSError } from '../types/vfs';
7
+ // ============================================================================
8
+ // Error Types
9
+ // ============================================================================
10
+ export { VFSError };
6
11
  // ============================================================================
7
12
  // Error Factory
8
13
  // ============================================================================
@@ -10,9 +15,7 @@
10
15
  * Create a structured VFS error
11
16
  */
12
17
  export function createVFSError(code, message, options = {}) {
13
- const error = new Error(message);
14
- error.name = 'VFSError';
15
- error.code = code;
18
+ const error = new VFSError(message, code, options.cause);
16
19
  error.statusCode = options.statusCode;
17
20
  error.path = options.path;
18
21
  error.workspaceId = options.workspaceId;
@@ -29,6 +32,14 @@ export function createVFSError(code, message, options = {}) {
29
32
  export function parseError(error, context) {
30
33
  // Already a VFSError
31
34
  if (isVFSError(error)) {
35
+ error.path ??= context?.path;
36
+ error.workspaceId ??= context?.workspaceId;
37
+ error.retryable = isRetryableError(error.code);
38
+ error.userMessage = getUserMessage(error.code, error.path);
39
+ error.technicalDetails ??= error.message;
40
+ if (error.recoveryOptions.length === 0) {
41
+ error.recoveryOptions = getRecoveryOptions(error.code);
42
+ }
32
43
  return error;
33
44
  }
34
45
  // Network/fetch errors
@@ -97,7 +108,7 @@ function parseStatusCode(status, message, context) {
97
108
  * Check if error is a VFSError
98
109
  */
99
110
  export function isVFSError(error) {
100
- return error instanceof Error && 'code' in error && 'retryable' in error;
111
+ return error instanceof VFSError;
101
112
  }
102
113
  /**
103
114
  * Check if error code is retryable
@@ -13,6 +13,7 @@ export declare class LSPClient {
13
13
  private _capabilities;
14
14
  private reconnectAttempts;
15
15
  private reconnectTimeout;
16
+ private intentionalDisconnect;
16
17
  private openDocuments;
17
18
  private diagnosticsCache;
18
19
  private eventHandlers;
@@ -28,6 +29,8 @@ export declare class LSPClient {
28
29
  private emitEvent;
29
30
  private setState;
30
31
  connect(): Promise<void>;
32
+ private cleanupSocket;
33
+ private getReconnectDelay;
31
34
  private handleDisconnect;
32
35
  disconnect(): Promise<void>;
33
36
  private initialize;
@@ -109,6 +109,7 @@ export class LSPClient {
109
109
  _capabilities = null;
110
110
  reconnectAttempts = 0;
111
111
  reconnectTimeout = null;
112
+ intentionalDisconnect = false;
112
113
  // Document tracking
113
114
  openDocuments = new Map();
114
115
  diagnosticsCache = new Map();
@@ -177,11 +178,24 @@ export class LSPClient {
177
178
  if (this._state !== 'disconnected' && this._state !== 'error') {
178
179
  return;
179
180
  }
181
+ this.intentionalDisconnect = false;
182
+ if (this.reconnectTimeout) {
183
+ clearTimeout(this.reconnectTimeout);
184
+ this.reconnectTimeout = null;
185
+ }
186
+ this.cleanupSocket();
180
187
  this.setState('connecting');
181
188
  return new Promise((resolve, reject) => {
182
189
  try {
183
- this.ws = new WebSocket(this.config.serverUrl);
184
- this.ws.onopen = async () => {
190
+ const socket = new WebSocket(this.config.serverUrl);
191
+ this.ws = socket;
192
+ socket.onopen = async () => {
193
+ if (socket !== this.ws)
194
+ return;
195
+ if (this.reconnectTimeout) {
196
+ clearTimeout(this.reconnectTimeout);
197
+ this.reconnectTimeout = null;
198
+ }
185
199
  this.reconnectAttempts = 0;
186
200
  this.setState('connected');
187
201
  try {
@@ -192,17 +206,21 @@ export class LSPClient {
192
206
  reject(err);
193
207
  }
194
208
  };
195
- this.ws.onclose = () => {
196
- this.handleDisconnect();
209
+ socket.onclose = () => {
210
+ this.handleDisconnect(socket);
197
211
  };
198
- this.ws.onerror = (_event) => {
212
+ socket.onerror = (_event) => {
213
+ if (socket !== this.ws)
214
+ return;
199
215
  const error = new Error('WebSocket error');
200
216
  this.emitEvent('onError', error);
201
217
  if (this._state === 'connecting') {
202
218
  reject(error);
203
219
  }
204
220
  };
205
- this.ws.onmessage = (event) => {
221
+ socket.onmessage = (event) => {
222
+ if (socket !== this.ws)
223
+ return;
206
224
  this.handleMessage(event.data);
207
225
  };
208
226
  }
@@ -212,7 +230,29 @@ export class LSPClient {
212
230
  }
213
231
  });
214
232
  }
215
- handleDisconnect() {
233
+ cleanupSocket(socket = this.ws) {
234
+ if (!socket)
235
+ return;
236
+ socket.onopen = null;
237
+ socket.onclose = null;
238
+ socket.onerror = null;
239
+ socket.onmessage = null;
240
+ if (socket.readyState !== WebSocket.CLOSING) {
241
+ socket.close();
242
+ }
243
+ if (socket === this.ws) {
244
+ this.ws = null;
245
+ }
246
+ }
247
+ getReconnectDelay(attempt) {
248
+ const baseDelay = this.config.reconnectDelay ?? 1000;
249
+ const exponentialDelay = baseDelay * 2 ** (attempt - 1);
250
+ const jitter = 0.75 + Math.random() * 0.5;
251
+ return Math.round(exponentialDelay * jitter);
252
+ }
253
+ handleDisconnect(socket = this.ws) {
254
+ const wasIntentional = this.intentionalDisconnect;
255
+ this.cleanupSocket(socket);
216
256
  this.setState('disconnected');
217
257
  // Cancel pending requests
218
258
  for (const [_id, pending] of this.pendingRequests) {
@@ -220,11 +260,15 @@ export class LSPClient {
220
260
  pending.reject(new Error('Connection closed'));
221
261
  }
222
262
  this.pendingRequests.clear();
263
+ if (wasIntentional) {
264
+ this.intentionalDisconnect = false;
265
+ return;
266
+ }
223
267
  // Auto-reconnect
224
268
  if (this.config.autoReconnect &&
225
269
  this.reconnectAttempts < (this.config.maxReconnectAttempts ?? 5)) {
226
270
  this.reconnectAttempts++;
227
- const delay = (this.config.reconnectDelay ?? 1000) * this.reconnectAttempts;
271
+ const delay = this.getReconnectDelay(this.reconnectAttempts);
228
272
  if (this.config.debug) {
229
273
  console.log(`[LSP] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
230
274
  }
@@ -236,6 +280,7 @@ export class LSPClient {
236
280
  }
237
281
  }
238
282
  async disconnect() {
283
+ this.intentionalDisconnect = true;
239
284
  if (this.reconnectTimeout) {
240
285
  clearTimeout(this.reconnectTimeout);
241
286
  this.reconnectTimeout = null;
@@ -251,10 +296,10 @@ export class LSPClient {
251
296
  // Ignore errors during shutdown
252
297
  }
253
298
  }
254
- this.ws.close();
255
- this.ws = null;
299
+ this.cleanupSocket();
256
300
  }
257
301
  this.setState('disconnected');
302
+ this.intentionalDisconnect = false;
258
303
  }
259
304
  // ============================================================================
260
305
  // Initialize
@@ -6,6 +6,7 @@
6
6
  */
7
7
  export interface OptimisticOperation<T> {
8
8
  id: string;
9
+ scopeKey?: string;
9
10
  type: string;
10
11
  payload: T;
11
12
  timestamp: number;
@@ -53,6 +54,7 @@ type CommitFn<T> = () => Promise<T>;
53
54
  * ```
54
55
  */
55
56
  export declare function optimisticUpdate<T, R>(options: {
57
+ scopeKey?: string;
56
58
  type: string;
57
59
  payload: T;
58
60
  apply: () => void;
@@ -84,6 +86,7 @@ export declare function createOptimisticState<T>(initialValue: T): {
84
86
  * Batch multiple optimistic operations
85
87
  */
86
88
  export declare function batchOptimisticUpdates(operations: Array<{
89
+ scopeKey?: string;
87
90
  type: string;
88
91
  payload: unknown;
89
92
  apply: () => void;
@@ -95,9 +98,9 @@ export declare function batchOptimisticUpdates(operations: Array<{
95
98
  failedCount: number;
96
99
  }>;
97
100
  /**
98
- * Get all pending operations
101
+ * Get pending operations, optionally limited to one document/workspace scope.
99
102
  */
100
- export declare function getPendingOperations(): OptimisticOperation<unknown>[];
103
+ export declare function getPendingOperations(scopeKey?: string): OptimisticOperation<unknown>[];
101
104
  /**
102
105
  * Get operation by ID
103
106
  */
@@ -105,11 +108,11 @@ export declare function getOperation(id: string): OptimisticOperation<unknown> |
105
108
  /**
106
109
  * Cancel a pending operation
107
110
  */
108
- export declare function cancelOperation(id: string): Promise<boolean>;
111
+ export declare function cancelOperation(id: string, scopeKey?: string): Promise<boolean>;
109
112
  /**
110
- * Cancel all pending operations
113
+ * Cancel pending operations, optionally limited to one document/workspace scope.
111
114
  */
112
- export declare function cancelAllOperations(): Promise<void>;
115
+ export declare function cancelAllOperations(scopeKey?: string): Promise<void>;
113
116
  export interface ConflictInfo {
114
117
  type: 'version' | 'lock' | 'concurrent';
115
118
  localVersion?: number;
@@ -4,6 +4,7 @@
4
4
  * Provides utilities for optimistic UI updates with automatic rollback on failure.
5
5
  * Implements a queue-based system for managing pending operations.
6
6
  */
7
+ const DEFAULT_SCOPE_KEY = '__global__';
7
8
  // ============================================================================
8
9
  // Operation Queue
9
10
  // ============================================================================
@@ -38,8 +39,10 @@ const rollbackFunctions = new Map();
38
39
  export async function optimisticUpdate(options) {
39
40
  const { type, payload, apply, rollback, commit, config = {} } = options;
40
41
  const { maxRetries = 3, retryDelay = 1000, onCommit, onRollback, onRetry } = config;
42
+ const scopeKey = options.scopeKey ?? getScopeKeyFromPayload(payload);
41
43
  const operation = {
42
44
  id: crypto.randomUUID(),
45
+ scopeKey,
43
46
  type,
44
47
  payload,
45
48
  timestamp: Date.now(),
@@ -85,7 +88,7 @@ export async function optimisticUpdate(options) {
85
88
  operation.retryCount++;
86
89
  if (operation.retryCount <= maxRetries) {
87
90
  onRetry?.(operation, operation.retryCount);
88
- await delay(retryDelay * operation.retryCount); // Exponential backoff
91
+ await delay(getRetryDelay(retryDelay, operation.retryCount));
89
92
  }
90
93
  }
91
94
  }
@@ -227,10 +230,10 @@ export async function batchOptimisticUpdates(operations, config) {
227
230
  // Queue Management
228
231
  // ============================================================================
229
232
  /**
230
- * Get all pending operations
233
+ * Get pending operations, optionally limited to one document/workspace scope.
231
234
  */
232
- export function getPendingOperations() {
233
- return Array.from(operationQueue.values()).filter((op) => op.status === 'pending');
235
+ export function getPendingOperations(scopeKey) {
236
+ return Array.from(operationQueue.values()).filter((op) => op.status === 'pending' && (!scopeKey || op.scopeKey === scopeKey));
234
237
  }
235
238
  /**
236
239
  * Get operation by ID
@@ -241,9 +244,11 @@ export function getOperation(id) {
241
244
  /**
242
245
  * Cancel a pending operation
243
246
  */
244
- export async function cancelOperation(id) {
247
+ export async function cancelOperation(id, scopeKey) {
245
248
  const operation = operationQueue.get(id);
246
- if (!operation || operation.status !== 'pending') {
249
+ if (!operation ||
250
+ operation.status !== 'pending' ||
251
+ (scopeKey && operation.scopeKey !== scopeKey)) {
247
252
  return false;
248
253
  }
249
254
  const rollback = rollbackFunctions.get(id);
@@ -262,12 +267,12 @@ export async function cancelOperation(id) {
262
267
  return true;
263
268
  }
264
269
  /**
265
- * Cancel all pending operations
270
+ * Cancel pending operations, optionally limited to one document/workspace scope.
266
271
  */
267
- export async function cancelAllOperations() {
268
- const pending = getPendingOperations();
272
+ export async function cancelAllOperations(scopeKey) {
273
+ const pending = getPendingOperations(scopeKey);
269
274
  for (const op of pending) {
270
- await cancelOperation(op.id);
275
+ await cancelOperation(op.id, scopeKey);
271
276
  }
272
277
  }
273
278
  /**
@@ -319,6 +324,27 @@ export function parseConflictDetails(error) {
319
324
  function delay(ms) {
320
325
  return new Promise((resolve) => setTimeout(resolve, ms));
321
326
  }
327
+ function getScopeKeyFromPayload(payload) {
328
+ if (payload && typeof payload === 'object') {
329
+ const candidate = payload;
330
+ if (typeof candidate.scopeKey === 'string')
331
+ return candidate.scopeKey;
332
+ if (typeof candidate.workspaceId === 'string' && typeof candidate.path === 'string') {
333
+ return `${candidate.workspaceId}:${candidate.path}`;
334
+ }
335
+ if (typeof candidate.workspace === 'string' && typeof candidate.path === 'string') {
336
+ return `${candidate.workspace}:${candidate.path}`;
337
+ }
338
+ if (typeof candidate.path === 'string')
339
+ return candidate.path;
340
+ }
341
+ return DEFAULT_SCOPE_KEY;
342
+ }
343
+ function getRetryDelay(baseDelay, attempt) {
344
+ const exponentialDelay = baseDelay * 2 ** (attempt - 1);
345
+ const jitter = 0.75 + Math.random() * 0.5;
346
+ return Math.round(exponentialDelay * jitter);
347
+ }
322
348
  /**
323
349
  * Create a debounced optimistic updater
324
350
  */
@@ -55,7 +55,7 @@ async function request(path, options = {}) {
55
55
  if (err instanceof VFSError)
56
56
  throw err;
57
57
  if (err instanceof Error && err.name === 'AbortError') {
58
- throw new VFSError('Request timeout', 'NETWORK_ERROR');
58
+ throw new VFSError('Request timeout', 'TIMEOUT');
59
59
  }
60
60
  throw new VFSError(err instanceof Error ? err.message : 'Unknown error', 'NETWORK_ERROR', err);
61
61
  }
@@ -65,14 +65,22 @@ async function request(path, options = {}) {
65
65
  }
66
66
  function mapStatusToErrorCode(status) {
67
67
  switch (status) {
68
- case 404:
69
- return 'FILE_NOT_FOUND';
68
+ case 401:
70
69
  case 403:
71
70
  return 'PERMISSION_DENIED';
71
+ case 404:
72
+ return 'FILE_NOT_FOUND';
72
73
  case 409:
73
74
  return 'VERSION_CONFLICT';
74
75
  case 423:
75
76
  return 'FILE_LOCKED';
77
+ case 429:
78
+ return 'RATE_LIMITED';
79
+ case 500:
80
+ case 502:
81
+ case 503:
82
+ case 504:
83
+ return 'SERVER_ERROR';
76
84
  default:
77
85
  return 'NETWORK_ERROR';
78
86
  }
@@ -5,13 +5,14 @@
5
5
  * Note: Svelte 5 modules cannot directly export $derived values.
6
6
  * We use getter functions to expose reactive derived state.
7
7
  */
8
+ import { SvelteMap } from 'svelte/reactivity';
8
9
  const state = $state({
9
- agents: new Map(),
10
+ agents: new SvelteMap(),
10
11
  events: [],
11
12
  maxEvents: 200,
12
13
  activities: [],
13
14
  maxActivities: 500,
14
- cursors: new Map(),
15
+ cursors: new SvelteMap(),
15
16
  selectedAgentId: null,
16
17
  filter: 'all',
17
18
  connected: false,
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * Handles saving and loading AI conversations to IndexedDB/localStorage
5
5
  */
6
+ import { browser } from '$app/environment';
6
7
  const DEFAULT_CONFIG = {
7
8
  dbName: 'svelte-ide-ai',
8
9
  storeName: 'conversations',
@@ -31,7 +32,7 @@ export async function initPersistence(options) {
31
32
  config = { ...DEFAULT_CONFIG, ...options };
32
33
  try {
33
34
  // Check if IndexedDB is available
34
- if (!('indexedDB' in globalThis)) {
35
+ if (!browser || !('indexedDB' in globalThis)) {
35
36
  console.warn('IndexedDB not available, using localStorage fallback');
36
37
  initialized = true;
37
38
  return true;
@@ -52,7 +53,7 @@ export async function initPersistence(options) {
52
53
  */
53
54
  function openDatabase() {
54
55
  return new Promise((resolve, reject) => {
55
- const request = indexedDB.open(config.dbName, config.version);
56
+ const request = globalThis.indexedDB.open(config.dbName, config.version);
56
57
  request.onerror = () => reject(request.error);
57
58
  request.onsuccess = () => resolve(request.result);
58
59
  request.onupgradeneeded = (event) => {
@@ -318,6 +319,8 @@ export async function pruneOldConversations() {
318
319
  }
319
320
  // localStorage helpers
320
321
  function getLocalStorageConversations() {
322
+ if (typeof localStorage === 'undefined')
323
+ return [];
321
324
  try {
322
325
  const data = localStorage.getItem('ai-conversations');
323
326
  return data ? JSON.parse(data) : [];
@@ -327,6 +330,8 @@ function getLocalStorageConversations() {
327
330
  }
328
331
  }
329
332
  function setLocalStorageConversations(conversations) {
333
+ if (typeof localStorage === 'undefined')
334
+ return;
330
335
  try {
331
336
  localStorage.setItem('ai-conversations', JSON.stringify(conversations));
332
337
  }
@@ -5,6 +5,7 @@
5
5
  * Note: Svelte 5 modules cannot directly export $derived values.
6
6
  * We use getter functions to expose reactive derived state.
7
7
  */
8
+ import { SvelteMap } from 'svelte/reactivity';
8
9
  // Default configuration
9
10
  const defaultConfig = {
10
11
  endpoint: '/api/chat',
@@ -19,7 +20,7 @@ const defaultConfig = {
19
20
  const state = $state({
20
21
  conversations: [],
21
22
  activeConversationId: null,
22
- tools: new Map(),
23
+ tools: new SvelteMap(),
23
24
  config: { ...defaultConfig },
24
25
  editSessions: [],
25
26
  suggestions: [],
@@ -24,7 +24,7 @@ export declare const config: {
24
24
  readonly current: CollaborationConfig | null;
25
25
  };
26
26
  export declare const status: {
27
- readonly current: "error" | "connected" | "disconnected" | "connecting";
27
+ readonly current: "error" | "disconnected" | "connecting" | "connected";
28
28
  };
29
29
  export declare const error: {
30
30
  readonly current: string | null;
@@ -5,6 +5,7 @@
5
5
  * Note: Svelte 5 modules cannot directly export $derived values.
6
6
  * We use getter functions to expose reactive derived state.
7
7
  */
8
+ import { SvelteMap } from 'svelte/reactivity';
8
9
  // Reactive state
9
10
  const state = $state({
10
11
  config: null,
@@ -12,8 +13,8 @@ const state = $state({
12
13
  error: null,
13
14
  synced: false,
14
15
  users: [],
15
- cursors: new Map(),
16
- awareness: new Map(),
16
+ cursors: new SvelteMap(),
17
+ awareness: new SvelteMap(),
17
18
  aiSessions: [],
18
19
  pendingChanges: [],
19
20
  snapshots: [],