@webmcp-auto-ui/ui 2.5.31 → 2.5.33

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 (80) hide show
  1. package/package.json +15 -2
  2. package/src/agent/DiagnosticModal.svelte +126 -50
  3. package/src/agent/EphemeralBubble.svelte +13 -3
  4. package/src/agent/MCPserversList.svelte +147 -0
  5. package/src/agent/McpConnector.svelte +10 -1
  6. package/src/agent/RecipeBrowser.svelte +384 -0
  7. package/src/agent/RemoteMCPserversDemo.svelte +5 -121
  8. package/src/agent/ToolBrowser.svelte +133 -0
  9. package/src/agent/WebMCPserversList.svelte +2 -0
  10. package/src/agent/useAgentLoop.svelte.ts +396 -0
  11. package/src/base/chat-inline.svelte +64 -0
  12. package/src/base/dialog-content.svelte +3 -1
  13. package/src/components/HeaderControls.svelte +78 -0
  14. package/src/index.ts +13 -35
  15. package/src/stores/canvas.svelte.ts +0 -6
  16. package/src/widgets/SafeImage.svelte +67 -0
  17. package/src/widgets/WidgetRenderer.svelte +153 -78
  18. package/src/widgets/notebook/executors/index.ts +0 -1
  19. package/src/widgets/notebook/executors/sql.ts +32 -182
  20. package/src/widgets/notebook/import-modal-api.ts +237 -0
  21. package/src/widgets/notebook/import-modal.svelte +738 -0
  22. package/src/widgets/notebook/left-pane.ts +1 -1
  23. package/src/widgets/notebook/notebook.svelte +75 -0
  24. package/src/widgets/notebook/notebook.ts +38 -73
  25. package/src/widgets/notebook/prose.ts +6 -3
  26. package/src/widgets/notebook/shared.ts +68 -49
  27. package/src/widgets/rich/cards.svelte +74 -0
  28. package/src/widgets/rich/carousel.svelte +126 -0
  29. package/src/widgets/rich/chart-rich.svelte +221 -0
  30. package/src/widgets/rich/chat-input.svelte +52 -0
  31. package/src/widgets/rich/data-table.svelte +132 -0
  32. package/src/widgets/rich/gallery.svelte +115 -0
  33. package/src/widgets/rich/grid-data.svelte +85 -0
  34. package/src/widgets/rich/hemicycle.svelte +95 -0
  35. package/src/widgets/rich/js-sandbox.svelte +67 -0
  36. package/src/widgets/rich/json-viewer.svelte +82 -0
  37. package/src/widgets/rich/log.svelte +62 -0
  38. package/src/widgets/rich/profile.svelte +91 -0
  39. package/src/widgets/rich/sankey.svelte +73 -0
  40. package/src/widgets/rich/stat-card.svelte +60 -0
  41. package/src/widgets/rich/timeline.svelte +95 -0
  42. package/src/widgets/rich/trombinoscope.svelte +87 -0
  43. package/src/widgets/simple/actions.svelte +36 -0
  44. package/src/widgets/simple/alert.svelte +52 -0
  45. package/src/widgets/simple/chart.svelte +38 -0
  46. package/src/widgets/simple/code.svelte +30 -0
  47. package/src/widgets/simple/kv.svelte +31 -0
  48. package/src/widgets/simple/list.svelte +35 -0
  49. package/src/widgets/simple/stat.svelte +36 -0
  50. package/src/widgets/simple/tags.svelte +34 -0
  51. package/src/widgets/simple/text.svelte +130 -0
  52. package/src/widgets/helpers/safe-image.ts +0 -78
  53. package/src/widgets/notebook/import-modals.ts +0 -560
  54. package/src/widgets/notebook/recipe-browser.ts +0 -350
  55. package/src/widgets/rich/cards.ts +0 -181
  56. package/src/widgets/rich/carousel.ts +0 -319
  57. package/src/widgets/rich/chart-rich.ts +0 -386
  58. package/src/widgets/rich/d3.ts +0 -503
  59. package/src/widgets/rich/data-table.ts +0 -342
  60. package/src/widgets/rich/gallery.ts +0 -350
  61. package/src/widgets/rich/grid-data.ts +0 -173
  62. package/src/widgets/rich/hemicycle.ts +0 -313
  63. package/src/widgets/rich/js-sandbox.ts +0 -122
  64. package/src/widgets/rich/json-viewer.ts +0 -202
  65. package/src/widgets/rich/log.ts +0 -143
  66. package/src/widgets/rich/map.ts +0 -218
  67. package/src/widgets/rich/profile.ts +0 -256
  68. package/src/widgets/rich/sankey.ts +0 -257
  69. package/src/widgets/rich/stat-card.ts +0 -125
  70. package/src/widgets/rich/timeline.ts +0 -179
  71. package/src/widgets/rich/trombinoscope.ts +0 -246
  72. package/src/widgets/simple/actions.ts +0 -89
  73. package/src/widgets/simple/alert.ts +0 -100
  74. package/src/widgets/simple/chart.ts +0 -189
  75. package/src/widgets/simple/code.ts +0 -79
  76. package/src/widgets/simple/kv.ts +0 -68
  77. package/src/widgets/simple/list.ts +0 -89
  78. package/src/widgets/simple/stat.ts +0 -58
  79. package/src/widgets/simple/tags.ts +0 -125
  80. package/src/widgets/simple/text.ts +0 -198
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webmcp-auto-ui/ui",
3
- "version": "2.5.31",
3
+ "version": "2.5.33",
4
4
  "description": "Svelte 5 UI components — primitives, widgets, window manager",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "type": "module",
@@ -16,11 +16,23 @@
16
16
  "svelte": "./src/stores/canvas.svelte.ts",
17
17
  "import": "./src/stores/canvas.svelte.ts"
18
18
  },
19
+ "./agent": {
20
+ "svelte": "./src/agent/useAgentLoop.svelte.ts",
21
+ "import": "./src/agent/useAgentLoop.svelte.ts"
22
+ },
19
23
  "./widgets/notebook/*": {
20
24
  "svelte": "./src/widgets/notebook/*",
21
25
  "import": "./src/widgets/notebook/*"
22
26
  },
23
- "./widgets/notebook/recipes/*": "./src/widgets/notebook/recipes/*"
27
+ "./widgets/notebook/recipes/*": "./src/widgets/notebook/recipes/*",
28
+ "./widgets/simple/*": {
29
+ "svelte": "./src/widgets/simple/*",
30
+ "import": "./src/widgets/simple/*"
31
+ },
32
+ "./widgets/rich/*": {
33
+ "svelte": "./src/widgets/rich/*",
34
+ "import": "./src/widgets/rich/*"
35
+ }
24
36
  },
25
37
  "scripts": {
26
38
  "build": "svelte-package -i src",
@@ -58,6 +70,7 @@
58
70
  ],
59
71
  "dependencies": {
60
72
  "@types/d3": "^7.4.3",
73
+ "@webmcp-auto-ui/agent": "*",
61
74
  "@webmcp-auto-ui/core": "*",
62
75
  "@webmcp-auto-ui/sdk": "*",
63
76
  "bits-ui": "^2.17.2",
@@ -7,13 +7,32 @@
7
7
  codeFix?: string;
8
8
  }
9
9
 
10
+ interface DebugLayer {
11
+ protocol: string;
12
+ serverName?: string;
13
+ toolCount?: number;
14
+ }
15
+
16
+ interface DebugInfo {
17
+ estimatedTokens: number;
18
+ layerCount: number;
19
+ toolCount: number;
20
+ mcpToolCount: number;
21
+ recipeCount: number;
22
+ layers: DebugLayer[];
23
+ prompt: string;
24
+ }
25
+
10
26
  interface Props {
11
27
  open: boolean;
12
28
  diagnostics: DiagnosticItem[];
13
29
  onclose: () => void;
30
+ debugInfo?: DebugInfo;
14
31
  }
15
32
 
16
- let { open = $bindable(false), diagnostics, onclose }: Props = $props();
33
+ let { open = $bindable(false), diagnostics, onclose, debugInfo }: Props = $props();
34
+
35
+ let activeTab = $state<'diagnostics' | 'debug'>('diagnostics');
17
36
 
18
37
  let expandedQuickFix = $state<Set<number>>(new Set());
19
38
  let expandedCodeFix = $state<Set<number>>(new Set());
@@ -56,59 +75,116 @@
56
75
  onclick={close}>x</button>
57
76
  </div>
58
77
 
59
- <!-- Body -->
60
- <div class="flex-1 overflow-y-auto p-4 flex flex-col gap-3">
61
- {#if diagnostics.length === 0}
62
- <div class="font-mono text-xs text-text2 text-center py-8">Aucun probleme detecte.</div>
63
- {:else}
64
- {#each diagnostics as diag, i}
65
- <div class="rounded-lg border p-3 flex flex-col gap-1.5
66
- {diag.severity === 'error' ? 'border-accent2/30 bg-accent2/5' : 'border-amber-500/30 bg-amber-500/5'}">
67
- <!-- Severity icon + title -->
68
- <div class="flex items-start gap-2">
69
- {#if diag.severity === 'error'}
70
- <span class="flex-shrink-0 w-4 h-4 rounded-full bg-accent2/20 text-accent2 flex items-center justify-center text-[10px] mt-0.5">!</span>
71
- {:else}
72
- <span class="flex-shrink-0 w-4 h-4 flex items-center justify-center text-amber-500 text-xs mt-0.5">&#x26A0;</span>
73
- {/if}
74
- <span class="font-mono text-xs font-bold text-text1">{diag.title}</span>
75
- </div>
76
- <!-- Detail -->
77
- <div class="font-mono text-[11px] text-text2 leading-relaxed pl-6">{diag.detail}</div>
78
-
79
- <!-- Quick fix -->
80
- {#if diag.quickFix}
81
- <div class="pl-6">
82
- <button class="font-mono text-[10px] text-accent hover:underline cursor-pointer"
83
- onclick={() => toggleQuickFix(i)}>
84
- {expandedQuickFix.has(i) ? '- Quick fix (prompt)' : '+ Quick fix (prompt)'}
85
- </button>
86
- {#if expandedQuickFix.has(i)}
87
- <div class="mt-1 relative">
88
- <pre class="bg-surface2 border border-border2 rounded p-2 text-[10px] font-mono text-text2 whitespace-pre-wrap break-words">{diag.quickFix}</pre>
89
- <button class="absolute top-1 right-1 px-1.5 py-0.5 rounded text-[9px] font-mono bg-surface border border-border2 text-text2 hover:text-text1 transition-colors"
90
- onclick={() => copyText(diag.quickFix!)}>Copier</button>
91
- </div>
78
+ <!-- Tabs (only shown when debugInfo is provided) -->
79
+ {#if debugInfo}
80
+ <div class="flex border-b border-border flex-shrink-0">
81
+ <button
82
+ class="px-5 py-2 font-mono text-xs transition-colors border-b-2
83
+ {activeTab === 'diagnostics' ? 'border-accent text-text1' : 'border-transparent text-text2 hover:text-text1'}"
84
+ onclick={() => activeTab = 'diagnostics'}>
85
+ Diagnostics
86
+ </button>
87
+ <button
88
+ class="px-5 py-2 font-mono text-xs transition-colors border-b-2
89
+ {activeTab === 'debug' ? 'border-accent text-text1' : 'border-transparent text-text2 hover:text-text1'}"
90
+ onclick={() => activeTab = 'debug'}>
91
+ Debug
92
+ </button>
93
+ </div>
94
+ {/if}
95
+
96
+ <!-- Body: Diagnostics tab -->
97
+ {#if !debugInfo || activeTab === 'diagnostics'}
98
+ <div class="flex-1 overflow-y-auto p-4 flex flex-col gap-3">
99
+ {#if diagnostics.length === 0}
100
+ <div class="font-mono text-xs text-text2 text-center py-8">Aucun probleme detecte.</div>
101
+ {:else}
102
+ {#each diagnostics as diag, i}
103
+ <div class="rounded-lg border p-3 flex flex-col gap-1.5
104
+ {diag.severity === 'error' ? 'border-accent2/30 bg-accent2/5' : 'border-amber-500/30 bg-amber-500/5'}">
105
+ <!-- Severity icon + title -->
106
+ <div class="flex items-start gap-2">
107
+ {#if diag.severity === 'error'}
108
+ <span class="flex-shrink-0 w-4 h-4 rounded-full bg-accent2/20 text-accent2 flex items-center justify-center text-[10px] mt-0.5">!</span>
109
+ {:else}
110
+ <span class="flex-shrink-0 w-4 h-4 flex items-center justify-center text-amber-500 text-xs mt-0.5">&#x26A0;</span>
92
111
  {/if}
112
+ <span class="font-mono text-xs font-bold text-text1">{diag.title}</span>
93
113
  </div>
94
- {/if}
95
-
96
- <!-- Code fix -->
97
- {#if diag.codeFix}
98
- <div class="pl-6">
99
- <button class="font-mono text-[10px] text-teal hover:underline cursor-pointer"
100
- onclick={() => toggleCodeFix(i)}>
101
- {expandedCodeFix.has(i) ? '- Fix (code)' : '+ Fix (code)'}
102
- </button>
103
- {#if expandedCodeFix.has(i)}
104
- <pre class="mt-1 bg-surface2 border border-border2 rounded p-2 text-[10px] font-mono text-text2 whitespace-pre-wrap break-words">{diag.codeFix}</pre>
105
- {/if}
114
+ <!-- Detail -->
115
+ <div class="font-mono text-[11px] text-text2 leading-relaxed pl-6">{diag.detail}</div>
116
+
117
+ <!-- Quick fix -->
118
+ {#if diag.quickFix}
119
+ <div class="pl-6">
120
+ <button class="font-mono text-[10px] text-accent hover:underline cursor-pointer"
121
+ onclick={() => toggleQuickFix(i)}>
122
+ {expandedQuickFix.has(i) ? '- Quick fix (prompt)' : '+ Quick fix (prompt)'}
123
+ </button>
124
+ {#if expandedQuickFix.has(i)}
125
+ <div class="mt-1 relative">
126
+ <pre class="bg-surface2 border border-border2 rounded p-2 text-[10px] font-mono text-text2 whitespace-pre-wrap break-words">{diag.quickFix}</pre>
127
+ <button class="absolute top-1 right-1 px-1.5 py-0.5 rounded text-[9px] font-mono bg-surface border border-border2 text-text2 hover:text-text1 transition-colors"
128
+ onclick={() => copyText(diag.quickFix!)}>Copier</button>
129
+ </div>
130
+ {/if}
131
+ </div>
132
+ {/if}
133
+
134
+ <!-- Code fix -->
135
+ {#if diag.codeFix}
136
+ <div class="pl-6">
137
+ <button class="font-mono text-[10px] text-teal hover:underline cursor-pointer"
138
+ onclick={() => toggleCodeFix(i)}>
139
+ {expandedCodeFix.has(i) ? '- Fix (code)' : '+ Fix (code)'}
140
+ </button>
141
+ {#if expandedCodeFix.has(i)}
142
+ <pre class="mt-1 bg-surface2 border border-border2 rounded p-2 text-[10px] font-mono text-text2 whitespace-pre-wrap break-words">{diag.codeFix}</pre>
143
+ {/if}
144
+ </div>
145
+ {/if}
146
+ </div>
147
+ {/each}
148
+ {/if}
149
+ </div>
150
+ {/if}
151
+
152
+ <!-- Body: Debug tab -->
153
+ {#if debugInfo && activeTab === 'debug'}
154
+ <div class="flex-1 overflow-y-auto p-4 flex flex-col gap-3">
155
+ <!-- Stats grid -->
156
+ <div class="grid grid-cols-2 gap-x-4 gap-y-1 text-[10px]">
157
+ <span class="text-text2">Prompt tokens (est.)</span>
158
+ <span class="text-text1 font-mono">{debugInfo.estimatedTokens.toLocaleString()}</span>
159
+ <span class="text-text2">Active layers</span>
160
+ <span class="text-text1 font-mono">{debugInfo.layerCount}</span>
161
+ <span class="text-text2">Tools sent</span>
162
+ <span class="text-text1 font-mono">{debugInfo.toolCount} <span class="text-text2">(MCP: {debugInfo.mcpToolCount}, UI: {debugInfo.toolCount - debugInfo.mcpToolCount})</span></span>
163
+ <span class="text-text2">Recipes</span>
164
+ <span class="text-text1 font-mono">{debugInfo.recipeCount}</span>
165
+ </div>
166
+
167
+ <!-- Layers -->
168
+ <div>
169
+ <div class="text-[9px] text-text2 uppercase tracking-wider mb-1">Layers</div>
170
+ <div class="flex flex-col gap-1">
171
+ {#each debugInfo.layers as layer, i}
172
+ <div class="text-[10px] font-mono text-text1 px-2 py-1 bg-surface2/50 rounded">
173
+ [{i}] {layer.protocol}
174
+ {#if layer.serverName}— {layer.serverName}{/if}
175
+ {#if layer.toolCount !== undefined}({layer.toolCount} tools){/if}
106
176
  </div>
107
- {/if}
177
+ {/each}
108
178
  </div>
109
- {/each}
110
- {/if}
111
- </div>
179
+ </div>
180
+
181
+ <!-- System prompt -->
182
+ <div>
183
+ <div class="text-[9px] text-text2 uppercase tracking-wider mb-1">System Prompt</div>
184
+ <pre class="text-[9px] text-text1 bg-surface2/50 rounded p-2 max-h-[300px] overflow-auto whitespace-pre-wrap break-all">{debugInfo.prompt}</pre>
185
+ </div>
186
+ </div>
187
+ {/if}
112
188
  </div>
113
189
  </div>
114
190
  {/if}
@@ -3,8 +3,16 @@
3
3
  import { renderMarkdown } from '../primitives/markdown-renderer.js';
4
4
 
5
5
  interface EphemeralMsg { id: string; role: 'user' | 'assistant'; html: string; }
6
- interface Props { ephemeral: EphemeralMsg[]; }
7
- let { ephemeral }: Props = $props();
6
+ interface Props { ephemeral: EphemeralMsg[]; ondismiss?: () => void; }
7
+ let { ephemeral, ondismiss }: Props = $props();
8
+
9
+ let wrapper: HTMLDivElement | undefined = $state();
10
+
11
+ function onWindowPointerDown(e: PointerEvent) {
12
+ if (!ondismiss || ephemeral.length === 0) return;
13
+ const target = e.target as Node | null;
14
+ if (wrapper && target && !wrapper.contains(target)) ondismiss();
15
+ }
8
16
 
9
17
  // Detect if content has any markdown markers worth parsing.
10
18
  // If not, we skip marked entirely and fall back to {@html} for the
@@ -36,7 +44,9 @@
36
44
  }
37
45
  </script>
38
46
 
39
- <div class="flex flex-col gap-2 items-start w-full">
47
+ <svelte:window onpointerdown={onWindowPointerDown} />
48
+
49
+ <div bind:this={wrapper} class="flex flex-col gap-2 items-start w-full">
40
50
  {#each ephemeral as msg (msg.id)}
41
51
  <div
42
52
  in:fly={{ y: 16, duration: 280, opacity: 0 }}
@@ -0,0 +1,147 @@
1
+ <svelte:options customElement={{ tag: 'auto-mcp-servers-list', shadow: 'none' }} />
2
+
3
+ <script lang="ts">
4
+ interface Server {
5
+ id: string;
6
+ name: string;
7
+ description: string;
8
+ url: string;
9
+ tags?: string[];
10
+ }
11
+
12
+ interface Props {
13
+ servers: Server[];
14
+ connectedUrls?: string[];
15
+ loading?: string[];
16
+ onconnect?: (url: string) => void;
17
+ onconnectall?: () => void;
18
+ ondisconnect?: (url: string) => void;
19
+ recipeCountByServer?: Record<string, number>;
20
+ onrecipeclick?: (url: string) => void;
21
+ toolCountByServer?: Record<string, number>;
22
+ ontoolclick?: (url: string) => void;
23
+ }
24
+
25
+ let {
26
+ servers,
27
+ connectedUrls = [],
28
+ loading = [],
29
+ onconnect,
30
+ onconnectall,
31
+ ondisconnect,
32
+ recipeCountByServer,
33
+ onrecipeclick,
34
+ toolCountByServer,
35
+ ontoolclick,
36
+ }: Props = $props();
37
+
38
+ const allConnected = $derived(
39
+ servers.length > 0 && servers.every(s => connectedUrls.includes(s.url))
40
+ );
41
+ const anyConnected = $derived(
42
+ servers.some(s => connectedUrls.includes(s.url))
43
+ );
44
+
45
+ function isConnected(url: string) {
46
+ return connectedUrls.includes(url);
47
+ }
48
+ function isLoading(url: string) {
49
+ return loading.includes(url);
50
+ }
51
+ </script>
52
+
53
+ <div class="flex flex-col gap-2">
54
+ <span class="text-[9px] font-mono uppercase tracking-wider text-text2">
55
+ Available MCP servers
56
+ </span>
57
+
58
+ <div class="flex flex-col gap-1">
59
+ {#each servers as server (server.id)}
60
+ {@const connected = isConnected(server.url)}
61
+ {@const busy = isLoading(server.url)}
62
+ <div
63
+ class="group flex items-center gap-2 px-2 py-1.5 rounded border border-border2 bg-surface2 hover:border-accent/30 transition-colors"
64
+ >
65
+ <!-- status dot -->
66
+ <div
67
+ class="w-1.5 h-1.5 rounded-full flex-shrink-0 {busy
68
+ ? 'bg-amber animate-pulse'
69
+ : connected
70
+ ? 'bg-teal'
71
+ : 'bg-text2/30'}"
72
+ ></div>
73
+
74
+ <!-- info -->
75
+ <div class="flex-1 min-w-0 flex flex-col">
76
+ <span class="font-mono text-xs font-medium text-text1">{server.name}</span>
77
+ <span class="text-[10px] text-text2 truncate">{server.description}</span>
78
+ {#if connected && (recipeCountByServer?.[server.url] || toolCountByServer?.[server.url])}
79
+ <span class="flex items-center gap-1.5 mt-0.5">
80
+ {#if recipeCountByServer?.[server.url]}
81
+ <button class="text-[10px] font-mono text-accent hover:underline"
82
+ onclick={(e) => { e.stopPropagation(); onrecipeclick?.(server.url); }}>
83
+ {recipeCountByServer[server.url]} recipes
84
+ </button>
85
+ {/if}
86
+ {#if recipeCountByServer?.[server.url] && toolCountByServer?.[server.url]}
87
+ <span class="text-[10px] text-text2">·</span>
88
+ {/if}
89
+ {#if toolCountByServer?.[server.url]}
90
+ <button class="text-[10px] font-mono text-accent hover:underline"
91
+ onclick={(e) => { e.stopPropagation(); ontoolclick?.(server.url); }}>
92
+ {toolCountByServer[server.url]} tools
93
+ </button>
94
+ {/if}
95
+ </span>
96
+ {/if}
97
+ </div>
98
+
99
+ <!-- action -->
100
+ <div class="flex-shrink-0">
101
+ {#if busy}
102
+ <div class="w-4 h-4 border border-accent/50 border-t-accent rounded-full animate-spin"></div>
103
+ {:else if connected}
104
+ <button
105
+ onclick={() => ondisconnect?.(server.url)}
106
+ class="text-xs font-mono px-1.5 h-6 rounded text-teal group-hover:text-accent2 transition-colors"
107
+ title="Disconnect"
108
+ >
109
+ <span class="group-hover:hidden">&#10003;</span>
110
+ <span class="hidden group-hover:inline text-accent2">&#215;</span>
111
+ </button>
112
+ {:else}
113
+ <button
114
+ onclick={() => onconnect?.(server.url)}
115
+ class="text-[10px] font-mono px-1.5 h-6 rounded border border-border2 bg-surface2 hover:border-accent/50 hover:text-accent text-text2 transition-colors"
116
+ >
117
+ connect
118
+ </button>
119
+ {/if}
120
+ </div>
121
+ </div>
122
+ {/each}
123
+ </div>
124
+
125
+ <!-- bottom actions -->
126
+ <div class="flex items-center gap-2 mt-1">
127
+ <button
128
+ onclick={onconnectall}
129
+ disabled={allConnected}
130
+ class="text-xs font-mono px-2 h-7 rounded border border-accent/40 bg-accent/10 text-accent hover:bg-accent/20 transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
131
+ >
132
+ Load all
133
+ </button>
134
+ {#if anyConnected}
135
+ <button
136
+ onclick={() => {
137
+ for (const s of servers) {
138
+ if (isConnected(s.url)) ondisconnect?.(s.url);
139
+ }
140
+ }}
141
+ class="text-xs font-mono px-2 h-7 rounded border border-border2 bg-surface2 hover:border-accent2/50 hover:text-accent2 text-text2 transition-colors"
142
+ >
143
+ Disconnect all
144
+ </button>
145
+ {/if}
146
+ </div>
147
+ </div>
@@ -34,10 +34,15 @@
34
34
  }: Props = $props();
35
35
 
36
36
  let debounceTimer: ReturnType<typeof setTimeout> | null = null;
37
+ let urlChangeTimer: ReturnType<typeof setTimeout> | null = null;
37
38
 
38
39
  function handleUrlInput(e: Event) {
39
40
  const v = (e.target as HTMLInputElement).value;
40
- onurlchange?.(v);
41
+ // Debounce onurlchange so we don't pollute the canvas store with every keystroke.
42
+ if (urlChangeTimer) clearTimeout(urlChangeTimer);
43
+ urlChangeTimer = setTimeout(() => {
44
+ onurlchange?.(v);
45
+ }, 400);
41
46
  if (debounceTimer) clearTimeout(debounceTimer);
42
47
  debounceTimer = setTimeout(() => {
43
48
  if (v.startsWith('http') && !connected && !connecting) {
@@ -48,7 +53,11 @@
48
53
 
49
54
  function handleKeydown(e: KeyboardEvent) {
50
55
  if (e.key === 'Enter') {
56
+ if (urlChangeTimer) { clearTimeout(urlChangeTimer); urlChangeTimer = null; }
51
57
  if (debounceTimer) clearTimeout(debounceTimer);
58
+ // Flush current URL immediately before connecting.
59
+ const v = (e.target as HTMLInputElement).value;
60
+ onurlchange?.(v);
52
61
  onconnect?.();
53
62
  }
54
63
  }