@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.
- package/package.json +15 -2
- package/src/agent/DiagnosticModal.svelte +126 -50
- package/src/agent/EphemeralBubble.svelte +13 -3
- package/src/agent/MCPserversList.svelte +147 -0
- package/src/agent/McpConnector.svelte +10 -1
- package/src/agent/RecipeBrowser.svelte +384 -0
- package/src/agent/RemoteMCPserversDemo.svelte +5 -121
- package/src/agent/ToolBrowser.svelte +133 -0
- package/src/agent/WebMCPserversList.svelte +2 -0
- package/src/agent/useAgentLoop.svelte.ts +396 -0
- package/src/base/chat-inline.svelte +64 -0
- package/src/base/dialog-content.svelte +3 -1
- package/src/components/HeaderControls.svelte +78 -0
- package/src/index.ts +13 -35
- package/src/stores/canvas.svelte.ts +0 -6
- package/src/widgets/SafeImage.svelte +67 -0
- package/src/widgets/WidgetRenderer.svelte +153 -78
- package/src/widgets/notebook/executors/index.ts +0 -1
- package/src/widgets/notebook/executors/sql.ts +32 -182
- package/src/widgets/notebook/import-modal-api.ts +237 -0
- package/src/widgets/notebook/import-modal.svelte +738 -0
- package/src/widgets/notebook/left-pane.ts +1 -1
- package/src/widgets/notebook/notebook.svelte +75 -0
- package/src/widgets/notebook/notebook.ts +38 -73
- package/src/widgets/notebook/prose.ts +6 -3
- package/src/widgets/notebook/shared.ts +68 -49
- package/src/widgets/rich/cards.svelte +74 -0
- package/src/widgets/rich/carousel.svelte +126 -0
- package/src/widgets/rich/chart-rich.svelte +221 -0
- package/src/widgets/rich/chat-input.svelte +52 -0
- package/src/widgets/rich/data-table.svelte +132 -0
- package/src/widgets/rich/gallery.svelte +115 -0
- package/src/widgets/rich/grid-data.svelte +85 -0
- package/src/widgets/rich/hemicycle.svelte +95 -0
- package/src/widgets/rich/js-sandbox.svelte +67 -0
- package/src/widgets/rich/json-viewer.svelte +82 -0
- package/src/widgets/rich/log.svelte +62 -0
- package/src/widgets/rich/profile.svelte +91 -0
- package/src/widgets/rich/sankey.svelte +73 -0
- package/src/widgets/rich/stat-card.svelte +60 -0
- package/src/widgets/rich/timeline.svelte +95 -0
- package/src/widgets/rich/trombinoscope.svelte +87 -0
- package/src/widgets/simple/actions.svelte +36 -0
- package/src/widgets/simple/alert.svelte +52 -0
- package/src/widgets/simple/chart.svelte +38 -0
- package/src/widgets/simple/code.svelte +30 -0
- package/src/widgets/simple/kv.svelte +31 -0
- package/src/widgets/simple/list.svelte +35 -0
- package/src/widgets/simple/stat.svelte +36 -0
- package/src/widgets/simple/tags.svelte +34 -0
- package/src/widgets/simple/text.svelte +130 -0
- package/src/widgets/helpers/safe-image.ts +0 -78
- package/src/widgets/notebook/import-modals.ts +0 -560
- package/src/widgets/notebook/recipe-browser.ts +0 -350
- package/src/widgets/rich/cards.ts +0 -181
- package/src/widgets/rich/carousel.ts +0 -319
- package/src/widgets/rich/chart-rich.ts +0 -386
- package/src/widgets/rich/d3.ts +0 -503
- package/src/widgets/rich/data-table.ts +0 -342
- package/src/widgets/rich/gallery.ts +0 -350
- package/src/widgets/rich/grid-data.ts +0 -173
- package/src/widgets/rich/hemicycle.ts +0 -313
- package/src/widgets/rich/js-sandbox.ts +0 -122
- package/src/widgets/rich/json-viewer.ts +0 -202
- package/src/widgets/rich/log.ts +0 -143
- package/src/widgets/rich/map.ts +0 -218
- package/src/widgets/rich/profile.ts +0 -256
- package/src/widgets/rich/sankey.ts +0 -257
- package/src/widgets/rich/stat-card.ts +0 -125
- package/src/widgets/rich/timeline.ts +0 -179
- package/src/widgets/rich/trombinoscope.ts +0 -246
- package/src/widgets/simple/actions.ts +0 -89
- package/src/widgets/simple/alert.ts +0 -100
- package/src/widgets/simple/chart.ts +0 -189
- package/src/widgets/simple/code.ts +0 -79
- package/src/widgets/simple/kv.ts +0 -68
- package/src/widgets/simple/list.ts +0 -89
- package/src/widgets/simple/stat.ts +0 -58
- package/src/widgets/simple/tags.ts +0 -125
- package/src/widgets/simple/text.ts +0 -198
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
<svelte:options customElement={{ tag: 'auto-recipe-browser', shadow: 'none' }} />
|
|
2
|
+
|
|
3
|
+
<script lang="ts">
|
|
4
|
+
import { fade, fly } from 'svelte/transition';
|
|
5
|
+
import { filterRecipes, sortRecipes, recipeToMarkdown, recipeToDownloadBlob } from '@webmcp-auto-ui/agent';
|
|
6
|
+
import { encode } from '@webmcp-auto-ui/sdk';
|
|
7
|
+
import { extractCellsFromRecipe } from '@webmcp-auto-ui/ui';
|
|
8
|
+
import type { McpMultiClient } from '@webmcp-auto-ui/core';
|
|
9
|
+
import type { RecipeData } from '@webmcp-auto-ui/sdk';
|
|
10
|
+
|
|
11
|
+
interface RecipeItem {
|
|
12
|
+
name: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
body?: string;
|
|
15
|
+
when?: string;
|
|
16
|
+
components_used?: string[];
|
|
17
|
+
servers?: string[];
|
|
18
|
+
layout?: { type: string; columns?: number; arrangement?: string };
|
|
19
|
+
originalName?: string;
|
|
20
|
+
serverUrl?: string;
|
|
21
|
+
server?: string;
|
|
22
|
+
serverName?: string;
|
|
23
|
+
id?: string;
|
|
24
|
+
[key: string]: unknown;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface Props {
|
|
28
|
+
open: boolean;
|
|
29
|
+
mcpRecipes: RecipeItem[];
|
|
30
|
+
webmcpRecipes: RecipeItem[];
|
|
31
|
+
initialFilter?: string;
|
|
32
|
+
/** Layout toggle: list (default) or grid */
|
|
33
|
+
layout?: 'list' | 'grid';
|
|
34
|
+
onOpenInNotebook?: (type: string, data: Record<string, unknown>) => void;
|
|
35
|
+
/** Called when user clicks a recipe to view its detail. Host shows the recipe modal. */
|
|
36
|
+
onOpenRecipe?: (recipe: RecipeItem) => void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let {
|
|
40
|
+
open = $bindable(false),
|
|
41
|
+
mcpRecipes = [],
|
|
42
|
+
webmcpRecipes = [],
|
|
43
|
+
initialFilter = '',
|
|
44
|
+
layout: initialLayout = 'list',
|
|
45
|
+
onOpenInNotebook,
|
|
46
|
+
onOpenRecipe,
|
|
47
|
+
}: Props = $props();
|
|
48
|
+
|
|
49
|
+
function getMultiClient(): McpMultiClient | undefined {
|
|
50
|
+
return (globalThis as unknown as { __multiMcp?: { multiClient: McpMultiClient } }).__multiMcp?.multiClient;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let query = $state('');
|
|
54
|
+
/** Kind filter: 'all' | 'mcp' | 'webmcp' — ported from vanilla recipe-browser */
|
|
55
|
+
let kind = $state<'all' | 'mcp' | 'webmcp'>('all');
|
|
56
|
+
/** Layout toggle — ported from vanilla recipe-browser */
|
|
57
|
+
let layout = $state<'list' | 'grid'>(initialLayout);
|
|
58
|
+
let selected = $state<RecipeItem | null>(null);
|
|
59
|
+
let mcpCollapsed = $state(false);
|
|
60
|
+
let webmcpCollapsed = $state(false);
|
|
61
|
+
let copyState = $state<'idle' | 'copied'>('idle');
|
|
62
|
+
let copyTimer: ReturnType<typeof setTimeout> | undefined;
|
|
63
|
+
|
|
64
|
+
// Sync initialFilter into search when modal opens
|
|
65
|
+
$effect(() => {
|
|
66
|
+
if (open) {
|
|
67
|
+
query = initialFilter ?? '';
|
|
68
|
+
kind = 'all';
|
|
69
|
+
selected = null;
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
function applyKindFilter(recipes: RecipeItem[], filterKind: 'all' | 'mcp' | 'webmcp'): RecipeItem[] {
|
|
74
|
+
if (filterKind === 'all') return recipes;
|
|
75
|
+
return recipes.filter((_r) => {
|
|
76
|
+
// webmcpRecipes are already separated — use the array membership to determine kind
|
|
77
|
+
return filterKind === 'webmcp' ? webmcpRecipes.includes(_r) : mcpRecipes.includes(_r);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const filteredMcp = $derived(
|
|
82
|
+
kind === 'webmcp' ? [] : sortRecipes(filterRecipes(mcpRecipes, query))
|
|
83
|
+
);
|
|
84
|
+
const filteredWebmcp = $derived(
|
|
85
|
+
kind === 'mcp' ? [] : sortRecipes(filterRecipes(webmcpRecipes, query))
|
|
86
|
+
);
|
|
87
|
+
const totalResults = $derived(filteredMcp.length + filteredWebmcp.length);
|
|
88
|
+
|
|
89
|
+
function close() { open = false; selected = null; }
|
|
90
|
+
|
|
91
|
+
function onKeydown(e: KeyboardEvent) {
|
|
92
|
+
if (e.key === 'Escape') close();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function openRecipe(recipe: RecipeItem) {
|
|
96
|
+
await ensureBody(recipe);
|
|
97
|
+
selected = recipe;
|
|
98
|
+
onOpenRecipe?.(recipe);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function downloadRecipe(recipe: RecipeItem) {
|
|
102
|
+
const { blob, filename } = recipeToDownloadBlob(recipe as Record<string, unknown>);
|
|
103
|
+
const url = URL.createObjectURL(blob);
|
|
104
|
+
const a = document.createElement('a');
|
|
105
|
+
a.href = url;
|
|
106
|
+
a.download = filename;
|
|
107
|
+
document.body.appendChild(a);
|
|
108
|
+
a.click();
|
|
109
|
+
document.body.removeChild(a);
|
|
110
|
+
URL.revokeObjectURL(url);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function ensureBody(recipe: RecipeItem) {
|
|
114
|
+
const multiClient = getMultiClient();
|
|
115
|
+
if (recipe.body || !recipe.serverUrl || !multiClient) return;
|
|
116
|
+
try {
|
|
117
|
+
const identifier = recipe.originalName ?? recipe.name;
|
|
118
|
+
const res = await multiClient.callToolOn(recipe.serverUrl, 'get_recipe', {
|
|
119
|
+
name: identifier,
|
|
120
|
+
id: (recipe as any).id ?? identifier,
|
|
121
|
+
});
|
|
122
|
+
const text = res.content?.find((c: { type: string }) => c.type === 'text') as { text?: string } | undefined;
|
|
123
|
+
if (text?.text) {
|
|
124
|
+
let body = text.text;
|
|
125
|
+
try {
|
|
126
|
+
const parsed = JSON.parse(body);
|
|
127
|
+
if (parsed && typeof parsed === 'object' && typeof parsed.content === 'string') {
|
|
128
|
+
body = parsed.content;
|
|
129
|
+
}
|
|
130
|
+
} catch { /* not JSON — keep raw text */ }
|
|
131
|
+
recipe.body = body;
|
|
132
|
+
}
|
|
133
|
+
} catch (err) {
|
|
134
|
+
console.warn('[RecipeBrowser] get_recipe failed:', err);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function openInNotebook(recipe: RecipeItem) {
|
|
139
|
+
await ensureBody(recipe);
|
|
140
|
+
const body = recipe.body ?? '';
|
|
141
|
+
|
|
142
|
+
const cells = extractCellsFromRecipe(body, {
|
|
143
|
+
title: recipe.name,
|
|
144
|
+
description: recipe.description,
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const connected = getMultiClient()?.listServers() ?? [];
|
|
148
|
+
const serverNames = Array.isArray(recipe.servers) ? recipe.servers : [];
|
|
149
|
+
const servers = serverNames
|
|
150
|
+
.map((name) => {
|
|
151
|
+
const hit = connected.find((s) => s.name === name);
|
|
152
|
+
const url = hit?.url ?? (typeof recipe.serverUrl === 'string' ? recipe.serverUrl : undefined);
|
|
153
|
+
return url ? { name, url, kind: 'data' as const } : null;
|
|
154
|
+
})
|
|
155
|
+
.filter((s): s is { name: string; url: string; kind: 'data' } => s !== null);
|
|
156
|
+
|
|
157
|
+
const data: Record<string, unknown> = {
|
|
158
|
+
title: recipe.name,
|
|
159
|
+
cells,
|
|
160
|
+
mode: 'edit',
|
|
161
|
+
servers,
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
onOpenInNotebook?.('notebook', data);
|
|
165
|
+
close();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function copyHyperSkillUrl(recipe: RecipeItem) {
|
|
169
|
+
try {
|
|
170
|
+
const md = recipeToMarkdown(recipe as Record<string, unknown>);
|
|
171
|
+
const hsUrl = await encode(window.location.origin, md);
|
|
172
|
+
await navigator.clipboard.writeText(hsUrl);
|
|
173
|
+
copyState = 'copied';
|
|
174
|
+
if (copyTimer) clearTimeout(copyTimer);
|
|
175
|
+
copyTimer = setTimeout(() => { copyState = 'idle'; }, 2000);
|
|
176
|
+
} catch (err) {
|
|
177
|
+
console.error('Failed to copy HyperSkill URL:', err);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
</script>
|
|
181
|
+
|
|
182
|
+
<svelte:window onkeydown={onKeydown} />
|
|
183
|
+
|
|
184
|
+
{#if open}
|
|
185
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
186
|
+
<div
|
|
187
|
+
class="fixed inset-0 bg-black/60 z-50 flex items-center justify-center p-6"
|
|
188
|
+
transition:fade={{ duration: 180 }}
|
|
189
|
+
onclick={(e) => { if (e.target === e.currentTarget) close(); }}
|
|
190
|
+
>
|
|
191
|
+
<div
|
|
192
|
+
class="w-full max-w-2xl max-h-[85vh] bg-surface border border-border2 rounded-2xl flex flex-col shadow-2xl overflow-hidden"
|
|
193
|
+
transition:fly={{ y: 24, duration: 240 }}
|
|
194
|
+
>
|
|
195
|
+
<!-- Header -->
|
|
196
|
+
<div class="flex items-center gap-4 px-6 py-4 border-b border-border flex-shrink-0">
|
|
197
|
+
<span class="font-mono text-sm font-bold text-text1 flex-1">Recipe Browser</span>
|
|
198
|
+
<button class="text-text2 hover:text-text1 font-mono text-base leading-none transition-colors"
|
|
199
|
+
onclick={close}>x</button>
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
<!-- Toolbar: search + kind filter + layout toggle -->
|
|
203
|
+
<div class="px-6 pt-4 pb-2 flex-shrink-0 flex items-center gap-2">
|
|
204
|
+
<input
|
|
205
|
+
type="text"
|
|
206
|
+
bind:value={query}
|
|
207
|
+
placeholder="Search recipes..."
|
|
208
|
+
class="font-mono text-xs h-8 px-3 rounded-lg border border-border2 bg-surface2 text-text1 flex-1 placeholder:text-text2/40 focus:outline-none focus:border-accent/50 transition-colors"
|
|
209
|
+
/>
|
|
210
|
+
|
|
211
|
+
<!-- Kind filter — ported from vanilla recipe-browser -->
|
|
212
|
+
<select
|
|
213
|
+
bind:value={kind}
|
|
214
|
+
class="font-mono text-xs h-8 px-2 rounded-lg border border-border2 bg-surface2 text-text1 focus:outline-none focus:border-accent/50 transition-colors"
|
|
215
|
+
>
|
|
216
|
+
<option value="all">All kinds</option>
|
|
217
|
+
<option value="webmcp">WebMCP</option>
|
|
218
|
+
<option value="mcp">MCP</option>
|
|
219
|
+
</select>
|
|
220
|
+
|
|
221
|
+
<!-- Layout toggle — ported from vanilla recipe-browser -->
|
|
222
|
+
<div class="flex rounded-lg border border-border2 overflow-hidden">
|
|
223
|
+
<button
|
|
224
|
+
type="button"
|
|
225
|
+
onclick={() => layout = 'list'}
|
|
226
|
+
class="font-mono text-xs px-2 h-8 transition-colors {layout === 'list' ? 'bg-accent text-white' : 'bg-surface2 text-text2 hover:text-text1'}"
|
|
227
|
+
title="List view"
|
|
228
|
+
>list</button>
|
|
229
|
+
<button
|
|
230
|
+
type="button"
|
|
231
|
+
onclick={() => layout = 'grid'}
|
|
232
|
+
class="font-mono text-xs px-2 h-8 transition-colors {layout === 'grid' ? 'bg-accent text-white' : 'bg-surface2 text-text2 hover:text-text1'}"
|
|
233
|
+
title="Grid view"
|
|
234
|
+
>grid</button>
|
|
235
|
+
</div>
|
|
236
|
+
</div>
|
|
237
|
+
|
|
238
|
+
<!-- Recipe lists -->
|
|
239
|
+
<div class="flex-1 overflow-y-auto px-6 pb-5 flex flex-col gap-3">
|
|
240
|
+
|
|
241
|
+
{#if totalResults === 0}
|
|
242
|
+
<div class="flex items-center justify-center py-12">
|
|
243
|
+
<span class="font-mono text-xs text-text2">No recipes found</span>
|
|
244
|
+
</div>
|
|
245
|
+
{:else}
|
|
246
|
+
|
|
247
|
+
<!-- Grid layout merges both lists -->
|
|
248
|
+
{#if layout === 'grid'}
|
|
249
|
+
<div class="grid grid-cols-2 gap-2 mt-2">
|
|
250
|
+
{#each [...filteredMcp, ...filteredWebmcp] as recipe, i (`grid:${recipe.name}:${i}`)}
|
|
251
|
+
{@const isWebmcp = webmcpRecipes.includes(recipe)}
|
|
252
|
+
<div class="group flex flex-col gap-1 p-3 bg-surface2/50 rounded-lg hover:bg-surface2 transition-colors cursor-pointer border border-border2/50"
|
|
253
|
+
onclick={() => openRecipe(recipe)}>
|
|
254
|
+
<div class="font-mono text-[11px] text-text1 font-medium truncate">{recipe.name}</div>
|
|
255
|
+
{#if recipe.description}
|
|
256
|
+
<div class="font-mono text-[9px] text-text2 line-clamp-2">{recipe.description}</div>
|
|
257
|
+
{/if}
|
|
258
|
+
<div class="flex items-center gap-1 mt-auto pt-1">
|
|
259
|
+
<span class="font-mono text-[9px] px-1.5 py-0.5 rounded-full border {isWebmcp ? 'border-teal/40 text-teal' : 'border-accent/40 text-accent'}">
|
|
260
|
+
{isWebmcp ? 'webmcp' : 'mcp'}
|
|
261
|
+
</span>
|
|
262
|
+
</div>
|
|
263
|
+
<div class="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
264
|
+
<button
|
|
265
|
+
title="Download .md"
|
|
266
|
+
class="font-mono text-[10px] h-5 px-1.5 rounded border border-border2 text-text2 hover:text-text1 transition-colors"
|
|
267
|
+
onclick={(e) => { e.stopPropagation(); downloadRecipe(recipe); }}
|
|
268
|
+
>.md</button>
|
|
269
|
+
<button
|
|
270
|
+
title="Open in notebook"
|
|
271
|
+
class="font-mono text-[10px] h-5 px-1.5 rounded border border-border2 text-text2 hover:text-text1 transition-colors"
|
|
272
|
+
onclick={(e) => { e.stopPropagation(); openInNotebook(recipe); }}
|
|
273
|
+
>nb</button>
|
|
274
|
+
<button
|
|
275
|
+
title="Copy HyperSkill URL"
|
|
276
|
+
class="font-mono text-[10px] h-5 px-1.5 rounded border transition-colors {copyState === 'copied' ? 'border-teal/40 text-teal' : 'border-accent/40 text-accent hover:bg-accent/10'}"
|
|
277
|
+
onclick={(e) => { e.stopPropagation(); copyHyperSkillUrl(recipe); }}
|
|
278
|
+
>{copyState === 'copied' ? '✓' : 'hs'}</button>
|
|
279
|
+
</div>
|
|
280
|
+
</div>
|
|
281
|
+
{/each}
|
|
282
|
+
</div>
|
|
283
|
+
|
|
284
|
+
{:else}
|
|
285
|
+
<!-- List layout (default) -->
|
|
286
|
+
|
|
287
|
+
<!-- MCP Recipes -->
|
|
288
|
+
{#if filteredMcp.length > 0}
|
|
289
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
290
|
+
<div
|
|
291
|
+
class="flex items-center gap-1 cursor-pointer select-none mt-2"
|
|
292
|
+
onclick={() => mcpCollapsed = !mcpCollapsed}
|
|
293
|
+
>
|
|
294
|
+
<span class="text-[10px] font-mono text-text2 uppercase tracking-wider">MCP Recipes ({filteredMcp.length})</span>
|
|
295
|
+
<span class="text-[10px] text-text2 ml-auto transition-transform {mcpCollapsed ? '' : 'rotate-90'}">{@html '▶'}</span>
|
|
296
|
+
</div>
|
|
297
|
+
{#if !mcpCollapsed}
|
|
298
|
+
<div class="flex flex-col gap-1">
|
|
299
|
+
{#each filteredMcp as recipe, i (`mcp:${recipe.name}:${i}`)}
|
|
300
|
+
<div class="group flex items-center gap-2 px-3 py-2 bg-surface2/50 rounded-lg hover:bg-surface2 transition-colors">
|
|
301
|
+
<button
|
|
302
|
+
class="flex-1 min-w-0 text-left cursor-pointer"
|
|
303
|
+
onclick={() => openRecipe(recipe)}
|
|
304
|
+
>
|
|
305
|
+
<div class="font-mono text-[11px] text-text1 font-medium truncate">{recipe.name}</div>
|
|
306
|
+
{#if recipe.description}
|
|
307
|
+
<div class="font-mono text-[9px] text-text2 line-clamp-1 mt-0.5">{recipe.description}</div>
|
|
308
|
+
{/if}
|
|
309
|
+
</button>
|
|
310
|
+
<div class="flex items-center gap-1 flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
311
|
+
<button
|
|
312
|
+
title="Download .md"
|
|
313
|
+
class="font-mono text-[10px] h-6 px-2 rounded border border-border2 text-text2 hover:text-text1 transition-colors"
|
|
314
|
+
onclick={(e) => { e.stopPropagation(); downloadRecipe(recipe); }}
|
|
315
|
+
>.md</button>
|
|
316
|
+
<button
|
|
317
|
+
title="Open in notebook"
|
|
318
|
+
class="font-mono text-[10px] h-6 px-2 rounded border border-border2 text-text2 hover:text-text1 transition-colors"
|
|
319
|
+
onclick={(e) => { e.stopPropagation(); openInNotebook(recipe); }}
|
|
320
|
+
>nb</button>
|
|
321
|
+
<button
|
|
322
|
+
title="Copy HyperSkill URL"
|
|
323
|
+
class="font-mono text-[10px] h-6 px-2 rounded border transition-colors {copyState === 'copied' ? 'border-teal/40 text-teal' : 'border-accent/40 text-accent hover:bg-accent/10'}"
|
|
324
|
+
onclick={(e) => { e.stopPropagation(); copyHyperSkillUrl(recipe); }}
|
|
325
|
+
>{copyState === 'copied' ? '✓' : 'hs'}</button>
|
|
326
|
+
</div>
|
|
327
|
+
</div>
|
|
328
|
+
{/each}
|
|
329
|
+
</div>
|
|
330
|
+
{/if}
|
|
331
|
+
{/if}
|
|
332
|
+
|
|
333
|
+
<!-- WebMCP Recipes -->
|
|
334
|
+
{#if filteredWebmcp.length > 0}
|
|
335
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
336
|
+
<div
|
|
337
|
+
class="flex items-center gap-1 cursor-pointer select-none mt-2"
|
|
338
|
+
onclick={() => webmcpCollapsed = !webmcpCollapsed}
|
|
339
|
+
>
|
|
340
|
+
<span class="text-[10px] font-mono text-text2 uppercase tracking-wider">WebMCP Recipes ({filteredWebmcp.length})</span>
|
|
341
|
+
<span class="text-[10px] text-text2 ml-auto transition-transform {webmcpCollapsed ? '' : 'rotate-90'}">{@html '▶'}</span>
|
|
342
|
+
</div>
|
|
343
|
+
{#if !webmcpCollapsed}
|
|
344
|
+
<div class="flex flex-col gap-1">
|
|
345
|
+
{#each filteredWebmcp as recipe, i (`webmcp:${recipe.name}:${i}`)}
|
|
346
|
+
<div class="group flex items-center gap-2 px-3 py-2 bg-surface2/50 rounded-lg hover:bg-surface2 transition-colors">
|
|
347
|
+
<button
|
|
348
|
+
class="flex-1 min-w-0 text-left cursor-pointer"
|
|
349
|
+
onclick={() => openRecipe(recipe)}
|
|
350
|
+
>
|
|
351
|
+
<div class="font-mono text-[11px] text-text1 font-medium truncate">{recipe.name}</div>
|
|
352
|
+
{#if recipe.description}
|
|
353
|
+
<div class="font-mono text-[9px] text-text2 line-clamp-1 mt-0.5">{recipe.description}</div>
|
|
354
|
+
{/if}
|
|
355
|
+
</button>
|
|
356
|
+
<div class="flex items-center gap-1 flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity">
|
|
357
|
+
<button
|
|
358
|
+
title="Download .md"
|
|
359
|
+
class="font-mono text-[10px] h-6 px-2 rounded border border-border2 text-text2 hover:text-text1 transition-colors"
|
|
360
|
+
onclick={(e) => { e.stopPropagation(); downloadRecipe(recipe); }}
|
|
361
|
+
>.md</button>
|
|
362
|
+
<button
|
|
363
|
+
title="Open in notebook"
|
|
364
|
+
class="font-mono text-[10px] h-6 px-2 rounded border border-border2 text-text2 hover:text-text1 transition-colors"
|
|
365
|
+
onclick={(e) => { e.stopPropagation(); openInNotebook(recipe); }}
|
|
366
|
+
>nb</button>
|
|
367
|
+
<button
|
|
368
|
+
title="Copy HyperSkill URL"
|
|
369
|
+
class="font-mono text-[10px] h-6 px-2 rounded border transition-colors {copyState === 'copied' ? 'border-teal/40 text-teal' : 'border-accent/40 text-accent hover:bg-accent/10'}"
|
|
370
|
+
onclick={(e) => { e.stopPropagation(); copyHyperSkillUrl(recipe); }}
|
|
371
|
+
>{copyState === 'copied' ? '✓' : 'hs'}</button>
|
|
372
|
+
</div>
|
|
373
|
+
</div>
|
|
374
|
+
{/each}
|
|
375
|
+
</div>
|
|
376
|
+
{/if}
|
|
377
|
+
{/if}
|
|
378
|
+
{/if}
|
|
379
|
+
|
|
380
|
+
{/if}
|
|
381
|
+
</div>
|
|
382
|
+
</div>
|
|
383
|
+
</div>
|
|
384
|
+
{/if}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
<!-- Backward-compat shim for Track F rename. Remove after index.ts is updated to MCPserversList. -->
|
|
1
2
|
<script lang="ts">
|
|
3
|
+
import MCPserversList from './MCPserversList.svelte';
|
|
4
|
+
|
|
2
5
|
interface Server {
|
|
3
6
|
id: string;
|
|
4
7
|
name: string;
|
|
@@ -20,126 +23,7 @@
|
|
|
20
23
|
ontoolclick?: (url: string) => void;
|
|
21
24
|
}
|
|
22
25
|
|
|
23
|
-
let
|
|
24
|
-
servers,
|
|
25
|
-
connectedUrls = [],
|
|
26
|
-
loading = [],
|
|
27
|
-
onconnect,
|
|
28
|
-
onconnectall,
|
|
29
|
-
ondisconnect,
|
|
30
|
-
recipeCountByServer,
|
|
31
|
-
onrecipeclick,
|
|
32
|
-
toolCountByServer,
|
|
33
|
-
ontoolclick,
|
|
34
|
-
}: Props = $props();
|
|
35
|
-
|
|
36
|
-
const allConnected = $derived(
|
|
37
|
-
servers.length > 0 && servers.every(s => connectedUrls.includes(s.url))
|
|
38
|
-
);
|
|
39
|
-
const anyConnected = $derived(
|
|
40
|
-
servers.some(s => connectedUrls.includes(s.url))
|
|
41
|
-
);
|
|
42
|
-
|
|
43
|
-
function isConnected(url: string) {
|
|
44
|
-
return connectedUrls.includes(url);
|
|
45
|
-
}
|
|
46
|
-
function isLoading(url: string) {
|
|
47
|
-
return loading.includes(url);
|
|
48
|
-
}
|
|
26
|
+
let props: Props = $props();
|
|
49
27
|
</script>
|
|
50
28
|
|
|
51
|
-
<
|
|
52
|
-
<span class="text-[9px] font-mono uppercase tracking-wider text-text2">
|
|
53
|
-
Available MCP servers
|
|
54
|
-
</span>
|
|
55
|
-
|
|
56
|
-
<div class="flex flex-col gap-1">
|
|
57
|
-
{#each servers as server (server.id)}
|
|
58
|
-
{@const connected = isConnected(server.url)}
|
|
59
|
-
{@const busy = isLoading(server.url)}
|
|
60
|
-
<div
|
|
61
|
-
class="group flex items-center gap-2 px-2 py-1.5 rounded border border-border2 bg-surface2 hover:border-accent/30 transition-colors"
|
|
62
|
-
>
|
|
63
|
-
<!-- status dot -->
|
|
64
|
-
<div
|
|
65
|
-
class="w-1.5 h-1.5 rounded-full flex-shrink-0 {busy
|
|
66
|
-
? 'bg-amber animate-pulse'
|
|
67
|
-
: connected
|
|
68
|
-
? 'bg-teal'
|
|
69
|
-
: 'bg-text2/30'}"
|
|
70
|
-
></div>
|
|
71
|
-
|
|
72
|
-
<!-- info -->
|
|
73
|
-
<div class="flex-1 min-w-0 flex flex-col">
|
|
74
|
-
<span class="font-mono text-xs font-medium text-text1">{server.name}</span>
|
|
75
|
-
<span class="text-[10px] text-text2 truncate">{server.description}</span>
|
|
76
|
-
{#if connected && (recipeCountByServer?.[server.url] || toolCountByServer?.[server.url])}
|
|
77
|
-
<span class="flex items-center gap-1.5 mt-0.5">
|
|
78
|
-
{#if recipeCountByServer?.[server.url]}
|
|
79
|
-
<button class="text-[10px] font-mono text-accent hover:underline"
|
|
80
|
-
onclick={(e) => { e.stopPropagation(); onrecipeclick?.(server.url); }}>
|
|
81
|
-
{recipeCountByServer[server.url]} recipes
|
|
82
|
-
</button>
|
|
83
|
-
{/if}
|
|
84
|
-
{#if recipeCountByServer?.[server.url] && toolCountByServer?.[server.url]}
|
|
85
|
-
<span class="text-[10px] text-text2">·</span>
|
|
86
|
-
{/if}
|
|
87
|
-
{#if toolCountByServer?.[server.url]}
|
|
88
|
-
<button class="text-[10px] font-mono text-accent hover:underline"
|
|
89
|
-
onclick={(e) => { e.stopPropagation(); ontoolclick?.(server.url); }}>
|
|
90
|
-
{toolCountByServer[server.url]} tools
|
|
91
|
-
</button>
|
|
92
|
-
{/if}
|
|
93
|
-
</span>
|
|
94
|
-
{/if}
|
|
95
|
-
</div>
|
|
96
|
-
|
|
97
|
-
<!-- action -->
|
|
98
|
-
<div class="flex-shrink-0">
|
|
99
|
-
{#if busy}
|
|
100
|
-
<div class="w-4 h-4 border border-accent/50 border-t-accent rounded-full animate-spin"></div>
|
|
101
|
-
{:else if connected}
|
|
102
|
-
<button
|
|
103
|
-
onclick={() => ondisconnect?.(server.url)}
|
|
104
|
-
class="text-xs font-mono px-1.5 h-6 rounded text-teal group-hover:text-accent2 transition-colors"
|
|
105
|
-
title="Disconnect"
|
|
106
|
-
>
|
|
107
|
-
<span class="group-hover:hidden">✓</span>
|
|
108
|
-
<span class="hidden group-hover:inline text-accent2">×</span>
|
|
109
|
-
</button>
|
|
110
|
-
{:else}
|
|
111
|
-
<button
|
|
112
|
-
onclick={() => onconnect?.(server.url)}
|
|
113
|
-
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"
|
|
114
|
-
>
|
|
115
|
-
connect
|
|
116
|
-
</button>
|
|
117
|
-
{/if}
|
|
118
|
-
</div>
|
|
119
|
-
</div>
|
|
120
|
-
{/each}
|
|
121
|
-
</div>
|
|
122
|
-
|
|
123
|
-
<!-- bottom actions -->
|
|
124
|
-
<div class="flex items-center gap-2 mt-1">
|
|
125
|
-
<button
|
|
126
|
-
onclick={onconnectall}
|
|
127
|
-
disabled={allConnected}
|
|
128
|
-
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"
|
|
129
|
-
>
|
|
130
|
-
Load all
|
|
131
|
-
</button>
|
|
132
|
-
{#if anyConnected}
|
|
133
|
-
<button
|
|
134
|
-
onclick={() => {
|
|
135
|
-
for (const s of servers) {
|
|
136
|
-
if (isConnected(s.url)) ondisconnect?.(s.url);
|
|
137
|
-
}
|
|
138
|
-
}}
|
|
139
|
-
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"
|
|
140
|
-
>
|
|
141
|
-
Disconnect all
|
|
142
|
-
</button>
|
|
143
|
-
{/if}
|
|
144
|
-
</div>
|
|
145
|
-
</div>
|
|
29
|
+
<MCPserversList {...props} />
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
<svelte:options customElement={{ tag: 'auto-tool-browser', shadow: 'none' }} />
|
|
2
|
+
|
|
3
|
+
<script lang="ts">
|
|
4
|
+
import { filterRecipes, sortRecipes, groupToolsByServer, formatToolSchema } from '@webmcp-auto-ui/agent';
|
|
5
|
+
import type { BrowsableTool } from '@webmcp-auto-ui/agent';
|
|
6
|
+
import { MarkdownView, CodeView } from '@webmcp-auto-ui/ui';
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
open: boolean;
|
|
10
|
+
tools: BrowsableTool[];
|
|
11
|
+
initialFilter?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let { open = $bindable(false), tools = [], initialFilter = '' }: Props = $props();
|
|
15
|
+
|
|
16
|
+
let query = $state('');
|
|
17
|
+
let selected = $state<BrowsableTool | null>(null);
|
|
18
|
+
|
|
19
|
+
$effect(() => {
|
|
20
|
+
if (open) {
|
|
21
|
+
query = initialFilter || '';
|
|
22
|
+
selected = null;
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const filtered = $derived(sortRecipes(filterRecipes(tools, query)));
|
|
27
|
+
const grouped = $derived(groupToolsByServer(filtered));
|
|
28
|
+
|
|
29
|
+
function close() {
|
|
30
|
+
open = false;
|
|
31
|
+
selected = null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function handleKeydown(e: KeyboardEvent) {
|
|
35
|
+
if (e.key === 'Escape') {
|
|
36
|
+
if (selected) selected = null;
|
|
37
|
+
else close();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
</script>
|
|
41
|
+
|
|
42
|
+
<svelte:window onkeydown={open ? handleKeydown : undefined} />
|
|
43
|
+
|
|
44
|
+
{#if open}
|
|
45
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
46
|
+
<div class="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4"
|
|
47
|
+
onclick={close}>
|
|
48
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
49
|
+
<div class="bg-surface border border-border2 rounded-2xl shadow-2xl w-full max-w-2xl max-h-[85vh] flex flex-col overflow-hidden"
|
|
50
|
+
onclick={(e) => e.stopPropagation()}>
|
|
51
|
+
|
|
52
|
+
<!-- Header -->
|
|
53
|
+
<div class="flex items-center gap-3 px-6 py-4 border-b border-border flex-shrink-0">
|
|
54
|
+
{#if selected}
|
|
55
|
+
<button class="text-text2 hover:text-text1 transition-colors" onclick={() => selected = null}>
|
|
56
|
+
←
|
|
57
|
+
</button>
|
|
58
|
+
<span class="font-mono text-sm font-bold text-text1 truncate flex-1">{selected.name}</span>
|
|
59
|
+
{:else}
|
|
60
|
+
<span class="font-mono text-sm font-bold text-text1 flex-1">Tool Browser</span>
|
|
61
|
+
<span class="text-[10px] font-mono text-text2">{filtered.length} tools</span>
|
|
62
|
+
{/if}
|
|
63
|
+
<button class="text-text2 hover:text-text1 text-lg leading-none transition-colors"
|
|
64
|
+
onclick={close}>x</button>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
{#if selected}
|
|
68
|
+
<!-- Detail view -->
|
|
69
|
+
<div class="flex-1 overflow-y-auto p-6 flex flex-col gap-4">
|
|
70
|
+
{#if selected.description}
|
|
71
|
+
<MarkdownView source={selected.description} class="text-text2" />
|
|
72
|
+
{:else}
|
|
73
|
+
<p class="text-sm text-text2 italic">No description available</p>
|
|
74
|
+
{/if}
|
|
75
|
+
|
|
76
|
+
{#if selected.server}
|
|
77
|
+
<div class="flex flex-col gap-1">
|
|
78
|
+
<span class="text-[9px] font-mono text-text2 uppercase tracking-wider">Server</span>
|
|
79
|
+
<span class="font-mono text-xs text-text1 bg-surface2 rounded px-3 py-2 border border-border2">{selected.server}</span>
|
|
80
|
+
</div>
|
|
81
|
+
{/if}
|
|
82
|
+
|
|
83
|
+
{#if selected.inputSchema}
|
|
84
|
+
<div class="flex flex-col gap-1">
|
|
85
|
+
<span class="text-[9px] font-mono text-text2 uppercase tracking-wider">Input schema</span>
|
|
86
|
+
<div class="max-h-[400px] overflow-y-auto">
|
|
87
|
+
<CodeView lang="json" source={formatToolSchema(selected) ?? ''} />
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
{/if}
|
|
91
|
+
</div>
|
|
92
|
+
|
|
93
|
+
{:else}
|
|
94
|
+
<!-- Search -->
|
|
95
|
+
<div class="px-4 py-3 border-b border-border flex-shrink-0">
|
|
96
|
+
<input
|
|
97
|
+
type="text"
|
|
98
|
+
bind:value={query}
|
|
99
|
+
placeholder="Search tools..."
|
|
100
|
+
class="w-full bg-surface2 border border-border2 rounded-lg px-3 h-8 text-xs font-mono text-text1
|
|
101
|
+
outline-none placeholder:text-text2/40 focus:border-accent/50 transition-colors"
|
|
102
|
+
/>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<!-- List -->
|
|
106
|
+
<div class="flex-1 overflow-y-auto min-h-0">
|
|
107
|
+
{#each [...grouped.entries()] as [serverName, serverTools] (serverName)}
|
|
108
|
+
<div class="px-4 pt-3 pb-1">
|
|
109
|
+
<span class="text-[9px] font-mono uppercase tracking-wider text-text2">
|
|
110
|
+
{serverName} ({serverTools.length})
|
|
111
|
+
</span>
|
|
112
|
+
</div>
|
|
113
|
+
{#each serverTools as tool, i (`${serverName}:${tool.name}:${i}`)}
|
|
114
|
+
<button
|
|
115
|
+
class="w-full text-left px-4 py-2 border-b border-border/30 hover:bg-surface2/80 transition-colors"
|
|
116
|
+
onclick={() => selected = tool}
|
|
117
|
+
>
|
|
118
|
+
<div class="font-mono text-xs font-medium text-text1">{tool.name}</div>
|
|
119
|
+
{#if tool.description}
|
|
120
|
+
<div class="text-[10px] text-text2 truncate mt-0.5">{tool.description}</div>
|
|
121
|
+
{/if}
|
|
122
|
+
</button>
|
|
123
|
+
{/each}
|
|
124
|
+
{/each}
|
|
125
|
+
|
|
126
|
+
{#if filtered.length === 0}
|
|
127
|
+
<div class="p-8 text-center text-text2 text-xs font-mono">No tools found</div>
|
|
128
|
+
{/if}
|
|
129
|
+
</div>
|
|
130
|
+
{/if}
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
{/if}
|