@webmcp-auto-ui/ui 2.5.32 → 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,67 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* SafeImage — URL validation + error fallback. Replaces raw <img> in widgets.
|
|
4
|
+
*/
|
|
5
|
+
interface Props {
|
|
6
|
+
src: string | undefined | null;
|
|
7
|
+
alt?: string;
|
|
8
|
+
class?: string;
|
|
9
|
+
style?: string;
|
|
10
|
+
loading?: 'lazy' | 'eager';
|
|
11
|
+
fallbackText?: string;
|
|
12
|
+
hideOnError?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let { src, alt = '', class: className = '', style = '', loading = 'lazy', fallbackText, hideOnError = false }: Props = $props();
|
|
16
|
+
|
|
17
|
+
const VALID_PREFIXES = ['http://', 'https://', 'data:', '/'];
|
|
18
|
+
|
|
19
|
+
const isValidUrl = $derived(
|
|
20
|
+
typeof src === 'string' && src.length > 0 && VALID_PREFIXES.some(p => src!.startsWith(p))
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
let hasError = $state(false);
|
|
24
|
+
let isLoaded = $state(false);
|
|
25
|
+
|
|
26
|
+
$effect(() => {
|
|
27
|
+
if (src) {
|
|
28
|
+
hasError = false;
|
|
29
|
+
isLoaded = false;
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
function onError() { hasError = true; }
|
|
34
|
+
function onLoad() { isLoaded = true; }
|
|
35
|
+
|
|
36
|
+
const showPlaceholder = $derived(!isValidUrl || hasError);
|
|
37
|
+
const label = $derived(fallbackText ?? alt ?? 'Image');
|
|
38
|
+
</script>
|
|
39
|
+
|
|
40
|
+
{#if showPlaceholder}
|
|
41
|
+
{#if !hideOnError}
|
|
42
|
+
<div
|
|
43
|
+
class="flex items-center justify-center bg-surface2 text-text2 text-xs {className}"
|
|
44
|
+
{style}
|
|
45
|
+
role="img"
|
|
46
|
+
aria-label={label}
|
|
47
|
+
>
|
|
48
|
+
<svg class="w-5 h-5 opacity-40 mr-1" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
|
|
49
|
+
<rect x="3" y="3" width="18" height="18" rx="2" />
|
|
50
|
+
<circle cx="8.5" cy="8.5" r="1.5" />
|
|
51
|
+
<path d="M21 15l-5-5L5 21" />
|
|
52
|
+
</svg>
|
|
53
|
+
<span class="truncate max-w-[80%]">{label}</span>
|
|
54
|
+
</div>
|
|
55
|
+
{/if}
|
|
56
|
+
{:else}
|
|
57
|
+
<img
|
|
58
|
+
{src}
|
|
59
|
+
{alt}
|
|
60
|
+
class={className}
|
|
61
|
+
{style}
|
|
62
|
+
{loading}
|
|
63
|
+
onerror={onError}
|
|
64
|
+
onload={onLoad}
|
|
65
|
+
referrerpolicy="no-referrer"
|
|
66
|
+
/>
|
|
67
|
+
{/if}
|
|
@@ -13,84 +13,89 @@
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
// Safe clone for data passed to vanilla renderers — strips Svelte 5 $state
|
|
16
|
-
// proxies
|
|
16
|
+
// proxies, then falls back to a JSON pass that drops non-serializable values.
|
|
17
17
|
function safeClone<T>(value: T): T {
|
|
18
18
|
if (value === null || typeof value !== 'object') return value;
|
|
19
19
|
try {
|
|
20
20
|
return structuredClone(value);
|
|
21
|
-
} catch
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
} catch {
|
|
22
|
+
try {
|
|
23
|
+
const seen = new WeakSet<object>();
|
|
24
|
+
return JSON.parse(JSON.stringify(value, (_k, v) => {
|
|
25
|
+
if (v === null || v === undefined) return v;
|
|
26
|
+
const t = typeof v;
|
|
27
|
+
if (t === 'function' || t === 'symbol' || t === 'bigint') return undefined;
|
|
28
|
+
if (t !== 'object') return v;
|
|
29
|
+
if (typeof Node !== 'undefined' && v instanceof Node) return undefined;
|
|
30
|
+
if (typeof Window !== 'undefined' && v instanceof Window) return undefined;
|
|
31
|
+
if (typeof Event !== 'undefined' && v instanceof Event) return undefined;
|
|
32
|
+
if (seen.has(v)) return undefined;
|
|
33
|
+
seen.add(v);
|
|
34
|
+
return v;
|
|
35
|
+
})) as T;
|
|
36
|
+
} catch {
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
24
39
|
}
|
|
25
40
|
}
|
|
26
41
|
|
|
27
|
-
// ── Native
|
|
28
|
-
//
|
|
29
|
-
|
|
30
|
-
import
|
|
31
|
-
import
|
|
32
|
-
import
|
|
33
|
-
import
|
|
34
|
-
import
|
|
35
|
-
import
|
|
36
|
-
import
|
|
37
|
-
import
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
import
|
|
41
|
-
import
|
|
42
|
-
import
|
|
43
|
-
import
|
|
44
|
-
import
|
|
45
|
-
import
|
|
46
|
-
import
|
|
47
|
-
import
|
|
48
|
-
import
|
|
49
|
-
import
|
|
50
|
-
import
|
|
51
|
-
import
|
|
52
|
-
import
|
|
53
|
-
import
|
|
54
|
-
import
|
|
55
|
-
import
|
|
56
|
-
|
|
57
|
-
|
|
42
|
+
// ── Native custom-element widgets (Svelte 5 compiled) ────────────────────
|
|
43
|
+
// Side-effect imports register the custom elements with the browser.
|
|
44
|
+
// Simple (9)
|
|
45
|
+
import './simple/stat.svelte';
|
|
46
|
+
import './simple/kv.svelte';
|
|
47
|
+
import './simple/list.svelte';
|
|
48
|
+
import './simple/chart.svelte';
|
|
49
|
+
import './simple/alert.svelte';
|
|
50
|
+
import './simple/code.svelte';
|
|
51
|
+
import './simple/text.svelte';
|
|
52
|
+
import './simple/actions.svelte';
|
|
53
|
+
import './simple/tags.svelte';
|
|
54
|
+
// Rich (15) — map and d3 intentionally omitted (see plan: handled by leaflet/d3 servers)
|
|
55
|
+
import './rich/stat-card.svelte';
|
|
56
|
+
import './rich/profile.svelte';
|
|
57
|
+
import './rich/json-viewer.svelte';
|
|
58
|
+
import './rich/chart-rich.svelte';
|
|
59
|
+
import './rich/sankey.svelte';
|
|
60
|
+
import './rich/hemicycle.svelte';
|
|
61
|
+
import './rich/data-table.svelte';
|
|
62
|
+
import './rich/timeline.svelte';
|
|
63
|
+
import './rich/trombinoscope.svelte';
|
|
64
|
+
import './rich/cards.svelte';
|
|
65
|
+
import './rich/grid-data.svelte';
|
|
66
|
+
import './rich/js-sandbox.svelte';
|
|
67
|
+
import './rich/log.svelte';
|
|
68
|
+
import './rich/gallery.svelte';
|
|
69
|
+
import './rich/carousel.svelte';
|
|
70
|
+
import './rich/chat-input.svelte';
|
|
71
|
+
// Notebook (1)
|
|
72
|
+
import './notebook/notebook.svelte';
|
|
73
|
+
// Agent browsers (registered as widgets for widget_display)
|
|
74
|
+
import '../agent/RecipeBrowser.svelte';
|
|
75
|
+
|
|
76
|
+
/** Native widget types served as custom elements (`<auto-${type}>`). */
|
|
77
|
+
const NATIVE_CUSTOM_ELEMENTS = new Set<string>([
|
|
78
|
+
// Simple
|
|
79
|
+
'stat', 'kv', 'list', 'chart', 'alert', 'code', 'text', 'actions', 'tags',
|
|
80
|
+
// Rich
|
|
81
|
+
'stat-card', 'profile', 'json-viewer', 'chart-rich', 'sankey', 'hemicycle',
|
|
82
|
+
'data-table', 'timeline', 'trombinoscope', 'cards', 'grid-data',
|
|
83
|
+
'js-sandbox', 'log', 'gallery', 'carousel', 'chat-input',
|
|
84
|
+
// Notebook
|
|
85
|
+
'notebook',
|
|
86
|
+
// Agent browsers
|
|
87
|
+
'recipe-browser',
|
|
88
|
+
]);
|
|
89
|
+
|
|
90
|
+
/** A vanilla renderer: returns cleanup or Promise thereof. Still used for
|
|
91
|
+
* server-provided custom widgets via `widget.vanilla = true`. */
|
|
58
92
|
type VanillaRenderer = (
|
|
59
93
|
container: HTMLElement,
|
|
60
94
|
data: Record<string, unknown>,
|
|
61
95
|
) => void | (() => void) | Promise<void | (() => void)>;
|
|
62
96
|
|
|
63
|
-
/**
|
|
64
|
-
const NATIVE_VANILLA_MAP: Record<string, VanillaRenderer> = {
|
|
65
|
-
// Simple
|
|
66
|
-
'stat': renderStat,
|
|
67
|
-
'kv': renderKv,
|
|
68
|
-
'list': renderList,
|
|
69
|
-
'chart': renderChart,
|
|
70
|
-
'alert': renderAlert,
|
|
71
|
-
'code': renderCode,
|
|
72
|
-
'text': renderText,
|
|
73
|
-
'actions': renderActions,
|
|
74
|
-
'tags': renderTags,
|
|
75
|
-
// Rich
|
|
76
|
-
'stat-card': renderStatCard,
|
|
77
|
-
'profile': renderProfile,
|
|
78
|
-
'json-viewer': renderJsonViewer,
|
|
79
|
-
'chart-rich': renderChartRich,
|
|
80
|
-
'sankey': renderSankey,
|
|
81
|
-
'hemicycle': renderHemicycle,
|
|
82
|
-
'data-table': renderDataTable,
|
|
83
|
-
'timeline': renderTimeline,
|
|
84
|
-
'trombinoscope': renderTrombinoscope,
|
|
85
|
-
'cards': renderCards,
|
|
86
|
-
'grid-data': renderGridData,
|
|
87
|
-
'map': renderMap,
|
|
88
|
-
'd3': renderD3 as unknown as VanillaRenderer,
|
|
89
|
-
'js-sandbox': renderJsSandbox as unknown as VanillaRenderer,
|
|
90
|
-
'log': renderLog,
|
|
91
|
-
'gallery': renderGallery,
|
|
92
|
-
'carousel': renderCarousel,
|
|
93
|
-
};
|
|
97
|
+
/** No native vanilla widgets remain — all native widgets are custom elements. */
|
|
98
|
+
const NATIVE_VANILLA_MAP: Record<string, VanillaRenderer> = {};
|
|
94
99
|
|
|
95
100
|
interface Props {
|
|
96
101
|
id?: string;
|
|
@@ -115,7 +120,7 @@
|
|
|
115
120
|
bus.broadcast(busId, 'interact', { type, action, payload });
|
|
116
121
|
}
|
|
117
122
|
|
|
118
|
-
// ── Renderer resolution: servers > native vanilla > fallback
|
|
123
|
+
// ── Renderer resolution: servers > native custom-element > native vanilla > fallback ────
|
|
119
124
|
|
|
120
125
|
// Look up a custom widget entry from connected WebMCP servers
|
|
121
126
|
const customWidgetEntry = $derived.by(() => {
|
|
@@ -130,8 +135,13 @@
|
|
|
130
135
|
const customRenderer = $derived(customWidgetEntry?.renderer ?? null);
|
|
131
136
|
const isCustomVanilla = $derived(customWidgetEntry?.vanilla === true);
|
|
132
137
|
|
|
138
|
+
/** True if this widget type is served as a native Svelte-compiled custom element. */
|
|
139
|
+
const isNativeCustomElement = $derived(
|
|
140
|
+
!customRenderer && NATIVE_CUSTOM_ELEMENTS.has(type),
|
|
141
|
+
);
|
|
142
|
+
|
|
133
143
|
const nativeVanillaRenderer = $derived<VanillaRenderer | undefined>(
|
|
134
|
-
customRenderer ? undefined : NATIVE_VANILLA_MAP[type],
|
|
144
|
+
customRenderer || isNativeCustomElement ? undefined : NATIVE_VANILLA_MAP[type],
|
|
135
145
|
);
|
|
136
146
|
|
|
137
147
|
/** True when a vanilla renderer (custom or native) should be used */
|
|
@@ -148,9 +158,54 @@
|
|
|
148
158
|
// libs (D3, Leaflet, etc.) rely on Object.defineProperty which conflicts with proxies.
|
|
149
159
|
const plainData: Record<string, unknown> = $derived(safeClone(data) as Record<string, unknown>);
|
|
150
160
|
|
|
161
|
+
// ── Custom element container + element handle ─────────
|
|
162
|
+
let ceContainer: HTMLElement | undefined = $state(undefined);
|
|
163
|
+
let ceElement: HTMLElement | undefined = undefined;
|
|
164
|
+
|
|
165
|
+
$effect(() => {
|
|
166
|
+
if (!isNativeCustomElement || !ceContainer) return;
|
|
167
|
+
const tag = `auto-${type}`;
|
|
168
|
+
// Instantiate on first mount. `data` setter is reactive via Svelte 5 custom-element.
|
|
169
|
+
const el = document.createElement(tag) as HTMLElement;
|
|
170
|
+
(el as unknown as { data: unknown }).data = plainData;
|
|
171
|
+
const onInteract = (ev: Event) => {
|
|
172
|
+
const ce = ev as CustomEvent<{ action?: string; payload?: unknown }>;
|
|
173
|
+
const action = ce.detail?.action ?? 'interact';
|
|
174
|
+
emit(action, ce.detail?.payload);
|
|
175
|
+
};
|
|
176
|
+
el.addEventListener('widget:interact', onInteract);
|
|
177
|
+
ceContainer.innerHTML = '';
|
|
178
|
+
ceContainer.appendChild(el);
|
|
179
|
+
ceElement = el;
|
|
180
|
+
return () => {
|
|
181
|
+
el.removeEventListener('widget:interact', onInteract);
|
|
182
|
+
ceElement = undefined;
|
|
183
|
+
if (ceContainer) ceContainer.innerHTML = '';
|
|
184
|
+
};
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// In-place data updates on the custom element — no remount.
|
|
188
|
+
$effect(() => {
|
|
189
|
+
const next = plainData;
|
|
190
|
+
if (!isNativeCustomElement || !ceElement) return;
|
|
191
|
+
(ceElement as unknown as { data: unknown }).data = next;
|
|
192
|
+
});
|
|
193
|
+
|
|
151
194
|
// ── Vanilla renderer container + lifecycle ────────────
|
|
152
195
|
let vanillaContainer: HTMLElement | undefined = $state(undefined);
|
|
153
196
|
|
|
197
|
+
// Cleanup handle shared between the mount effect and the data-update fallback
|
|
198
|
+
// remount — so a fallback remount can tear down the previous render even
|
|
199
|
+
// though it doesn't re-run the mount effect.
|
|
200
|
+
let currentCleanup: (() => void) | undefined = undefined;
|
|
201
|
+
function runCurrentCleanup() {
|
|
202
|
+
const c = currentCleanup;
|
|
203
|
+
currentCleanup = undefined;
|
|
204
|
+
if (typeof c === 'function') {
|
|
205
|
+
try { c(); } catch (err) { console.error('[WidgetRenderer] cleanup failed:', err); }
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
154
209
|
// Mount effect — re-runs only when the widget identity changes (type /
|
|
155
210
|
// renderer / container). Data changes are handled separately to avoid a
|
|
156
211
|
// flickering full remount on every agent update.
|
|
@@ -160,6 +215,9 @@
|
|
|
160
215
|
if (!useVanilla || !vanillaRenderer || !vanillaContainer) return;
|
|
161
216
|
const container = vanillaContainer;
|
|
162
217
|
const renderer = vanillaRenderer;
|
|
218
|
+
// If a previous render is still live (e.g. via data-update fallback),
|
|
219
|
+
// tear it down before we clear the DOM.
|
|
220
|
+
runCurrentCleanup();
|
|
163
221
|
container.innerHTML = '';
|
|
164
222
|
|
|
165
223
|
const onInteract = (ev: Event) => {
|
|
@@ -169,17 +227,24 @@
|
|
|
169
227
|
};
|
|
170
228
|
container.addEventListener('widget:interact', onInteract);
|
|
171
229
|
|
|
172
|
-
let cleanup: (() => void) | void;
|
|
173
230
|
let cancelled = false;
|
|
174
231
|
|
|
175
232
|
try {
|
|
176
233
|
const result = renderer(container, untrack(() => plainData));
|
|
177
234
|
if (result && typeof (result as Promise<unknown>).then === 'function') {
|
|
178
235
|
(result as Promise<void | (() => void)>).then(
|
|
179
|
-
(c) => {
|
|
236
|
+
(c) => {
|
|
237
|
+
// If we were torn down before the promise resolved, invoke the
|
|
238
|
+
// late cleanup immediately rather than leaking resources.
|
|
239
|
+
if (cancelled && typeof c === 'function') {
|
|
240
|
+
try { c(); } catch { /* ignore */ }
|
|
241
|
+
} else {
|
|
242
|
+
currentCleanup = c ?? undefined;
|
|
243
|
+
}
|
|
244
|
+
},
|
|
180
245
|
).catch((err) => { console.error('[WidgetRenderer] async render failed:', err); });
|
|
181
246
|
} else {
|
|
182
|
-
|
|
247
|
+
currentCleanup = (result as (() => void) | undefined) ?? undefined;
|
|
183
248
|
}
|
|
184
249
|
} catch (err) {
|
|
185
250
|
console.error('[WidgetRenderer] sync render failed:', err);
|
|
@@ -188,9 +253,7 @@
|
|
|
188
253
|
return () => {
|
|
189
254
|
cancelled = true;
|
|
190
255
|
container.removeEventListener('widget:interact', onInteract);
|
|
191
|
-
|
|
192
|
-
try { cleanup(); } catch (err) { console.error('[WidgetRenderer] cleanup failed:', err); }
|
|
193
|
-
}
|
|
256
|
+
runCurrentCleanup();
|
|
194
257
|
};
|
|
195
258
|
});
|
|
196
259
|
|
|
@@ -208,10 +271,20 @@
|
|
|
208
271
|
const handled = !vanillaContainer.dispatchEvent(ev);
|
|
209
272
|
if (handled || !vanillaRenderer) return;
|
|
210
273
|
// Not handled — fall back to remount by clearing + calling renderer again.
|
|
274
|
+
// Run the previous cleanup first so the old renderer releases its
|
|
275
|
+
// resources (timers, observers, third-party instances).
|
|
211
276
|
const container = vanillaContainer;
|
|
277
|
+
runCurrentCleanup();
|
|
212
278
|
container.innerHTML = '';
|
|
213
279
|
try {
|
|
214
|
-
vanillaRenderer(container, data);
|
|
280
|
+
const result = vanillaRenderer(container, data);
|
|
281
|
+
if (result && typeof (result as Promise<unknown>).then === 'function') {
|
|
282
|
+
(result as Promise<void | (() => void)>).then(
|
|
283
|
+
(c) => { currentCleanup = c ?? undefined; },
|
|
284
|
+
).catch((err) => { console.error('[WidgetRenderer] fallback async render failed:', err); });
|
|
285
|
+
} else {
|
|
286
|
+
currentCleanup = (result as (() => void) | undefined) ?? undefined;
|
|
287
|
+
}
|
|
215
288
|
} catch (err) {
|
|
216
289
|
console.error('[WidgetRenderer] fallback remount failed:', err);
|
|
217
290
|
}
|
|
@@ -250,7 +323,7 @@
|
|
|
250
323
|
description: `Update the data of ${type} widget (id: ${busId}).`,
|
|
251
324
|
inputSchema: { type: 'object', properties: {}, additionalProperties: true },
|
|
252
325
|
execute: (args: Record<string, unknown>) => {
|
|
253
|
-
oninteract?.(type, 'update', args);
|
|
326
|
+
oninteract?.(type, 'bus-update', args);
|
|
254
327
|
return { content: [{ type: 'text', text: `widget_${busId} updated` }] };
|
|
255
328
|
},
|
|
256
329
|
});
|
|
@@ -260,7 +333,7 @@
|
|
|
260
333
|
description: `Remove ${type} widget (id: ${busId}) from the view.`,
|
|
261
334
|
inputSchema: { type: 'object', properties: {} },
|
|
262
335
|
execute: () => {
|
|
263
|
-
oninteract?.(type, 'remove', {});
|
|
336
|
+
oninteract?.(type, 'bus-remove', {});
|
|
264
337
|
return { content: [{ type: 'text', text: `widget_${busId} removed` }] };
|
|
265
338
|
},
|
|
266
339
|
annotations: { destructiveHint: true },
|
|
@@ -277,7 +350,9 @@
|
|
|
277
350
|
});
|
|
278
351
|
</script>
|
|
279
352
|
|
|
280
|
-
{#if
|
|
353
|
+
{#if isNativeCustomElement}
|
|
354
|
+
<div bind:this={ceContainer} class="ce-container w-full h-full overflow-auto p-2"></div>
|
|
355
|
+
{:else if useVanilla}
|
|
281
356
|
<div bind:this={vanillaContainer} class="vanilla-container w-full h-full overflow-auto p-2"></div>
|
|
282
357
|
{:else if customRenderer}
|
|
283
358
|
<svelte:component this={customRenderer as Component<any>} {data} {id} />
|
|
@@ -1,206 +1,56 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
|
-
/**
|
|
3
|
-
* SQL executor for notebook cells.
|
|
4
|
-
*
|
|
5
|
-
* Finds a SQL-capable tool on the connected data servers via auto-pattern,
|
|
6
|
-
* calls it via postMessage, parses the result into a `table` CellResult.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
1
|
import { callToolViaPostMessage } from '@webmcp-auto-ui/core';
|
|
10
|
-
import type { CellExecutor, CellExecContext, DataServerDescriptor } from '../shared.js';
|
|
11
|
-
|
|
12
|
-
export interface SqlExecutorOptions {
|
|
13
|
-
/** Timeout per query (ms). Default 30000 */
|
|
14
|
-
timeoutMs?: number;
|
|
15
|
-
/** Max rows to keep in result (truncate beyond). Default 1000 */
|
|
16
|
-
maxRows?: number;
|
|
17
|
-
}
|
|
2
|
+
import type { CellExecutor, CellExecContext, CellResult, DataServerDescriptor } from '../shared.js';
|
|
18
3
|
|
|
19
4
|
const PATTERN_PRIMARY = /^.*query_sql$/i;
|
|
20
5
|
const PATTERN_FALLBACK = /^(query|run|execute)(_sql)?$/i;
|
|
21
6
|
|
|
22
7
|
function findSqlTool(servers: DataServerDescriptor[]): string | null {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
if (PATTERN_PRIMARY.test(t.name)) return t.name;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
// Priority 2: query / run / execute (with optional _sql)
|
|
30
|
-
for (const srv of servers) {
|
|
31
|
-
for (const t of srv.tools ?? []) {
|
|
32
|
-
if (PATTERN_FALLBACK.test(t.name)) return t.name;
|
|
8
|
+
for (const p of [PATTERN_PRIMARY, PATTERN_FALLBACK]) {
|
|
9
|
+
for (const srv of servers) {
|
|
10
|
+
for (const t of srv.tools ?? []) if (p.test(t.name)) return t.name;
|
|
33
11
|
}
|
|
34
12
|
}
|
|
35
13
|
return null;
|
|
36
14
|
}
|
|
37
15
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
*/
|
|
41
|
-
function extractText(result: any): string | null {
|
|
42
|
-
if (!result) return null;
|
|
43
|
-
const content = result.content ?? result;
|
|
44
|
-
if (!Array.isArray(content)) {
|
|
45
|
-
if (typeof content === 'string') return content;
|
|
46
|
-
return null;
|
|
47
|
-
}
|
|
48
|
-
for (const item of content) {
|
|
49
|
-
if (item && item.type === 'text' && typeof item.text === 'string') {
|
|
50
|
-
return item.text;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
return null;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function tryParseJson(text: string): unknown {
|
|
57
|
-
try {
|
|
58
|
-
return JSON.parse(text);
|
|
59
|
-
} catch {
|
|
60
|
-
return text; // not JSON, return raw
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function createSqlExecutor(
|
|
65
|
-
getServers: () => DataServerDescriptor[],
|
|
66
|
-
opts?: SqlExecutorOptions
|
|
67
|
-
): CellExecutor {
|
|
68
|
-
const timeoutMs = opts?.timeoutMs ?? 30_000;
|
|
69
|
-
const maxRows = opts?.maxRows ?? 1000;
|
|
70
|
-
|
|
71
|
-
return async (ctx: CellExecContext) => {
|
|
16
|
+
export function createSqlExecutor(getServers: () => DataServerDescriptor[]): CellExecutor {
|
|
17
|
+
return async (ctx: CellExecContext): Promise<CellResult> => {
|
|
72
18
|
const startedAt = Date.now();
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
const toolName = findSqlTool(servers);
|
|
19
|
+
const toolName = findSqlTool(getServers());
|
|
76
20
|
if (!toolName) {
|
|
77
|
-
return {
|
|
78
|
-
ok: false,
|
|
79
|
-
error: 'No SQL tool found on connected servers',
|
|
80
|
-
errorKind: 'schema',
|
|
81
|
-
durationMs: Date.now() - startedAt,
|
|
82
|
-
};
|
|
21
|
+
return { ok: false, error: 'No SQL tool available on connected servers.', errorKind: 'schema', durationMs: Date.now() - startedAt };
|
|
83
22
|
}
|
|
84
|
-
|
|
85
23
|
const sql = (ctx.cell.content ?? '').trim();
|
|
86
|
-
if (!sql) {
|
|
87
|
-
return { ok: true, kind: 'empty', durationMs: Date.now() - startedAt };
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Wrap the tool call with the cell's AbortSignal so an external abort
|
|
91
|
-
// rejects the promise even if callToolViaPostMessage doesn't support signals.
|
|
92
|
-
const callPromise = callToolViaPostMessage(toolName, { sql }, { timeout: timeoutMs });
|
|
24
|
+
if (!sql) return { ok: true, kind: 'empty', durationMs: Date.now() - startedAt };
|
|
93
25
|
|
|
94
|
-
let
|
|
26
|
+
let raw: unknown;
|
|
95
27
|
try {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (settled) return;
|
|
100
|
-
settled = true;
|
|
101
|
-
reject(new Error('aborted'));
|
|
102
|
-
};
|
|
103
|
-
if (ctx.signal.aborted) {
|
|
104
|
-
onAbort();
|
|
105
|
-
return;
|
|
106
|
-
}
|
|
107
|
-
ctx.signal.addEventListener('abort', onAbort, { once: true });
|
|
108
|
-
callPromise.then(
|
|
109
|
-
(v) => {
|
|
110
|
-
if (settled) return;
|
|
111
|
-
settled = true;
|
|
112
|
-
ctx.signal.removeEventListener('abort', onAbort);
|
|
113
|
-
resolve(v);
|
|
114
|
-
},
|
|
115
|
-
(err) => {
|
|
116
|
-
if (settled) return;
|
|
117
|
-
settled = true;
|
|
118
|
-
ctx.signal.removeEventListener('abort', onAbort);
|
|
119
|
-
reject(err);
|
|
120
|
-
}
|
|
121
|
-
);
|
|
122
|
-
});
|
|
123
|
-
} catch (err: any) {
|
|
124
|
-
const durationMs = Date.now() - startedAt;
|
|
125
|
-
const msg = String(err?.message ?? err);
|
|
126
|
-
const isTimeout = /timed out|aborted/i.test(msg);
|
|
127
|
-
return {
|
|
128
|
-
ok: false,
|
|
129
|
-
error: msg,
|
|
130
|
-
errorKind: isTimeout ? 'timeout' : 'runtime',
|
|
131
|
-
durationMs,
|
|
132
|
-
};
|
|
28
|
+
raw = await callToolViaPostMessage(toolName, { sql });
|
|
29
|
+
} catch (err) {
|
|
30
|
+
return { ok: false, error: String((err as { message?: unknown })?.message ?? err), errorKind: 'runtime', durationMs: Date.now() - startedAt };
|
|
133
31
|
}
|
|
134
|
-
|
|
135
32
|
const durationMs = Date.now() - startedAt;
|
|
136
33
|
|
|
137
|
-
|
|
138
|
-
const text =
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
: rows.length > 0 && typeof rows[0] === 'object'
|
|
157
|
-
? Object.keys(rows[0] as Record<string, unknown>)
|
|
158
|
-
: [];
|
|
159
|
-
let truncated = false;
|
|
160
|
-
if (rows.length > maxRows) {
|
|
161
|
-
rows = rows.slice(0, maxRows);
|
|
162
|
-
truncated = true;
|
|
163
|
-
}
|
|
164
|
-
return {
|
|
165
|
-
ok: true,
|
|
166
|
-
kind: 'table',
|
|
167
|
-
rows,
|
|
168
|
-
columns,
|
|
169
|
-
rowCount: (parsed as any).rows.length,
|
|
170
|
-
truncated: truncated || undefined,
|
|
171
|
-
durationMs,
|
|
172
|
-
};
|
|
34
|
+
const content = (raw as { content?: unknown })?.content;
|
|
35
|
+
const text = Array.isArray(content)
|
|
36
|
+
? (content.find((c) => (c as { type?: unknown })?.type === 'text') as { text?: string } | undefined)?.text ?? ''
|
|
37
|
+
: '';
|
|
38
|
+
let parsed: unknown = text;
|
|
39
|
+
try { parsed = JSON.parse(text); } catch { /* not JSON */ }
|
|
40
|
+
|
|
41
|
+
if (!text) return { ok: true, kind: 'empty', durationMs };
|
|
42
|
+
|
|
43
|
+
const rows: unknown[] =
|
|
44
|
+
Array.isArray(parsed) ? parsed
|
|
45
|
+
: Array.isArray((parsed as { rows?: unknown })?.rows) ? (parsed as { rows: unknown[] }).rows
|
|
46
|
+
: [];
|
|
47
|
+
if (rows.length && rows.every((r) => r && typeof r === 'object')) {
|
|
48
|
+
const declared = (parsed as { columns?: unknown })?.columns;
|
|
49
|
+
const columns = Array.isArray(declared)
|
|
50
|
+
? declared.map(String)
|
|
51
|
+
: Array.from(new Set(rows.flatMap((r) => Object.keys(r as Record<string, unknown>))));
|
|
52
|
+
return { ok: true, kind: 'table', rows: rows as Record<string, unknown>[], columns, rowCount: rows.length, durationMs };
|
|
173
53
|
}
|
|
174
|
-
|
|
175
|
-
// Array of objects
|
|
176
|
-
if (Array.isArray(parsed)) {
|
|
177
|
-
if (parsed.length === 0) {
|
|
178
|
-
return { ok: true, kind: 'table', rows: [], columns: [], rowCount: 0, durationMs };
|
|
179
|
-
}
|
|
180
|
-
const first = parsed[0];
|
|
181
|
-
if (first && typeof first === 'object' && !Array.isArray(first)) {
|
|
182
|
-
let rows = parsed as Record<string, unknown>[];
|
|
183
|
-
const columns = Object.keys(first as Record<string, unknown>);
|
|
184
|
-
const rowCount = rows.length;
|
|
185
|
-
let truncated = false;
|
|
186
|
-
if (rows.length > maxRows) {
|
|
187
|
-
rows = rows.slice(0, maxRows);
|
|
188
|
-
truncated = true;
|
|
189
|
-
}
|
|
190
|
-
return {
|
|
191
|
-
ok: true,
|
|
192
|
-
kind: 'table',
|
|
193
|
-
rows,
|
|
194
|
-
columns,
|
|
195
|
-
rowCount,
|
|
196
|
-
truncated: truncated || undefined,
|
|
197
|
-
durationMs,
|
|
198
|
-
};
|
|
199
|
-
}
|
|
200
|
-
return { ok: true, kind: 'value', value: parsed, durationMs };
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// Anything else — a scalar or object that isn't tabular
|
|
204
54
|
return { ok: true, kind: 'value', value: parsed, durationMs };
|
|
205
55
|
};
|
|
206
56
|
}
|