@nocturnium/svelte-ide 1.0.0-rc.1 → 1.0.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 (85) hide show
  1. package/dist/components/agents/AgentCursor.svelte +1 -1
  2. package/dist/components/agents/AgentPresenceBar.svelte +0 -1
  3. package/dist/components/ai/AIConversationList.svelte +1 -0
  4. package/dist/components/ai/AIInlineEdit.svelte +6 -1
  5. package/dist/components/ai/AIMessage.svelte +1 -1
  6. package/dist/components/ai/AIMessageActions.svelte +0 -1
  7. package/dist/components/ai/AIMessageContent.svelte +2 -2
  8. package/dist/components/ai/AIPanel.svelte +7 -8
  9. package/dist/components/core/Avatar.svelte +1 -1
  10. package/dist/components/core/ContextMenu.svelte +1 -1
  11. package/dist/components/core/ErrorBoundary.svelte +1 -1
  12. package/dist/components/editor/BreakpointLayer.svelte +3 -3
  13. package/dist/components/editor/CognitiveLoadMeter.svelte +2 -2
  14. package/dist/components/editor/CollaborativeEditor.svelte +2 -2
  15. package/dist/components/editor/CommandPalette.svelte +7 -6
  16. package/dist/components/editor/ComplexityLayer.svelte +2 -1
  17. package/dist/components/editor/ComplexityLayer.svelte.d.ts +0 -7
  18. package/dist/components/editor/ConflictZoneLayer.svelte +2 -2
  19. package/dist/components/editor/ContextLens.svelte +1 -1
  20. package/dist/components/editor/CustomEditor.svelte +2 -4
  21. package/dist/components/editor/DebugConsole.svelte +6 -11
  22. package/dist/components/editor/EchoCursorLayer.svelte +4 -6
  23. package/dist/components/editor/EditorGutter.svelte +2 -2
  24. package/dist/components/editor/EditorPane.svelte +1 -1
  25. package/dist/components/editor/EditorSelections.svelte +2 -2
  26. package/dist/components/editor/FileExplorer.svelte +6 -18
  27. package/dist/components/editor/FindReplace.svelte +2 -5
  28. package/dist/components/editor/GhostBracketLayer.svelte +1 -2
  29. package/dist/components/editor/InlineDiagnosticsLayer.svelte +4 -3
  30. package/dist/components/editor/MinimalEditor.svelte +1 -1
  31. package/dist/components/editor/MinimalEditor2.svelte +1 -1
  32. package/dist/components/editor/PluginPreviewSandbox.svelte +2 -3
  33. package/dist/components/editor/PluginPreviewSandbox.svelte.d.ts +7 -0
  34. package/dist/components/editor/ProblemsPanel.svelte +6 -7
  35. package/dist/components/editor/QuickActionsMenu.svelte +1 -3
  36. package/dist/components/editor/SnippetPalette.svelte +6 -5
  37. package/dist/components/editor/SnippetPalette.svelte.d.ts +6 -0
  38. package/dist/components/editor/StructureMap.svelte +8 -3
  39. package/dist/components/editor/SymbolOutline.svelte +13 -25
  40. package/dist/components/editor/TimelineScrubber.svelte +3 -4
  41. package/dist/components/editor/TokenRenderer.svelte +1 -1
  42. package/dist/components/editor/core/bracket-healer.js +1 -1
  43. package/dist/components/editor/core/crdt-binding.js +1 -1
  44. package/dist/components/editor/core/folding.js +2 -2
  45. package/dist/components/editor/core/git-blame.js +1 -1
  46. package/dist/components/editor/core/keybindings.js +0 -4
  47. package/dist/components/editor/core/multi-cursor.d.ts +1 -1
  48. package/dist/components/editor/core/multi-cursor.js +1 -1
  49. package/dist/components/editor/core/snippet-manager.js +1 -1
  50. package/dist/components/editor/core/state.js +2 -2
  51. package/dist/components/editor/editor-input.js +4 -4
  52. package/dist/components/editor/languages.js +1 -1
  53. package/dist/components/editor/tokenizer/languages/css.js +0 -5
  54. package/dist/components/editor/tokenizer/languages/html.js +1 -1
  55. package/dist/components/editor/tokenizer/languages/javascript.js +1 -3
  56. package/dist/components/editor/tokenizer/languages/markdown.js +2 -2
  57. package/dist/components/editor/tokenizer/languages/svelte.js +1 -1
  58. package/dist/components/layout/IDELayout.svelte +28 -7
  59. package/dist/components/layout/IDELayout.svelte.d.ts +26 -24
  60. package/dist/components/layout/StatusBar.svelte +0 -1
  61. package/dist/components/lsp/AutocompleteWidget.svelte +8 -6
  62. package/dist/components/lsp/DiagnosticMarker.svelte +2 -1
  63. package/dist/components/lsp/DiagnosticsPanel.svelte +9 -15
  64. package/dist/components/lsp/HoverTooltip.svelte +4 -3
  65. package/dist/components/lsp/LSPEditor.svelte +9 -10
  66. package/dist/components/plugins/PluginCard.svelte +1 -0
  67. package/dist/components/plugins/PluginProposalForm.svelte +2 -3
  68. package/dist/components/vfs/LockConflictDialog.svelte +2 -2
  69. package/dist/components/vfs/LockIndicator.svelte +0 -6
  70. package/dist/components/vfs/VersionConflictDialog.svelte +3 -0
  71. package/dist/services/lsp-client.js +2 -2
  72. package/dist/services/optimistic.d.ts +1 -1
  73. package/dist/services/vfs-client.js +1 -1
  74. package/dist/stores/agents.svelte.js +19 -2
  75. package/dist/stores/ai-persistence.svelte.d.ts +3 -1
  76. package/dist/stores/ai-persistence.svelte.js +19 -6
  77. package/dist/stores/ai.svelte.js +9 -1
  78. package/dist/stores/collaboration.svelte.js +11 -3
  79. package/dist/stores/editor.svelte.js +1 -1
  80. package/dist/stores/layout.svelte.js +1 -1
  81. package/dist/stores/plugin.svelte.js +4 -2
  82. package/dist/stores/vfs.svelte.d.ts +1 -1
  83. package/dist/stores/vfs.svelte.js +12 -6
  84. package/dist/types/plugin.d.ts +0 -12
  85. package/package.json +1 -178
@@ -9,7 +9,7 @@
9
9
  * - Typing indicator animation
10
10
  */
11
11
 
12
- import type { Agent, AgentCursor as AgentCursorType, CursorPosition, CursorSelection } from '../../types';
12
+ import type { Agent, CursorPosition, CursorSelection } from '../../types';
13
13
 
14
14
  interface Props {
15
15
  /** The agent this cursor belongs to */
@@ -45,7 +45,6 @@
45
45
  let overflowCount = $derived(Math.max(0, activeAgents.length - maxVisible));
46
46
 
47
47
  // Group by status for summary
48
- let onlineCount = $derived(agents.filter((a) => a.status === 'online').length);
49
48
  let busyCount = $derived(agents.filter((a) => a.status === 'busy').length);
50
49
  let stalledCount = $derived(agents.filter((a) => a.status === 'stalled').length);
51
50
  </script>
@@ -78,6 +78,7 @@
78
78
  if (filter === 'starred') {
79
79
  result = result.filter((c) => c.starred);
80
80
  } else if (filter === 'today') {
81
+ // eslint-disable-next-line svelte/prefer-svelte-reactivity -- temporary comparison value, not reactive UI state
81
82
  const today = new Date();
82
83
  today.setHours(0, 0, 0, 0);
83
84
  result = result.filter((c) => new Date(c.updatedAt) >= today);
@@ -17,6 +17,8 @@
17
17
  class: className = ''
18
18
  }: Props = $props();
19
19
 
20
+ // Seeded once from the prop: this is the editable input buffer, mounted fresh per edit.
21
+ // svelte-ignore state_referenced_locally
20
22
  let prompt = $state(initialPrompt);
21
23
  let isSubmitting = $state(false);
22
24
 
@@ -40,6 +42,9 @@
40
42
  }
41
43
  </script>
42
44
 
45
+ <!-- Wrapper-level keydown only captures Escape / Cmd+Enter shortcuts; the real controls
46
+ (textarea, buttons) inside are the focusable interactive elements. -->
47
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
43
48
  <div class="ai-inline-edit {className}" onkeydown={handleKeydown}>
44
49
  <div class="ai-inline-edit__header">
45
50
  <Icon name="sparkles" size={14} />
@@ -75,7 +80,7 @@
75
80
  {#if isSubmitting}
76
81
  <Spinner size="xs" />
77
82
  {:else}
78
- {#snippet icon()}<Icon name="wand" size={14} />{/snippet}
83
+ {#snippet _icon()}<Icon name="wand" size={14} />{/snippet}
79
84
  {/if}
80
85
  Apply
81
86
  </Button>
@@ -85,7 +85,7 @@
85
85
 
86
86
  {#if message.toolCalls && message.toolCalls.length > 0}
87
87
  <div class="ai-message__tool-calls">
88
- {#each message.toolCalls as toolCall}
88
+ {#each message.toolCalls as toolCall (toolCall.id)}
89
89
  <AIToolCallDisplay
90
90
  {toolCall}
91
91
  status="completed"
@@ -17,7 +17,6 @@
17
17
  let showConfirmDelete = $state(false);
18
18
 
19
19
  const isUser = $derived(message.role === 'user');
20
- const isAssistant = $derived(message.role === 'assistant');
21
20
 
22
21
  async function handleCopy() {
23
22
  try {
@@ -154,7 +154,7 @@
154
154
  </script>
155
155
 
156
156
  <div class="message-content" class:streaming={isStreaming}>
157
- {#each blocks as block, i}
157
+ {#each blocks as block, i (i)}
158
158
  {#if block.type === 'code'}
159
159
  <div class="code-block">
160
160
  <div class="code-header">
@@ -173,7 +173,7 @@
173
173
  {/if}
174
174
  </button>
175
175
  </div>
176
- <pre class="code-content"><code>{#each tokenizeCode(block.content, block.language || 'plaintext') as token}<span class="token-{token.type}">{token.text}</span>{/each}</code></pre>
176
+ <pre class="code-content"><code>{#each tokenizeCode(block.content, block.language || 'plaintext') as token, ti (ti)}<span class="token-{token.type}">{token.text}</span>{/each}</code></pre>
177
177
  </div>
178
178
  {:else if block.type === 'inline-code'}
179
179
  <code class="inline-code">{block.content}</code>
@@ -17,6 +17,7 @@
17
17
  import {
18
18
  initPersistence,
19
19
  saveConversation,
20
+ autoSaveConversation,
20
21
  loadConversations,
21
22
  deleteConversation as deletePersisted,
22
23
  toggleStarConversation,
@@ -37,12 +38,8 @@
37
38
  let inputValue = $state('');
38
39
  let messagesContainer: HTMLDivElement;
39
40
  let textareaEl: HTMLTextAreaElement;
40
- let sidebarOpen = $state(false);
41
-
42
- // Sync with prop changes
43
- $effect(() => {
44
- sidebarOpen = showSidebar;
45
- });
41
+ // Writable derived: tracks the prop, but can be toggled locally (Svelte ≥5.25).
42
+ let sidebarOpen = $derived(showSidebar);
46
43
  let persistedConversations = $state<AIConversation[]>([]);
47
44
 
48
45
  // Auto-resize textarea without causing cursor jump
@@ -72,11 +69,13 @@
72
69
  }
73
70
  });
74
71
 
75
- // Auto-save conversation when messages change
72
+ // Auto-save conversation when messages change. Use the debounced helper (not
73
+ // saveConversation) so a burst of streamed tokens collapses into one write and
74
+ // the persistence call is deferred out of this effect's synchronous run.
76
75
  $effect(() => {
77
76
  const conv = getActiveConversation();
78
77
  if (conv && getMessages().length > 0) {
79
- saveConversation(conv);
78
+ autoSaveConversation(conv);
80
79
  }
81
80
  });
82
81
 
@@ -15,7 +15,7 @@
15
15
  size = 'md',
16
16
  color,
17
17
  isAI = false,
18
- status,
18
+ status: _status,
19
19
  class: className = ''
20
20
  }: Props = $props();
21
21
 
@@ -81,7 +81,7 @@
81
81
  style="left: {adjustedX}px; top: {adjustedY}px"
82
82
  role="menu"
83
83
  >
84
- {#each items as item}
84
+ {#each items as item (item.id)}
85
85
  {#if item.separator}
86
86
  <div class="ide-context-menu__separator"></div>
87
87
  {:else}
@@ -86,7 +86,7 @@ export function clearError() {
86
86
 
87
87
  {#if recoveryOptions.length > 0}
88
88
  <div class="recovery-options">
89
- {#each recoveryOptions as option}
89
+ {#each recoveryOptions as option (option.id)}
90
90
  <button
91
91
  class="recovery-btn"
92
92
  class:recommended={option.recommended}
@@ -10,7 +10,7 @@
10
10
  */
11
11
 
12
12
  import { onMount } from 'svelte';
13
- import type { BreakpointManager, Breakpoint, BreakpointType } from './core/breakpoints';
13
+ import type { BreakpointManager, Breakpoint } from './core/breakpoints';
14
14
  import { getBreakpointIcon, getBreakpointColor } from './core/breakpoints';
15
15
 
16
16
  interface Props {
@@ -37,7 +37,7 @@
37
37
  gutterWidth = 50,
38
38
  enabled = true,
39
39
  onToggle,
40
- onEdit
40
+ onEdit: _onEdit
41
41
  }: Props = $props();
42
42
 
43
43
  let breakpoints = $state<Breakpoint[]>([]);
@@ -251,7 +251,7 @@
251
251
  }}
252
252
  onkeydown={(e) => {
253
253
  if (e.key === 'Enter') {
254
- const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
254
+ const _rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
255
255
  handleGutterClick(0, e as unknown as MouseEvent);
256
256
  }
257
257
  }}
@@ -121,7 +121,7 @@
121
121
  <div class="cognitive-meter__tooltip-section">
122
122
  <span class="cognitive-meter__tooltip-label">High complexity regions:</span>
123
123
  <ul class="cognitive-meter__tooltip-list">
124
- {#each highComplexityRegions.slice(0, 5) as region}
124
+ {#each highComplexityRegions.slice(0, 5) as region (region.startLine)}
125
125
  <li>
126
126
  <span class="cognitive-meter__tooltip-region-name">
127
127
  {region.name || `${region.type} at line ${region.startLine + 1}`}
@@ -143,7 +143,7 @@
143
143
  {#if metrics.regions.some((r) => r.suggestion)}
144
144
  <div class="cognitive-meter__tooltip-section">
145
145
  <span class="cognitive-meter__tooltip-label">Suggestions:</span>
146
- {#each metrics.regions.filter((r) => r.suggestion).slice(0, 2) as region}
146
+ {#each metrics.regions.filter((r) => r.suggestion).slice(0, 2) as region (region.startLine)}
147
147
  <p class="cognitive-meter__tooltip-suggestion">
148
148
  {region.suggestion}
149
149
  </p>
@@ -46,14 +46,14 @@
46
46
 
47
47
  let {
48
48
  doc: externalDoc,
49
- documentId,
49
+ documentId: _documentId,
50
50
  initialContent = '',
51
51
  textName = 'content',
52
52
  language = 'plaintext',
53
53
  readonly = false,
54
54
  preferences = {},
55
55
  class: className = '',
56
- currentUser,
56
+ currentUser: _currentUser,
57
57
  onChange,
58
58
  onCursorChange,
59
59
  onSave
@@ -7,7 +7,7 @@
7
7
  */
8
8
 
9
9
  import { getCommandRegistry, type Command } from './core/commands';
10
- import { onMount } from 'svelte';
10
+ import { SvelteMap } from 'svelte/reactivity';
11
11
 
12
12
  interface Props {
13
13
  /** Whether the palette is open */
@@ -43,7 +43,7 @@
43
43
 
44
44
  // Group commands by category
45
45
  let groupedCommands = $derived.by(() => {
46
- const groups = new Map<string, Command[]>();
46
+ const groups = new SvelteMap<string, Command[]>();
47
47
  for (const cmd of commands) {
48
48
  const existing = groups.get(cmd.category) || [];
49
49
  existing.push(cmd);
@@ -57,7 +57,8 @@
57
57
 
58
58
  // Reset selection when query changes
59
59
  $effect(() => {
60
- query;
60
+ // Track query to reset selection on each change
61
+ void query;
61
62
  selectedIndex = 0;
62
63
  });
63
64
 
@@ -169,7 +170,6 @@
169
170
  </script>
170
171
 
171
172
  {#if open}
172
- <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
173
173
  <div
174
174
  class="command-palette__backdrop"
175
175
  onclick={handleBackdropClick}
@@ -200,13 +200,13 @@
200
200
  {#if flatCommands.length === 0}
201
201
  <div class="command-palette__empty">No commands found</div>
202
202
  {:else}
203
- {#each [...groupedCommands] as [category, categoryCommands], categoryIdx}
203
+ {#each [...groupedCommands] as [category, categoryCommands], _categoryIdx (category)}
204
204
  <div class="command-palette__group">
205
205
  <div class="command-palette__group-header">
206
206
  <span class="command-palette__group-icon">{getCategoryIcon(category)}</span>
207
207
  <span class="command-palette__group-label">{getCategoryLabel(category)}</span>
208
208
  </div>
209
- {#each categoryCommands as command, cmdIdx}
209
+ {#each categoryCommands as command, cmdIdx (command.id)}
210
210
  {@const flatIdx = getFlatIndex(category, cmdIdx)}
211
211
  <button
212
212
  class="command-palette__item"
@@ -216,6 +216,7 @@
216
216
  >
217
217
  <span class="command-palette__item-icon">{command.icon || ''}</span>
218
218
  <span class="command-palette__item-label">
219
+ <!-- eslint-disable-next-line svelte/no-at-html-tags -- content is sanitized via escapeHtml before regex substitution -->
219
220
  {@html highlightMatch(command.label, query)}
220
221
  </span>
221
222
  {#if command.shortcut}
@@ -7,6 +7,7 @@
7
7
  * Hover over gutter indicators for details.
8
8
  */
9
9
 
10
+ import { SvelteMap } from 'svelte/reactivity';
10
11
  import type { ComplexityMetrics, ComplexityRegion } from './core/complexity-analyzer';
11
12
 
12
13
  interface Props {
@@ -58,7 +59,7 @@
58
59
 
59
60
  // Group by line (in case of overlapping regions, take highest score)
60
61
  let lineScores = $derived.by(() => {
61
- const scores = new Map<number, { score: number; region: ComplexityRegion; isStart: boolean; isEnd: boolean }>();
62
+ const scores = new SvelteMap<number, { score: number; region: ComplexityRegion; isStart: boolean; isEnd: boolean }>();
62
63
 
63
64
  for (const indicator of lineIndicators) {
64
65
  const existing = scores.get(indicator.line);
@@ -1,10 +1,3 @@
1
- /**
2
- * Complexity Layer
3
- *
4
- * Shows complexity indicators in the gutter area only.
5
- * No background highlighting - keeps editing area clean.
6
- * Hover over gutter indicators for details.
7
- */
8
1
  import type { ComplexityMetrics } from './core/complexity-analyzer';
9
2
  interface Props {
10
3
  /** Complexity metrics for the current document */
@@ -149,7 +149,7 @@
149
149
  <!-- Participant avatars -->
150
150
  {#if showParticipants && zone.participants.length > 0}
151
151
  <div class="conflict-zone__participants">
152
- {#each zone.participants.slice(0, 3) as participant, i}
152
+ {#each zone.participants.slice(0, 3) as participant, i (participant.userId)}
153
153
  <div
154
154
  class="conflict-zone__avatar"
155
155
  class:conflict-zone__avatar--ai={participant.isAI}
@@ -209,7 +209,7 @@
209
209
  </div>
210
210
 
211
211
  <div class="conflict-tooltip__participants">
212
- {#each hoveredZone.participants as participant}
212
+ {#each hoveredZone.participants as participant (participant.userId)}
213
213
  <div class="conflict-tooltip__participant">
214
214
  <span
215
215
  class="conflict-tooltip__dot"
@@ -52,7 +52,7 @@
52
52
  charWidth,
53
53
  gutterWidth,
54
54
  enabled = true,
55
- language = 'typescript'
55
+ language: _language = 'typescript'
56
56
  }: Props = $props();
57
57
 
58
58
  // Extract context lens items from code
@@ -154,7 +154,6 @@
154
154
  let cursorBlinkInterval: ReturnType<typeof setInterval>;
155
155
 
156
156
  // Track previous content prop to detect external changes only
157
- // svelte-ignore state_referenced_locally
158
157
  let previousContentProp = $state(content);
159
158
 
160
159
  // Track the content the editor itself last emitted (via onChange / bind:content
@@ -163,7 +162,6 @@
163
162
  // rebuild the document model every keystroke, wiping undo/redo and collapsing
164
163
  // multi-cursor. Only a genuine external change (content !== what we emitted and
165
164
  // !== the current editor content) should re-apply via setContent.
166
- // svelte-ignore state_referenced_locally
167
165
  let lastEmittedContent = content;
168
166
 
169
167
  // Track last cursor position to avoid unnecessary blink resets
@@ -571,7 +569,7 @@
571
569
  });
572
570
  }
573
571
 
574
- function openFind(withReplace = false) {
572
+ function openFind(_withReplace = false) {
575
573
  showFindReplace = true;
576
574
 
577
575
  // Pre-fill with selected text if any
@@ -934,7 +932,7 @@
934
932
  <!-- Search match highlights (below selection) -->
935
933
  {#if showFindReplace && searchMatches.length > 0}
936
934
  <div class="custom-editor__matches">
937
- {#each matchRects as rect}
935
+ {#each matchRects as rect, i (i)}
938
936
  <div
939
937
  class="custom-editor__match"
940
938
  class:custom-editor__match--current={rect.isCurrent}
@@ -9,7 +9,7 @@
9
9
  * - Command history
10
10
  */
11
11
 
12
- import { onMount } from 'svelte';
12
+ import { SvelteSet } from 'svelte/reactivity';
13
13
 
14
14
  export type ConsoleEntryType = 'input' | 'output' | 'error' | 'warning' | 'info' | 'log';
15
15
 
@@ -69,10 +69,7 @@
69
69
  let inputRef = $state<HTMLInputElement>(null!);
70
70
  let consoleRef = $state<HTMLDivElement>(null!);
71
71
  let filter = $state<ConsoleEntryType | 'all'>('all');
72
- let expandedIds = $state<Set<string>>(new Set());
73
-
74
- // Entry counter for unique IDs
75
- let entryCounter = 0;
72
+ const expandedIds = new SvelteSet<string>();
76
73
 
77
74
  // Filtered entries
78
75
  const filteredEntries = $derived(
@@ -156,13 +153,11 @@
156
153
  * Toggle entry expansion
157
154
  */
158
155
  function toggleExpand(id: string) {
159
- const newExpanded = new Set(expandedIds);
160
- if (newExpanded.has(id)) {
161
- newExpanded.delete(id);
156
+ if (expandedIds.has(id)) {
157
+ expandedIds.delete(id);
162
158
  } else {
163
- newExpanded.add(id);
159
+ expandedIds.add(id);
164
160
  }
165
- expandedIds = newExpanded;
166
161
  }
167
162
 
168
163
  /**
@@ -265,7 +260,7 @@
265
260
  </div>
266
261
 
267
262
  <div class="header-filters">
268
- {#each (['all', 'error', 'warning', 'info', 'log'] as const) as filterType}
263
+ {#each (['all', 'error', 'warning', 'info', 'log'] as const) as filterType (filterType)}
269
264
  {@const count = entryCounts()[filterType]}
270
265
  <button
271
266
  class="filter-btn"
@@ -8,6 +8,7 @@
8
8
 
9
9
  import type { EchoCursor, EchoCursorManager, EchoCursorEvent } from './core/echo-cursor';
10
10
  import { onMount } from 'svelte';
11
+ import { SvelteMap } from 'svelte/reactivity';
11
12
 
12
13
  interface Props {
13
14
  /** Echo cursor manager instance */
@@ -30,12 +31,12 @@
30
31
  charWidth,
31
32
  gutterWidth = 50,
32
33
  enabled = true,
33
- onAddEchoPoint
34
+ onAddEchoPoint: _onAddEchoPoint
34
35
  }: Props = $props();
35
36
 
36
37
  let echoCursors = $state<EchoCursor[]>([]);
37
38
  let replayingCursors = $state<Set<string>>(new Set());
38
- let recentReplay = $state<Map<string, { text: string; opacity: number }>>(new Map());
39
+ let recentReplay = new SvelteMap<string, { text: string; opacity: number }>();
39
40
 
40
41
  // Subscribe to echo cursor events
41
42
  onMount(() => {
@@ -58,7 +59,6 @@
58
59
  text: event.keystroke.data.text,
59
60
  opacity: 1
60
61
  });
61
- recentReplay = new Map(recentReplay);
62
62
  }
63
63
  break;
64
64
  case 'replay-completed':
@@ -68,10 +68,8 @@
68
68
  const entry = recentReplay.get(event.cursorId);
69
69
  if (entry) {
70
70
  entry.opacity = 0;
71
- recentReplay = new Map(recentReplay);
72
71
  setTimeout(() => {
73
72
  recentReplay.delete(event.cursorId);
74
- recentReplay = new Map(recentReplay);
75
73
  }, 200);
76
74
  }
77
75
  }, 100);
@@ -80,7 +78,7 @@
80
78
  if (!event.enabled) {
81
79
  echoCursors = [];
82
80
  replayingCursors = new Set();
83
- recentReplay = new Map();
81
+ recentReplay.clear();
84
82
  }
85
83
  break;
86
84
  }
@@ -20,11 +20,11 @@
20
20
  let {
21
21
  lineIndex,
22
22
  lineNumbers,
23
- gutterWidth,
23
+ gutterWidth: _gutterWidth,
24
24
  folding,
25
25
  hasFoldIndicator,
26
26
  isFoldCollapsed,
27
- hiddenLineCount,
27
+ hiddenLineCount: _hiddenLineCount,
28
28
  onFoldIndicatorClick
29
29
  }: Props = $props();
30
30
  </script>
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">
2
2
  import Editor from './Editor.svelte';
3
3
  import EditorTabs from './EditorTabs.svelte';
4
- import type { EditorTab, EditorPreferences } from '../../types';
4
+ import type { EditorPreferences } from '../../types';
5
5
  import {
6
6
  getTabs,
7
7
  getActiveTab,
@@ -12,7 +12,7 @@
12
12
  getSelectionEnd,
13
13
  isSelectionEmpty
14
14
  } from './core';
15
- import { CONTENT_PADDING, FALLBACK_VIEWPORT_HEIGHT } from './constants';
15
+ import { FALLBACK_VIEWPORT_HEIGHT } from './constants';
16
16
 
17
17
  interface Props {
18
18
  cursors: readonly Cursor[];
@@ -128,7 +128,7 @@
128
128
 
129
129
  <!-- Selection layer (all cursor selections) -->
130
130
  <div class="custom-editor__selections">
131
- {#each getSelectionRects() as rect}
131
+ {#each getSelectionRects() as rect, i (i)}
132
132
  <div
133
133
  class="custom-editor__selection"
134
134
  class:custom-editor__selection--secondary={!rect.isPrimary}
@@ -31,6 +31,7 @@
31
31
  import AgentAvatar from '../agents/AgentAvatar.svelte';
32
32
  import type { VFSLockStatus, Agent } from '../../types';
33
33
  import { tick, onDestroy } from 'svelte';
34
+ import { SvelteSet } from 'svelte/reactivity';
34
35
 
35
36
  interface Props {
36
37
  /** Root file nodes */
@@ -110,15 +111,6 @@
110
111
  return statusMap.get(path) ?? 'clean';
111
112
  }
112
113
 
113
- // Helper to check if a folder contains any files with changes
114
- function folderHasChanges(node: FileNode): boolean {
115
- if (node.type === 'file') {
116
- return getFileStatus(node.path) !== 'clean';
117
- }
118
- if (!node.children) return false;
119
- return node.children.some((child) => folderHasChanges(child));
120
- }
121
-
122
114
  // Get the most "important" status for a folder (deleted > modified > created > renamed)
123
115
  function getFolderStatus(node: FileNode): FileChangeStatus {
124
116
  if (node.type === 'file') return getFileStatus(node.path);
@@ -171,7 +163,7 @@
171
163
  return agentsByFile.get(path) ?? [];
172
164
  }
173
165
 
174
- let expandedFolders = $state<Set<string>>(new Set());
166
+ let expandedFolders = new SvelteSet<string>();
175
167
  let contextMenuNode = $state<FileNode | null>(null);
176
168
  let contextMenuPos = $state({ x: 0, y: 0 });
177
169
  let showContextMenu = $state(false);
@@ -194,13 +186,11 @@
194
186
  function toggleFolder(node: FileNode) {
195
187
  if (node.type !== 'folder') return;
196
188
 
197
- const newExpanded = new Set(expandedFolders);
198
- if (newExpanded.has(node.path)) {
199
- newExpanded.delete(node.path);
189
+ if (expandedFolders.has(node.path)) {
190
+ expandedFolders.delete(node.path);
200
191
  } else {
201
- newExpanded.add(node.path);
192
+ expandedFolders.add(node.path);
202
193
  }
203
- expandedFolders = newExpanded;
204
194
  onToggle?.(node);
205
195
  }
206
196
 
@@ -208,9 +198,7 @@
208
198
  async function startInlineCreation(parentPath: string, type: 'file' | 'folder') {
209
199
  // Expand parent folder if it's not root
210
200
  if (parentPath) {
211
- const newExpanded = new Set(expandedFolders);
212
- newExpanded.add(parentPath);
213
- expandedFolders = newExpanded;
201
+ expandedFolders.add(parentPath);
214
202
  }
215
203
 
216
204
  inlineCreation = {
@@ -57,12 +57,9 @@
57
57
 
58
58
  let findInput = $state<HTMLInputElement | null>(null);
59
59
  let replaceInput = $state<HTMLInputElement | null>(null);
60
- let showReplaceLocal = $state(false);
61
60
 
62
- // Sync with prop
63
- $effect(() => {
64
- showReplaceLocal = showReplace;
65
- });
61
+ // Writable derived: tracks the showReplace prop but allows local overrides (e.g. toggle)
62
+ let showReplaceLocal = $derived(showReplace);
66
63
 
67
64
  // Focus find input on mount
68
65
  onMount(() => {
@@ -143,7 +143,6 @@
143
143
  {/if}
144
144
 
145
145
  <!-- Ghost bracket marker -->
146
- <!-- svelte-ignore a11y_no_static_element_interactions -->
147
146
  <div
148
147
  class="ghost-bracket"
149
148
  role="tooltip"
@@ -187,7 +186,7 @@
187
186
  {/each}
188
187
 
189
188
  <!-- Bracket mismatch markers -->
190
- {#each mismatches as mismatch}
189
+ {#each mismatches as mismatch (`${mismatch.position.line}:${mismatch.position.column}:${mismatch.issue}`)}
191
190
  {@const color = getSeverityColor(mismatch.severity)}
192
191
  <div
193
192
  class="bracket-mismatch bracket-mismatch--{mismatch.issue}"
@@ -72,6 +72,7 @@
72
72
 
73
73
  // Group diagnostics by line for gutter icons
74
74
  const diagnosticsByLine = $derived(() => {
75
+ // eslint-disable-next-line svelte/prefer-svelte-reactivity -- local computation variable recreated each derivation, not mutated reactive state
75
76
  const byLine = new Map<number, Diagnostic[]>();
76
77
  for (const d of diagnostics) {
77
78
  const line = d.range.start.line;
@@ -235,7 +236,6 @@
235
236
 
236
237
  <!-- Tooltip -->
237
238
  {#if showTooltip && hoveredDiagnostic}
238
- <!-- svelte-ignore a11y_no_static_element_interactions -->
239
239
  <div
240
240
  class="diagnostic-tooltip"
241
241
  role="tooltip"
@@ -254,6 +254,7 @@
254
254
  {#if hoveredDiagnostic.code}
255
255
  <span class="tooltip-code">
256
256
  {#if hoveredDiagnostic.codeUrl}
257
+ <!-- eslint-disable-next-line svelte/no-navigation-without-resolve -- codeUrl is an external URL from diagnostic data, not a local app path -->
257
258
  <a href={hoveredDiagnostic.codeUrl} target="_blank" rel="noopener">
258
259
  {hoveredDiagnostic.code}
259
260
  </a>
@@ -268,7 +269,7 @@
268
269
 
269
270
  {#if hoveredDiagnostic.relatedInfo && hoveredDiagnostic.relatedInfo.length > 0}
270
271
  <div class="tooltip-related">
271
- {#each hoveredDiagnostic.relatedInfo as info}
272
+ {#each hoveredDiagnostic.relatedInfo as info, i (i)}
272
273
  <div class="related-item">
273
274
  <span class="related-location">
274
275
  {info.filePath || 'this file'}:{info.range.start.line + 1}
@@ -282,7 +283,7 @@
282
283
  {#if hoveredDiagnostic.fixes && hoveredDiagnostic.fixes.length > 0}
283
284
  <div class="tooltip-fixes">
284
285
  <div class="fixes-header">Quick Fixes</div>
285
- {#each hoveredDiagnostic.fixes as fix}
286
+ {#each hoveredDiagnostic.fixes as fix, i (i)}
286
287
  <button
287
288
  class="fix-button"
288
289
  class:fix-button--preferred={fix.isPreferred}