@webmcp-auto-ui/ui 2.5.36 → 2.5.38
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 +3 -1
- package/src/agent/MCPserversList.svelte +44 -32
- package/src/agent/RecipeBrowser.svelte +54 -18
- package/src/agent/RemoteMCPserversDemo.svelte +7 -7
- package/src/agent/ToolBrowser.svelte +34 -5
- package/src/agent/WebMCPserversList.svelte +85 -40
- package/src/base/chat-inline.svelte +81 -9
- package/src/index.ts +1 -0
- package/src/primitives/MarkdownView.svelte +12 -2
- package/src/recipe/RecipeCodeBlock.svelte +10 -2
- package/src/recipe/RecipeRunModal.svelte +245 -0
- package/src/widgets/WidgetRenderer.svelte +8 -5
- package/src/widgets/notebook/executors/sql.ts +9 -6
- package/src/widgets/notebook/import-modal-api.ts +15 -19
- package/src/widgets/notebook/import-modal.svelte +5 -4
- package/src/widgets/notebook/left-pane.ts +23 -3
- package/src/widgets/notebook/notebook.svelte +0 -1
- package/src/widgets/notebook/notebook.ts +437 -80
- package/src/widgets/notebook/resource-extractor.ts +16 -1
- package/src/widgets/notebook/share-handlers.ts +90 -1
- package/src/widgets/notebook/shared.ts +260 -88
- package/src/widgets/rich/cards.svelte +3 -1
- package/src/widgets/rich/chart-rich.svelte +73 -7
- package/src/widgets/rich/data-table.svelte +28 -7
- package/src/widgets/rich/map.svelte +392 -0
- package/src/widgets/rich/stat-card.svelte +119 -20
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
}
|
|
33
33
|
</script>
|
|
34
34
|
|
|
35
|
-
<div class="
|
|
35
|
+
<div class="chat-inline">
|
|
36
36
|
<input
|
|
37
37
|
bind:this={inputEl}
|
|
38
38
|
bind:value
|
|
@@ -40,16 +40,13 @@
|
|
|
40
40
|
{placeholder}
|
|
41
41
|
{disabled}
|
|
42
42
|
onkeydown={handleKeydown}
|
|
43
|
-
class="
|
|
44
|
-
placeholder:text-text2/40 focus:outline-none focus:border-accent/50
|
|
45
|
-
disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
|
43
|
+
class="chat-inline__input"
|
|
46
44
|
/>
|
|
47
45
|
{#if disabled}
|
|
48
46
|
<button
|
|
49
47
|
type="button"
|
|
50
48
|
onclick={handleStop}
|
|
51
|
-
class="
|
|
52
|
-
text-xs font-mono hover:bg-accent2/20 transition-colors flex-shrink-0"
|
|
49
|
+
class="chat-inline__btn chat-inline__btn--stop"
|
|
53
50
|
>
|
|
54
51
|
stop
|
|
55
52
|
</button>
|
|
@@ -58,11 +55,86 @@
|
|
|
58
55
|
type="button"
|
|
59
56
|
onclick={handleSubmit}
|
|
60
57
|
disabled={!value?.trim()}
|
|
61
|
-
class="
|
|
62
|
-
text-xs font-mono hover:bg-accent/20 disabled:opacity-40 disabled:cursor-not-allowed
|
|
63
|
-
transition-colors flex-shrink-0"
|
|
58
|
+
class="chat-inline__btn chat-inline__btn--send"
|
|
64
59
|
>
|
|
65
60
|
send
|
|
66
61
|
</button>
|
|
67
62
|
{/if}
|
|
68
63
|
</div>
|
|
64
|
+
|
|
65
|
+
<style>
|
|
66
|
+
.chat-inline {
|
|
67
|
+
display: flex;
|
|
68
|
+
align-items: center;
|
|
69
|
+
gap: 0.5rem;
|
|
70
|
+
width: 100%;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.chat-inline__input {
|
|
74
|
+
flex: 1 1 0%;
|
|
75
|
+
min-width: 0;
|
|
76
|
+
height: 2.25rem;
|
|
77
|
+
padding: 0 0.75rem;
|
|
78
|
+
border-radius: 0.5rem;
|
|
79
|
+
border: 1px solid var(--color-border2, var(--color-border, #2a2a2a));
|
|
80
|
+
background: var(--color-surface2, var(--color-surface, #1a1a1a));
|
|
81
|
+
color: var(--color-text1, #f5f5f5);
|
|
82
|
+
font-size: 0.875rem;
|
|
83
|
+
line-height: 1.25rem;
|
|
84
|
+
font-family: inherit;
|
|
85
|
+
outline: none;
|
|
86
|
+
transition: border-color 150ms ease, background-color 150ms ease, color 150ms ease;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.chat-inline__input::placeholder {
|
|
90
|
+
color: var(--color-text2, #888);
|
|
91
|
+
opacity: 0.4;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.chat-inline__input:focus {
|
|
95
|
+
border-color: color-mix(in srgb, var(--color-accent, #4a9eff) 50%, transparent);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.chat-inline__input:disabled {
|
|
99
|
+
opacity: 0.5;
|
|
100
|
+
cursor: not-allowed;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.chat-inline__btn {
|
|
104
|
+
flex-shrink: 0;
|
|
105
|
+
height: 2.25rem;
|
|
106
|
+
padding: 0 0.75rem;
|
|
107
|
+
border-radius: 0.5rem;
|
|
108
|
+
border: 1px solid transparent;
|
|
109
|
+
font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace);
|
|
110
|
+
font-size: 0.6875rem;
|
|
111
|
+
line-height: 1;
|
|
112
|
+
cursor: pointer;
|
|
113
|
+
transition: background-color 150ms ease, border-color 150ms ease, color 150ms ease, opacity 150ms ease;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.chat-inline__btn:disabled {
|
|
117
|
+
opacity: 0.4;
|
|
118
|
+
cursor: not-allowed;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.chat-inline__btn--send {
|
|
122
|
+
background: color-mix(in srgb, var(--color-accent, #4a9eff) 10%, transparent);
|
|
123
|
+
color: var(--color-accent, #4a9eff);
|
|
124
|
+
border-color: color-mix(in srgb, var(--color-accent, #4a9eff) 40%, transparent);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.chat-inline__btn--send:hover:not(:disabled) {
|
|
128
|
+
background: color-mix(in srgb, var(--color-accent, #4a9eff) 20%, transparent);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.chat-inline__btn--stop {
|
|
132
|
+
background: color-mix(in srgb, var(--color-accent2, #ff6b6b) 10%, transparent);
|
|
133
|
+
color: var(--color-accent2, #ff6b6b);
|
|
134
|
+
border-color: color-mix(in srgb, var(--color-accent2, #ff6b6b) 40%, transparent);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.chat-inline__btn--stop:hover:not(:disabled) {
|
|
138
|
+
background: color-mix(in srgb, var(--color-accent2, #ff6b6b) 20%, transparent);
|
|
139
|
+
}
|
|
140
|
+
</style>
|
package/src/index.ts
CHANGED
|
@@ -21,6 +21,7 @@ export { renderMarkdown, highlightCode, createMarkdownRenderer } from './primiti
|
|
|
21
21
|
|
|
22
22
|
// Recipe building blocks (used by RecipeModal and notebook recipe-viewer)
|
|
23
23
|
export { default as RecipeCodeBlock } from './recipe/RecipeCodeBlock.svelte';
|
|
24
|
+
export { default as RecipeRunModal } from './recipe/RecipeRunModal.svelte';
|
|
24
25
|
export type { RecipeBlockAction } from './recipe/types.js';
|
|
25
26
|
|
|
26
27
|
// Widgets are shipped as Svelte 5 custom elements — import the widget file
|
|
@@ -5,13 +5,23 @@
|
|
|
5
5
|
interface Props {
|
|
6
6
|
source: string;
|
|
7
7
|
class?: string;
|
|
8
|
+
onLinkClick?: (href: string, ev: MouseEvent) => void;
|
|
8
9
|
}
|
|
9
10
|
|
|
10
|
-
let { source, class: className = '' }: Props = $props();
|
|
11
|
+
let { source, class: className = '', onLinkClick }: Props = $props();
|
|
11
12
|
const html = $derived(renderMarkdown(source ?? ''));
|
|
13
|
+
|
|
14
|
+
function handleClick(ev: MouseEvent) {
|
|
15
|
+
if (!onLinkClick) return;
|
|
16
|
+
const a = (ev.target as HTMLElement | null)?.closest?.('a');
|
|
17
|
+
if (!a) return;
|
|
18
|
+
const href = a.getAttribute('href');
|
|
19
|
+
if (!href) return;
|
|
20
|
+
onLinkClick(href, ev);
|
|
21
|
+
}
|
|
12
22
|
</script>
|
|
13
23
|
|
|
14
|
-
<div class="markdown-body {className}">
|
|
24
|
+
<div class="markdown-body {className}" onclick={handleClick} role="presentation">
|
|
15
25
|
{@html html}
|
|
16
26
|
</div>
|
|
17
27
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import type { McpMultiClient } from '@webmcp-auto-ui/core';
|
|
3
3
|
import type { RunResult } from '@webmcp-auto-ui/sdk';
|
|
4
4
|
import { runCode, estimateTokens } from '@webmcp-auto-ui/sdk';
|
|
5
|
+
import { canvas } from '@webmcp-auto-ui/sdk/canvas';
|
|
5
6
|
import { highlightCode } from '../primitives/markdown-renderer.js';
|
|
6
7
|
import type { RecipeBlockAction } from './types.js';
|
|
7
8
|
|
|
@@ -19,6 +20,12 @@
|
|
|
19
20
|
* Ignored when `actions` is provided.
|
|
20
21
|
*/
|
|
21
22
|
onrun?: (payload: { code: string; lang: string; result: RunResult }) => void;
|
|
23
|
+
/**
|
|
24
|
+
* Optional shared scope object. When provided, top-level decls of prior
|
|
25
|
+
* blocks are visible in this block, and this block's top-level decls are
|
|
26
|
+
* written back so subsequent blocks can read them. Owner: the host modal.
|
|
27
|
+
*/
|
|
28
|
+
scope?: Record<string, unknown>;
|
|
22
29
|
}
|
|
23
30
|
|
|
24
31
|
let {
|
|
@@ -26,6 +33,7 @@
|
|
|
26
33
|
lang = 'text',
|
|
27
34
|
actions = undefined,
|
|
28
35
|
onrun,
|
|
36
|
+
scope,
|
|
29
37
|
}: Props = $props();
|
|
30
38
|
|
|
31
39
|
let editable = $state('');
|
|
@@ -84,8 +92,8 @@
|
|
|
84
92
|
const t0 = performance.now();
|
|
85
93
|
startTimer(t0);
|
|
86
94
|
|
|
87
|
-
const multi =
|
|
88
|
-
const result = await runCode(editable, lang, multi);
|
|
95
|
+
const multi = canvas.multiClient as McpMultiClient | undefined;
|
|
96
|
+
const result = await runCode(editable, lang, multi, scope);
|
|
89
97
|
|
|
90
98
|
stopTimer();
|
|
91
99
|
lastDuration = result.durationMs;
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { fly } from 'svelte/transition';
|
|
3
|
+
import type { RunTab } from '@webmcp-auto-ui/sdk';
|
|
4
|
+
import { safeStringify } from '@webmcp-auto-ui/sdk';
|
|
5
|
+
import { WidgetRenderer } from '@webmcp-auto-ui/ui';
|
|
6
|
+
import type { WebMcpServer } from '@webmcp-auto-ui/core';
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
open: boolean;
|
|
10
|
+
runs: RunTab[];
|
|
11
|
+
activeTabId: string | null;
|
|
12
|
+
onclose: () => void;
|
|
13
|
+
onreplay: (tabId: string) => void;
|
|
14
|
+
onselectTab: (tabId: string) => void;
|
|
15
|
+
/** When true, render as an inline panel inside the host modal instead of a floating side panel. */
|
|
16
|
+
inline?: boolean;
|
|
17
|
+
/** Connected WebMCP servers — needed for custom widget renderers (e.g. canvas2d). */
|
|
18
|
+
servers?: WebMcpServer[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let {
|
|
22
|
+
open,
|
|
23
|
+
runs,
|
|
24
|
+
activeTabId,
|
|
25
|
+
onclose,
|
|
26
|
+
onreplay,
|
|
27
|
+
onselectTab,
|
|
28
|
+
inline = false,
|
|
29
|
+
servers = [],
|
|
30
|
+
}: Props = $props();
|
|
31
|
+
|
|
32
|
+
const active = $derived(runs.find((r) => r.id === activeTabId) ?? runs[runs.length - 1] ?? null);
|
|
33
|
+
|
|
34
|
+
let logsOpen = $state(true);
|
|
35
|
+
let copyState = $state<'idle' | 'copied'>('idle');
|
|
36
|
+
let copyTimer: ReturnType<typeof setTimeout> | undefined;
|
|
37
|
+
|
|
38
|
+
function formatTokens(n: number | undefined): string {
|
|
39
|
+
if (n == null) return '—';
|
|
40
|
+
if (n >= 1000) return `${(n / 1000).toFixed(1)}k`;
|
|
41
|
+
return `${n}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function copyOutput() {
|
|
45
|
+
if (!active) return;
|
|
46
|
+
try {
|
|
47
|
+
const text =
|
|
48
|
+
active.result.status === 'error'
|
|
49
|
+
? (active.result.error ?? '')
|
|
50
|
+
: safeStringify(active.result.output);
|
|
51
|
+
await navigator.clipboard.writeText(text);
|
|
52
|
+
copyState = 'copied';
|
|
53
|
+
if (copyTimer) clearTimeout(copyTimer);
|
|
54
|
+
copyTimer = setTimeout(() => { copyState = 'idle'; }, 1500);
|
|
55
|
+
} catch {
|
|
56
|
+
/* ignore */
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
</script>
|
|
60
|
+
|
|
61
|
+
{#if open}
|
|
62
|
+
<div
|
|
63
|
+
class="run-panel {inline ? 'inline' : 'side'} bg-surface border border-border2 rounded-2xl flex flex-col shadow-2xl overflow-hidden"
|
|
64
|
+
transition:fly={{ x: inline ? 0 : 24, y: inline ? 12 : 0, duration: 200 }}
|
|
65
|
+
>
|
|
66
|
+
<!-- Header -->
|
|
67
|
+
<div class="flex items-center gap-3 px-4 py-3 border-b border-border flex-shrink-0">
|
|
68
|
+
<span class="font-mono text-xs font-bold text-text1 flex-1 truncate">
|
|
69
|
+
{#if active}
|
|
70
|
+
{@html '▶'} Run · <span class="text-accent">{active.label}</span>
|
|
71
|
+
{:else}
|
|
72
|
+
Run
|
|
73
|
+
{/if}
|
|
74
|
+
</span>
|
|
75
|
+
<button
|
|
76
|
+
class="text-text2 hover:text-text1 font-mono text-base leading-none transition-colors"
|
|
77
|
+
onclick={onclose}
|
|
78
|
+
title="Close"
|
|
79
|
+
>x</button>
|
|
80
|
+
</div>
|
|
81
|
+
|
|
82
|
+
<!-- Tabs (if multiple) -->
|
|
83
|
+
{#if runs.length > 1}
|
|
84
|
+
<div class="flex items-center gap-1 px-3 py-1.5 border-b border-border overflow-x-auto flex-shrink-0">
|
|
85
|
+
{#each runs as tab (tab.id)}
|
|
86
|
+
<button
|
|
87
|
+
class="font-mono text-[10px] px-2 py-1 rounded border transition-colors whitespace-nowrap
|
|
88
|
+
{tab.id === active?.id
|
|
89
|
+
? 'border-accent/50 text-accent bg-accent/10'
|
|
90
|
+
: 'border-border2 text-text2 hover:text-text1'}"
|
|
91
|
+
onclick={() => onselectTab(tab.id)}
|
|
92
|
+
>
|
|
93
|
+
{tab.label}
|
|
94
|
+
</button>
|
|
95
|
+
{/each}
|
|
96
|
+
</div>
|
|
97
|
+
{/if}
|
|
98
|
+
|
|
99
|
+
{#if active}
|
|
100
|
+
<!-- Stats row -->
|
|
101
|
+
<div class="flex items-center gap-4 px-4 py-2 border-b border-border flex-shrink-0 font-mono text-[11px]">
|
|
102
|
+
<span class="text-text2">
|
|
103
|
+
{@html '⏱'} <span class="text-text1">{active.result.durationMs ?? '—'}{active.result.durationMs != null ? 'ms' : ''}</span>
|
|
104
|
+
</span>
|
|
105
|
+
<span class="text-text2">
|
|
106
|
+
{@html '◼'} <span class="text-text1">{formatTokens(active.result.tokens)} tok</span>
|
|
107
|
+
</span>
|
|
108
|
+
<span class="ml-auto">
|
|
109
|
+
{#if active.result.status === 'running'}
|
|
110
|
+
<span class="text-accent">{@html '◐'} running</span>
|
|
111
|
+
{:else if active.result.status === 'done'}
|
|
112
|
+
<span class="text-teal">{@html '✓'} done</span>
|
|
113
|
+
{:else if active.result.status === 'error'}
|
|
114
|
+
<span class="text-red-400">! error</span>
|
|
115
|
+
{:else}
|
|
116
|
+
<span class="text-text2">idle</span>
|
|
117
|
+
{/if}
|
|
118
|
+
</span>
|
|
119
|
+
<button
|
|
120
|
+
class="font-mono text-xs h-6 px-2 rounded border border-border2 text-text2 hover:text-text1 transition-colors"
|
|
121
|
+
onclick={() => onreplay(active.id)}
|
|
122
|
+
disabled={active.result.status === 'running'}
|
|
123
|
+
title="Replay"
|
|
124
|
+
>{@html '↻'}</button>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
<!-- Body -->
|
|
128
|
+
<div class="flex-1 overflow-y-auto px-4 py-3 flex flex-col gap-3">
|
|
129
|
+
<!-- Output -->
|
|
130
|
+
<div>
|
|
131
|
+
<div class="flex items-center mb-1">
|
|
132
|
+
<span class="text-[9px] font-mono text-text2 uppercase tracking-wider">
|
|
133
|
+
{active.result.status === 'error'
|
|
134
|
+
? 'Error'
|
|
135
|
+
: (active.result.widgets && active.result.widgets.length > 0) || active.result.widget
|
|
136
|
+
? `Widget${active.result.widgets && active.result.widgets.length > 1 ? `s (${active.result.widgets.length})` : ''}`
|
|
137
|
+
: 'Output'}
|
|
138
|
+
</span>
|
|
139
|
+
{#if !active.result.widget && !(active.result.widgets && active.result.widgets.length > 0)}
|
|
140
|
+
<button
|
|
141
|
+
class="ml-auto font-mono text-[10px] px-2 py-0.5 rounded border transition-colors
|
|
142
|
+
{copyState === 'copied' ? 'border-teal/40 text-teal' : 'border-border2 text-text2 hover:text-text1'}"
|
|
143
|
+
onclick={copyOutput}
|
|
144
|
+
>
|
|
145
|
+
{copyState === 'copied' ? 'copied' : 'copy'}
|
|
146
|
+
</button>
|
|
147
|
+
{/if}
|
|
148
|
+
</div>
|
|
149
|
+
{#if active.result.status === 'done' && active.result.widgets && active.result.widgets.length > 0}
|
|
150
|
+
<div class="widget-host flex flex-col gap-3">
|
|
151
|
+
{#key active.id}
|
|
152
|
+
{#each active.result.widgets as w, i (i)}
|
|
153
|
+
<WidgetRenderer type={w.name} data={w.params} {servers} />
|
|
154
|
+
{/each}
|
|
155
|
+
{/key}
|
|
156
|
+
</div>
|
|
157
|
+
{:else if active.result.status === 'done' && active.result.widget}
|
|
158
|
+
<div class="widget-host">
|
|
159
|
+
{#key active.id}
|
|
160
|
+
<WidgetRenderer
|
|
161
|
+
type={active.result.widget.name}
|
|
162
|
+
data={active.result.widget.params}
|
|
163
|
+
{servers}
|
|
164
|
+
/>
|
|
165
|
+
{/key}
|
|
166
|
+
</div>
|
|
167
|
+
{:else}
|
|
168
|
+
<pre class="output-pre font-mono"><code>{
|
|
169
|
+
active.result.status === 'running'
|
|
170
|
+
? '...'
|
|
171
|
+
: active.result.status === 'error'
|
|
172
|
+
? (active.result.error ?? '(unknown error)')
|
|
173
|
+
: safeStringify(active.result.output)
|
|
174
|
+
}</code></pre>
|
|
175
|
+
{/if}
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
<!-- Logs -->
|
|
179
|
+
{#if active.result.logs.length > 0}
|
|
180
|
+
<div>
|
|
181
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
182
|
+
<div
|
|
183
|
+
class="flex items-center gap-1 cursor-pointer select-none"
|
|
184
|
+
onclick={() => logsOpen = !logsOpen}
|
|
185
|
+
>
|
|
186
|
+
<span class="text-[9px] font-mono text-text2 uppercase tracking-wider">Logs ({active.result.logs.length})</span>
|
|
187
|
+
<span class="text-[10px] text-text2 ml-1 transition-transform {logsOpen ? 'rotate-90' : ''}">{@html '▶'}</span>
|
|
188
|
+
</div>
|
|
189
|
+
{#if logsOpen}
|
|
190
|
+
<div class="mt-1 flex flex-col gap-0.5">
|
|
191
|
+
{#each active.result.logs as entry, i (i)}
|
|
192
|
+
<div class="font-mono text-[10px] text-text2">
|
|
193
|
+
<span class="text-text2/60">[+{entry.t}ms]</span>
|
|
194
|
+
<span class="text-text1">{entry.msg}</span>
|
|
195
|
+
</div>
|
|
196
|
+
{/each}
|
|
197
|
+
</div>
|
|
198
|
+
{/if}
|
|
199
|
+
</div>
|
|
200
|
+
{/if}
|
|
201
|
+
</div>
|
|
202
|
+
{:else}
|
|
203
|
+
<div class="flex-1 flex items-center justify-center">
|
|
204
|
+
<span class="font-mono text-xs text-text2">No run yet</span>
|
|
205
|
+
</div>
|
|
206
|
+
{/if}
|
|
207
|
+
</div>
|
|
208
|
+
{/if}
|
|
209
|
+
|
|
210
|
+
<style>
|
|
211
|
+
.run-panel.side {
|
|
212
|
+
width: 100%;
|
|
213
|
+
height: 100%;
|
|
214
|
+
}
|
|
215
|
+
.run-panel.inline {
|
|
216
|
+
width: 100%;
|
|
217
|
+
max-height: 50vh;
|
|
218
|
+
}
|
|
219
|
+
.widget-host {
|
|
220
|
+
background: #0d1117;
|
|
221
|
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
222
|
+
border-radius: 0.375rem;
|
|
223
|
+
padding: 0.6rem 0.7rem;
|
|
224
|
+
min-height: 200px;
|
|
225
|
+
display: flex;
|
|
226
|
+
flex-direction: column;
|
|
227
|
+
}
|
|
228
|
+
.widget-host :global(> *) {
|
|
229
|
+
flex: 1;
|
|
230
|
+
min-height: 0;
|
|
231
|
+
}
|
|
232
|
+
.output-pre {
|
|
233
|
+
background: #0d1117;
|
|
234
|
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
235
|
+
border-radius: 0.375rem;
|
|
236
|
+
padding: 0.6rem 0.7rem;
|
|
237
|
+
margin: 0;
|
|
238
|
+
font-size: 0.7rem;
|
|
239
|
+
line-height: 1.5;
|
|
240
|
+
color: rgb(220, 220, 220);
|
|
241
|
+
overflow-x: auto;
|
|
242
|
+
white-space: pre-wrap;
|
|
243
|
+
word-break: break-word;
|
|
244
|
+
}
|
|
245
|
+
</style>
|
|
@@ -68,10 +68,12 @@
|
|
|
68
68
|
import './rich/gallery.svelte';
|
|
69
69
|
import './rich/carousel.svelte';
|
|
70
70
|
import './rich/chat-input.svelte';
|
|
71
|
+
import './rich/map.svelte';
|
|
71
72
|
// Notebook (1)
|
|
72
73
|
import './notebook/notebook.svelte';
|
|
73
74
|
// Agent browsers (registered as widgets for widget_display)
|
|
74
75
|
import '../agent/RecipeBrowser.svelte';
|
|
76
|
+
import '../agent/ToolBrowser.svelte';
|
|
75
77
|
|
|
76
78
|
/** Native widget types served as custom elements (`<auto-${type}>`). */
|
|
77
79
|
const NATIVE_CUSTOM_ELEMENTS = new Set<string>([
|
|
@@ -80,11 +82,12 @@
|
|
|
80
82
|
// Rich
|
|
81
83
|
'stat-card', 'profile', 'json-viewer', 'chart-rich', 'sankey', 'hemicycle',
|
|
82
84
|
'data-table', 'timeline', 'trombinoscope', 'cards', 'grid-data',
|
|
83
|
-
'js-sandbox', 'log', 'gallery', 'carousel', 'chat-input',
|
|
85
|
+
'js-sandbox', 'log', 'gallery', 'carousel', 'chat-input', 'map',
|
|
84
86
|
// Notebook
|
|
85
87
|
'notebook',
|
|
86
88
|
// Agent browsers
|
|
87
89
|
'recipe-browser',
|
|
90
|
+
'tool-browser',
|
|
88
91
|
]);
|
|
89
92
|
|
|
90
93
|
/** A vanilla renderer: returns cleanup or Promise thereof. Still used for
|
|
@@ -167,7 +170,7 @@
|
|
|
167
170
|
const tag = `auto-${type}`;
|
|
168
171
|
// Instantiate on first mount. `data` setter is reactive via Svelte 5 custom-element.
|
|
169
172
|
const el = document.createElement(tag) as HTMLElement;
|
|
170
|
-
(el as unknown as { data: unknown }).data = plainData;
|
|
173
|
+
(el as unknown as { data: unknown }).data = servers ? { ...plainData, webmcpServers: servers } : plainData;
|
|
171
174
|
const onInteract = (ev: Event) => {
|
|
172
175
|
const ce = ev as CustomEvent<{ action?: string; payload?: unknown }>;
|
|
173
176
|
const action = ce.detail?.action ?? 'interact';
|
|
@@ -188,7 +191,7 @@
|
|
|
188
191
|
$effect(() => {
|
|
189
192
|
const next = plainData;
|
|
190
193
|
if (!isNativeCustomElement || !ceElement) return;
|
|
191
|
-
(ceElement as unknown as { data: unknown }).data = next;
|
|
194
|
+
(ceElement as unknown as { data: unknown }).data = servers ? { ...next, webmcpServers: servers } : next;
|
|
192
195
|
});
|
|
193
196
|
|
|
194
197
|
// ── Vanilla renderer container + lifecycle ────────────
|
|
@@ -230,7 +233,7 @@
|
|
|
230
233
|
let cancelled = false;
|
|
231
234
|
|
|
232
235
|
try {
|
|
233
|
-
const result = renderer(container, untrack(() => plainData));
|
|
236
|
+
const result = renderer(container, untrack(() => servers ? { ...plainData, webmcpServers: servers } : plainData));
|
|
234
237
|
if (result && typeof (result as Promise<unknown>).then === 'function') {
|
|
235
238
|
(result as Promise<void | (() => void)>).then(
|
|
236
239
|
(c) => {
|
|
@@ -277,7 +280,7 @@
|
|
|
277
280
|
runCurrentCleanup();
|
|
278
281
|
container.innerHTML = '';
|
|
279
282
|
try {
|
|
280
|
-
const result = vanillaRenderer(container, data);
|
|
283
|
+
const result = vanillaRenderer(container, servers ? { ...data, webmcpServers: servers } : data);
|
|
281
284
|
if (result && typeof (result as Promise<unknown>).then === 'function') {
|
|
282
285
|
(result as Promise<void | (() => void)>).then(
|
|
283
286
|
(c) => { currentCleanup = c ?? undefined; },
|
|
@@ -1,14 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { canvas } from '@webmcp-auto-ui/sdk/canvas';
|
|
2
2
|
import { findCodeParamName, buildToolArgs } from '@webmcp-auto-ui/sdk';
|
|
3
3
|
import type { CellExecutor, CellExecContext, CellResult, DataServerDescriptor, DataServerTool } from '../shared.js';
|
|
4
4
|
|
|
5
5
|
const PATTERN_PRIMARY = /^.*query_sql$/i;
|
|
6
6
|
const PATTERN_FALLBACK = /^(query|run|execute)(_sql)?$/i;
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
interface SqlHit { srv: DataServerDescriptor; tool: DataServerTool; }
|
|
9
|
+
|
|
10
|
+
function findSqlTool(servers: DataServerDescriptor[]): SqlHit | null {
|
|
9
11
|
for (const p of [PATTERN_PRIMARY, PATTERN_FALLBACK]) {
|
|
10
12
|
for (const srv of servers) {
|
|
11
|
-
for (const t of srv.tools ?? []) if (p.test(t.name)) return t;
|
|
13
|
+
for (const t of srv.tools ?? []) if (p.test(t.name)) return { srv, tool: t };
|
|
12
14
|
}
|
|
13
15
|
}
|
|
14
16
|
return null;
|
|
@@ -17,10 +19,11 @@ function findSqlTool(servers: DataServerDescriptor[]): DataServerTool | null {
|
|
|
17
19
|
export function createSqlExecutor(getServers: () => DataServerDescriptor[]): CellExecutor {
|
|
18
20
|
return async (ctx: CellExecContext): Promise<CellResult> => {
|
|
19
21
|
const startedAt = Date.now();
|
|
20
|
-
const
|
|
21
|
-
if (!
|
|
22
|
+
const hit = findSqlTool(getServers());
|
|
23
|
+
if (!hit) {
|
|
22
24
|
return { ok: false, error: 'No SQL tool available on connected servers.', errorKind: 'schema', durationMs: Date.now() - startedAt };
|
|
23
25
|
}
|
|
26
|
+
const { srv, tool } = hit;
|
|
24
27
|
const sql = (ctx.cell.content ?? '').trim();
|
|
25
28
|
if (!sql) return { ok: true, kind: 'empty', durationMs: Date.now() - startedAt };
|
|
26
29
|
|
|
@@ -36,7 +39,7 @@ export function createSqlExecutor(getServers: () => DataServerDescriptor[]): Cel
|
|
|
36
39
|
|
|
37
40
|
let raw: unknown;
|
|
38
41
|
try {
|
|
39
|
-
raw = await
|
|
42
|
+
raw = await canvas.callTool(srv.name, tool.name, args);
|
|
40
43
|
} catch (err) {
|
|
41
44
|
return { ok: false, error: String((err as { message?: unknown })?.message ?? err), errorKind: 'runtime', durationMs: Date.now() - startedAt };
|
|
42
45
|
}
|
|
@@ -55,19 +55,15 @@ type ModalEl = HTMLElement & {
|
|
|
55
55
|
let _modal: ModalEl | null = null;
|
|
56
56
|
let _cleanup: (() => void) | null = null;
|
|
57
57
|
|
|
58
|
-
function ensureModal(): ModalEl {
|
|
58
|
+
async function ensureModal(): Promise<ModalEl> {
|
|
59
59
|
if (_modal && document.contains(_modal)) return _modal;
|
|
60
60
|
|
|
61
|
-
// Register the CE if not already done (Svelte registers it on first import
|
|
62
|
-
// but we ensure it here for safety).
|
|
63
|
-
if (!customElements.get('auto-import-modal')) {
|
|
64
|
-
// Dynamic import triggers CE registration via Svelte's customElement decorator.
|
|
65
|
-
// Since this module is already bundled with the CE, it's already registered.
|
|
66
|
-
// If somehow not registered, fall back gracefully.
|
|
67
|
-
}
|
|
68
|
-
|
|
69
61
|
const el = document.createElement('auto-import-modal') as ModalEl;
|
|
70
62
|
document.body.appendChild(el);
|
|
63
|
+
// Svelte 5's connectedCallback is async (awaits a microtask before creating
|
|
64
|
+
// $$c), and exported methods are exposed via getters that read $$c. Yield
|
|
65
|
+
// one microtask so el.openModal/closeModal are defined when we call them.
|
|
66
|
+
await Promise.resolve();
|
|
71
67
|
_modal = el;
|
|
72
68
|
return el;
|
|
73
69
|
}
|
|
@@ -84,8 +80,8 @@ export function closeImportModal(): void {
|
|
|
84
80
|
// openAddMdModal
|
|
85
81
|
// ---------------------------------------------------------------------------
|
|
86
82
|
|
|
87
|
-
export function openAddMdModal(onPick: (content: string) => void): void {
|
|
88
|
-
const el = ensureModal();
|
|
83
|
+
export async function openAddMdModal(onPick: (content: string) => void): Promise<void> {
|
|
84
|
+
const el = await ensureModal();
|
|
89
85
|
|
|
90
86
|
// Clean up previous listener
|
|
91
87
|
_cleanup?.();
|
|
@@ -111,8 +107,8 @@ export function openAddMdModal(onPick: (content: string) => void): void {
|
|
|
111
107
|
// openAddRecipeModal
|
|
112
108
|
// ---------------------------------------------------------------------------
|
|
113
109
|
|
|
114
|
-
export function openAddRecipeModal(opts: AddRecipeModalOptions): void {
|
|
115
|
-
const el = ensureModal();
|
|
110
|
+
export async function openAddRecipeModal(opts: AddRecipeModalOptions): Promise<void> {
|
|
111
|
+
const el = await ensureModal();
|
|
116
112
|
|
|
117
113
|
_cleanup?.();
|
|
118
114
|
|
|
@@ -141,11 +137,11 @@ export function openAddRecipeModal(opts: AddRecipeModalOptions): void {
|
|
|
141
137
|
// openRecipeViewerModal
|
|
142
138
|
// ---------------------------------------------------------------------------
|
|
143
139
|
|
|
144
|
-
export function openRecipeViewerModal(
|
|
140
|
+
export async function openRecipeViewerModal(
|
|
145
141
|
recipe: ImportedRecipe,
|
|
146
142
|
onInjectCell: (cell: NotebookCell) => void,
|
|
147
|
-
): void {
|
|
148
|
-
const el = ensureModal();
|
|
143
|
+
): Promise<void> {
|
|
144
|
+
const el = await ensureModal();
|
|
149
145
|
|
|
150
146
|
_cleanup?.();
|
|
151
147
|
|
|
@@ -184,11 +180,11 @@ export function openRecipeViewerModal(
|
|
|
184
180
|
// openToolViewerModal
|
|
185
181
|
// ---------------------------------------------------------------------------
|
|
186
182
|
|
|
187
|
-
export function openToolViewerModal(
|
|
183
|
+
export async function openToolViewerModal(
|
|
188
184
|
tool: McpToolLike,
|
|
189
185
|
onInjectCells: (cells: NotebookCell[]) => void,
|
|
190
|
-
): void {
|
|
191
|
-
const el = ensureModal();
|
|
186
|
+
): Promise<void> {
|
|
187
|
+
const el = await ensureModal();
|
|
192
188
|
|
|
193
189
|
_cleanup?.();
|
|
194
190
|
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
// ---------------------------------------------------------------------------
|
|
19
19
|
|
|
20
20
|
import { filterRecipes, sortRecipes, WEBMCP_RECIPES } from '@webmcp-auto-ui/agent';
|
|
21
|
-
import {
|
|
21
|
+
import { canvas } from '@webmcp-auto-ui/sdk/canvas';
|
|
22
22
|
import { parseBody } from '@webmcp-auto-ui/sdk';
|
|
23
23
|
import MarkdownView from '../../primitives/MarkdownView.svelte';
|
|
24
24
|
import RecipeCodeBlock from '../../recipe/RecipeCodeBlock.svelte';
|
|
@@ -157,7 +157,7 @@
|
|
|
157
157
|
if (d.mcpServers?.length) {
|
|
158
158
|
const fetches = d.mcpServers.map(async (srv) => {
|
|
159
159
|
try {
|
|
160
|
-
const res: any = await
|
|
160
|
+
const res: any = await canvas.callTool(srv.name, 'list_recipes', {});
|
|
161
161
|
const items = extractRecipeItems(res, srv);
|
|
162
162
|
if (items.length) recipes = [...recipes, ...items];
|
|
163
163
|
} catch { /* ignore */ }
|
|
@@ -239,8 +239,9 @@
|
|
|
239
239
|
// Fetch body on demand if missing
|
|
240
240
|
if (!r.body && r.serverName && r.serverName !== 'webmcp') {
|
|
241
241
|
try {
|
|
242
|
-
const res: any = await
|
|
243
|
-
|
|
242
|
+
const res: any = await canvas.callTool(
|
|
243
|
+
r.serverName,
|
|
244
|
+
'get_recipe',
|
|
244
245
|
{ name: r.originalName ?? r.name, id: r.id ?? r.name },
|
|
245
246
|
);
|
|
246
247
|
r = { ...r, body: extractRecipeBody(res) ?? '' };
|