@webmcp-auto-ui/ui 2.5.35 → 2.5.37
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 +1 -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/index.ts +5 -0
- package/src/primitives/MarkdownView.svelte +12 -2
- package/src/recipe/RecipeCodeBlock.svelte +331 -0
- package/src/recipe/RecipeRunModal.svelte +245 -0
- package/src/recipe/types.ts +10 -0
- package/src/widgets/WidgetRenderer.svelte +2 -0
- package/src/widgets/notebook/executors/sql.ts +21 -7
- package/src/widgets/notebook/import-modal-api.ts +15 -43
- package/src/widgets/notebook/import-modal.svelte +36 -66
- package/src/widgets/notebook/left-pane.ts +30 -12
- package/src/widgets/notebook/notebook.svelte +0 -2
- package/src/widgets/notebook/notebook.ts +12 -56
- package/src/widgets/notebook/prose.ts +0 -78
- package/src/widgets/notebook/recipes/notebook.md +0 -6
- package/src/widgets/notebook/resource-extractor.ts +21 -1
- package/src/widgets/notebook/share-handlers.ts +76 -3
- package/src/widgets/notebook/shared.ts +113 -79
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { McpMultiClient } from '@webmcp-auto-ui/core';
|
|
3
|
+
import type { RunResult } from '@webmcp-auto-ui/sdk';
|
|
4
|
+
import { runCode, estimateTokens } from '@webmcp-auto-ui/sdk';
|
|
5
|
+
import { canvas } from '@webmcp-auto-ui/sdk/canvas';
|
|
6
|
+
import { highlightCode } from '../primitives/markdown-renderer.js';
|
|
7
|
+
import type { RecipeBlockAction } from './types.js';
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
code: string;
|
|
11
|
+
lang?: string;
|
|
12
|
+
/**
|
|
13
|
+
* Custom action buttons in the gutter. If omitted AND `onrun` is provided,
|
|
14
|
+
* a default Run button is rendered (back-compat with original flex behavior).
|
|
15
|
+
*/
|
|
16
|
+
actions?: RecipeBlockAction[];
|
|
17
|
+
/**
|
|
18
|
+
* Back-compat: legacy flex usage. When set, a default Run button is rendered
|
|
19
|
+
* that calls runCode(code, lang) and forwards the result to this callback.
|
|
20
|
+
* Ignored when `actions` is provided.
|
|
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>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let {
|
|
32
|
+
code = '',
|
|
33
|
+
lang = 'text',
|
|
34
|
+
actions = undefined,
|
|
35
|
+
onrun,
|
|
36
|
+
scope,
|
|
37
|
+
}: Props = $props();
|
|
38
|
+
|
|
39
|
+
let editable = $state('');
|
|
40
|
+
let runStatus = $state<'idle' | 'running' | 'done' | 'error'>('idle');
|
|
41
|
+
let elapsed = $state(0);
|
|
42
|
+
let liveTokens = $state(0);
|
|
43
|
+
let lastDuration = $state<number | undefined>(undefined);
|
|
44
|
+
let lastTokens = $state<number | undefined>(undefined);
|
|
45
|
+
let timerId: ReturnType<typeof setInterval> | undefined;
|
|
46
|
+
let doneResetId: ReturnType<typeof setTimeout> | undefined;
|
|
47
|
+
|
|
48
|
+
$effect(() => {
|
|
49
|
+
editable = code;
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
let highlightedHtml = $derived(highlightCode(editable, lang || 'plaintext'));
|
|
53
|
+
let preEl: HTMLPreElement | undefined = $state(undefined);
|
|
54
|
+
let taEl: HTMLTextAreaElement | undefined = $state(undefined);
|
|
55
|
+
|
|
56
|
+
function syncScroll() {
|
|
57
|
+
if (preEl && taEl) {
|
|
58
|
+
preEl.scrollTop = taEl.scrollTop;
|
|
59
|
+
preEl.scrollLeft = taEl.scrollLeft;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function formatTokens(n: number): string {
|
|
64
|
+
if (n >= 1000) return `${(n / 1000).toFixed(1)}k`;
|
|
65
|
+
return `${n} tok`;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function startTimer(startAt: number) {
|
|
69
|
+
stopTimer();
|
|
70
|
+
timerId = setInterval(() => {
|
|
71
|
+
elapsed = Math.round(performance.now() - startAt);
|
|
72
|
+
liveTokens = estimateTokens(editable);
|
|
73
|
+
}, 60);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function stopTimer() {
|
|
77
|
+
if (timerId) {
|
|
78
|
+
clearInterval(timerId);
|
|
79
|
+
timerId = undefined;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function handleRun() {
|
|
84
|
+
if (runStatus === 'running') return;
|
|
85
|
+
if (doneResetId) {
|
|
86
|
+
clearTimeout(doneResetId);
|
|
87
|
+
doneResetId = undefined;
|
|
88
|
+
}
|
|
89
|
+
runStatus = 'running';
|
|
90
|
+
elapsed = 0;
|
|
91
|
+
liveTokens = estimateTokens(editable);
|
|
92
|
+
const t0 = performance.now();
|
|
93
|
+
startTimer(t0);
|
|
94
|
+
|
|
95
|
+
const multi = canvas.multiClient as McpMultiClient | undefined;
|
|
96
|
+
const result = await runCode(editable, lang, multi, scope);
|
|
97
|
+
|
|
98
|
+
stopTimer();
|
|
99
|
+
lastDuration = result.durationMs;
|
|
100
|
+
lastTokens = result.tokens;
|
|
101
|
+
runStatus = result.status === 'error' ? 'error' : 'done';
|
|
102
|
+
|
|
103
|
+
onrun?.({ code: editable, lang, result });
|
|
104
|
+
|
|
105
|
+
if (runStatus === 'done') {
|
|
106
|
+
doneResetId = setTimeout(() => {
|
|
107
|
+
if (runStatus === 'done') runStatus = 'idle';
|
|
108
|
+
}, 1000);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Resolve the action list: explicit actions OR back-compat single Run button.
|
|
113
|
+
const resolvedActions = $derived<RecipeBlockAction[]>(
|
|
114
|
+
actions && actions.length > 0
|
|
115
|
+
? actions
|
|
116
|
+
: onrun
|
|
117
|
+
? [{
|
|
118
|
+
icon: '▶',
|
|
119
|
+
label: 'Run',
|
|
120
|
+
onclick: () => handleRun(),
|
|
121
|
+
}]
|
|
122
|
+
: []
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
// For the single-action Run case, we render run-status icon + stats inline.
|
|
126
|
+
const isSingleRunAction = $derived(
|
|
127
|
+
!actions && !!onrun && resolvedActions.length === 1
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
function handleActionClick(a: RecipeBlockAction) {
|
|
131
|
+
a.onclick(editable, lang);
|
|
132
|
+
}
|
|
133
|
+
</script>
|
|
134
|
+
|
|
135
|
+
<div class="code-block">
|
|
136
|
+
<div class="gutter">
|
|
137
|
+
{#if isSingleRunAction}
|
|
138
|
+
<button
|
|
139
|
+
type="button"
|
|
140
|
+
class="run-btn {runStatus}"
|
|
141
|
+
onclick={() => handleRun()}
|
|
142
|
+
disabled={runStatus === 'running'}
|
|
143
|
+
title={runStatus === 'running' ? 'Running...' : 'Run'}
|
|
144
|
+
>
|
|
145
|
+
<span class="icon">
|
|
146
|
+
{#if runStatus === 'running'}
|
|
147
|
+
{@html '◐'}
|
|
148
|
+
{:else if runStatus === 'done'}
|
|
149
|
+
{@html '✓'}
|
|
150
|
+
{:else if runStatus === 'error'}
|
|
151
|
+
!
|
|
152
|
+
{:else}
|
|
153
|
+
{@html '▶'}
|
|
154
|
+
{/if}
|
|
155
|
+
</span>
|
|
156
|
+
{#if runStatus === 'running' || lastDuration !== undefined}
|
|
157
|
+
<span class="stats">
|
|
158
|
+
<span class="t">
|
|
159
|
+
{runStatus === 'running' ? `${elapsed}ms` : `${lastDuration}ms`}
|
|
160
|
+
</span>
|
|
161
|
+
<span class="tok">
|
|
162
|
+
{formatTokens(runStatus === 'running' ? liveTokens : (lastTokens ?? 0))}
|
|
163
|
+
</span>
|
|
164
|
+
</span>
|
|
165
|
+
{/if}
|
|
166
|
+
</button>
|
|
167
|
+
{:else}
|
|
168
|
+
{#each resolvedActions as a}
|
|
169
|
+
<button
|
|
170
|
+
type="button"
|
|
171
|
+
class="action-btn {a.variant ?? 'default'}"
|
|
172
|
+
onclick={() => handleActionClick(a)}
|
|
173
|
+
title={a.label ?? a.icon}
|
|
174
|
+
>
|
|
175
|
+
<span class="icon">{@html a.icon}</span>
|
|
176
|
+
</button>
|
|
177
|
+
{/each}
|
|
178
|
+
{/if}
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
<div class="editor-wrap">
|
|
182
|
+
{#if lang && lang !== 'text'}
|
|
183
|
+
<div class="lang-tag font-mono">{lang}</div>
|
|
184
|
+
{/if}
|
|
185
|
+
<pre bind:this={preEl} class="editor highlight-layer hljs font-mono" aria-hidden="true"><code class="hljs language-{lang || 'plaintext'}">{@html highlightedHtml}</code></pre>
|
|
186
|
+
<textarea
|
|
187
|
+
bind:this={taEl}
|
|
188
|
+
bind:value={editable}
|
|
189
|
+
onscroll={syncScroll}
|
|
190
|
+
spellcheck="false"
|
|
191
|
+
autocomplete="off"
|
|
192
|
+
rows={Math.min(Math.max(editable.split('\n').length, 3), 20)}
|
|
193
|
+
class="editor input-layer font-mono"
|
|
194
|
+
></textarea>
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
|
|
198
|
+
<style>
|
|
199
|
+
.code-block {
|
|
200
|
+
display: flex;
|
|
201
|
+
align-items: stretch;
|
|
202
|
+
gap: 6px;
|
|
203
|
+
margin: 0.5rem 0;
|
|
204
|
+
}
|
|
205
|
+
.gutter {
|
|
206
|
+
display: flex;
|
|
207
|
+
flex-direction: column;
|
|
208
|
+
gap: 4px;
|
|
209
|
+
align-items: stretch;
|
|
210
|
+
}
|
|
211
|
+
.run-btn,
|
|
212
|
+
.action-btn {
|
|
213
|
+
display: flex;
|
|
214
|
+
flex-direction: column;
|
|
215
|
+
align-items: center;
|
|
216
|
+
justify-content: center;
|
|
217
|
+
gap: 4px;
|
|
218
|
+
min-width: 56px;
|
|
219
|
+
padding: 8px 6px;
|
|
220
|
+
border-radius: 0.375rem;
|
|
221
|
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
222
|
+
background: #0d1117;
|
|
223
|
+
color: rgb(180, 180, 180);
|
|
224
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
225
|
+
font-size: 11px;
|
|
226
|
+
cursor: pointer;
|
|
227
|
+
transition: background 120ms ease, color 120ms ease, border-color 120ms ease;
|
|
228
|
+
}
|
|
229
|
+
.run-btn:hover:not(:disabled),
|
|
230
|
+
.action-btn:hover:not(:disabled) {
|
|
231
|
+
background: #161b22;
|
|
232
|
+
color: #fff;
|
|
233
|
+
}
|
|
234
|
+
.run-btn:disabled,
|
|
235
|
+
.action-btn:disabled {
|
|
236
|
+
cursor: progress;
|
|
237
|
+
}
|
|
238
|
+
.run-btn .icon,
|
|
239
|
+
.action-btn .icon {
|
|
240
|
+
font-size: 14px;
|
|
241
|
+
line-height: 1;
|
|
242
|
+
}
|
|
243
|
+
.run-btn.running .icon {
|
|
244
|
+
animation: spin 0.9s linear infinite;
|
|
245
|
+
}
|
|
246
|
+
.run-btn.done,
|
|
247
|
+
.action-btn.success {
|
|
248
|
+
color: rgb(74, 222, 128);
|
|
249
|
+
border-color: rgba(74, 222, 128, 0.4);
|
|
250
|
+
}
|
|
251
|
+
.run-btn.error,
|
|
252
|
+
.action-btn.error {
|
|
253
|
+
color: rgb(248, 113, 113);
|
|
254
|
+
border-color: rgba(248, 113, 113, 0.4);
|
|
255
|
+
}
|
|
256
|
+
.run-btn .stats {
|
|
257
|
+
display: flex;
|
|
258
|
+
flex-direction: column;
|
|
259
|
+
gap: 1px;
|
|
260
|
+
font-size: 9px;
|
|
261
|
+
color: rgb(160, 160, 160);
|
|
262
|
+
line-height: 1.2;
|
|
263
|
+
}
|
|
264
|
+
.run-btn .stats .t { font-weight: 600; }
|
|
265
|
+
.run-btn .stats .tok { opacity: 0.8; }
|
|
266
|
+
@keyframes spin {
|
|
267
|
+
from { transform: rotate(0deg); }
|
|
268
|
+
to { transform: rotate(360deg); }
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.editor-wrap {
|
|
272
|
+
flex: 1;
|
|
273
|
+
min-width: 0;
|
|
274
|
+
position: relative;
|
|
275
|
+
}
|
|
276
|
+
.lang-tag {
|
|
277
|
+
position: absolute;
|
|
278
|
+
top: 4px;
|
|
279
|
+
right: 8px;
|
|
280
|
+
font-size: 9px;
|
|
281
|
+
color: rgba(255, 255, 255, 0.35);
|
|
282
|
+
text-transform: lowercase;
|
|
283
|
+
pointer-events: none;
|
|
284
|
+
}
|
|
285
|
+
.editor {
|
|
286
|
+
display: block;
|
|
287
|
+
width: 100%;
|
|
288
|
+
box-sizing: border-box;
|
|
289
|
+
background: #0d1117;
|
|
290
|
+
color: rgb(220, 220, 220);
|
|
291
|
+
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
292
|
+
border-radius: 0.375rem;
|
|
293
|
+
padding: 0.7rem;
|
|
294
|
+
font-size: 0.7rem;
|
|
295
|
+
line-height: 1.5;
|
|
296
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
297
|
+
white-space: pre;
|
|
298
|
+
tab-size: 2;
|
|
299
|
+
margin: 0;
|
|
300
|
+
}
|
|
301
|
+
.highlight-layer {
|
|
302
|
+
position: absolute;
|
|
303
|
+
inset: 0;
|
|
304
|
+
overflow: auto;
|
|
305
|
+
pointer-events: none;
|
|
306
|
+
border-color: transparent;
|
|
307
|
+
}
|
|
308
|
+
.highlight-layer :global(code.hljs) {
|
|
309
|
+
background: transparent;
|
|
310
|
+
padding: 0;
|
|
311
|
+
}
|
|
312
|
+
.input-layer {
|
|
313
|
+
position: relative;
|
|
314
|
+
color: transparent;
|
|
315
|
+
caret-color: rgb(220, 220, 220);
|
|
316
|
+
background: transparent;
|
|
317
|
+
resize: vertical;
|
|
318
|
+
outline: none;
|
|
319
|
+
overflow: auto;
|
|
320
|
+
}
|
|
321
|
+
.input-layer::selection {
|
|
322
|
+
color: transparent;
|
|
323
|
+
background: rgba(96, 165, 250, 0.35);
|
|
324
|
+
}
|
|
325
|
+
.input-layer:focus {
|
|
326
|
+
border-color: rgba(96, 165, 250, 0.45);
|
|
327
|
+
}
|
|
328
|
+
.editor-wrap {
|
|
329
|
+
min-height: 0;
|
|
330
|
+
}
|
|
331
|
+
</style>
|
|
@@ -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>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface RecipeBlockAction {
|
|
2
|
+
/** Glyph or HTML entity. Examples: '▶', '+', '⧉', '✓' */
|
|
3
|
+
icon: string;
|
|
4
|
+
/** Tooltip text. Defaults to icon. */
|
|
5
|
+
label?: string;
|
|
6
|
+
/** Visual variant. */
|
|
7
|
+
variant?: 'default' | 'success' | 'error';
|
|
8
|
+
/** Click handler. Receives the (possibly edited) code + lang. */
|
|
9
|
+
onclick: (code: string, lang: string) => void;
|
|
10
|
+
}
|
|
@@ -72,6 +72,7 @@
|
|
|
72
72
|
import './notebook/notebook.svelte';
|
|
73
73
|
// Agent browsers (registered as widgets for widget_display)
|
|
74
74
|
import '../agent/RecipeBrowser.svelte';
|
|
75
|
+
import '../agent/ToolBrowser.svelte';
|
|
75
76
|
|
|
76
77
|
/** Native widget types served as custom elements (`<auto-${type}>`). */
|
|
77
78
|
const NATIVE_CUSTOM_ELEMENTS = new Set<string>([
|
|
@@ -85,6 +86,7 @@
|
|
|
85
86
|
'notebook',
|
|
86
87
|
// Agent browsers
|
|
87
88
|
'recipe-browser',
|
|
89
|
+
'tool-browser',
|
|
88
90
|
]);
|
|
89
91
|
|
|
90
92
|
/** A vanilla renderer: returns cleanup or Promise thereof. Still used for
|
|
@@ -1,13 +1,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
1
|
+
import { canvas } from '@webmcp-auto-ui/sdk/canvas';
|
|
2
|
+
import { findCodeParamName, buildToolArgs } from '@webmcp-auto-ui/sdk';
|
|
3
|
+
import type { CellExecutor, CellExecContext, CellResult, DataServerDescriptor, DataServerTool } from '../shared.js';
|
|
3
4
|
|
|
4
5
|
const PATTERN_PRIMARY = /^.*query_sql$/i;
|
|
5
6
|
const PATTERN_FALLBACK = /^(query|run|execute)(_sql)?$/i;
|
|
6
7
|
|
|
7
|
-
|
|
8
|
+
interface SqlHit { srv: DataServerDescriptor; tool: DataServerTool; }
|
|
9
|
+
|
|
10
|
+
function findSqlTool(servers: DataServerDescriptor[]): SqlHit | null {
|
|
8
11
|
for (const p of [PATTERN_PRIMARY, PATTERN_FALLBACK]) {
|
|
9
12
|
for (const srv of servers) {
|
|
10
|
-
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 };
|
|
11
14
|
}
|
|
12
15
|
}
|
|
13
16
|
return null;
|
|
@@ -16,16 +19,27 @@ function findSqlTool(servers: DataServerDescriptor[]): string | null {
|
|
|
16
19
|
export function createSqlExecutor(getServers: () => DataServerDescriptor[]): CellExecutor {
|
|
17
20
|
return async (ctx: CellExecContext): Promise<CellResult> => {
|
|
18
21
|
const startedAt = Date.now();
|
|
19
|
-
const
|
|
20
|
-
if (!
|
|
22
|
+
const hit = findSqlTool(getServers());
|
|
23
|
+
if (!hit) {
|
|
21
24
|
return { ok: false, error: 'No SQL tool available on connected servers.', errorKind: 'schema', durationMs: Date.now() - startedAt };
|
|
22
25
|
}
|
|
26
|
+
const { srv, tool } = hit;
|
|
23
27
|
const sql = (ctx.cell.content ?? '').trim();
|
|
24
28
|
if (!sql) return { ok: true, kind: 'empty', durationMs: Date.now() - startedAt };
|
|
25
29
|
|
|
30
|
+
// Build args from the tool's inputSchema:
|
|
31
|
+
// 1. Pick the code-carrying param (query / sql / statement / ...) via findCodeParamName.
|
|
32
|
+
// 2. Auto-infer required params (e.g. `schema` enum from FROM/JOIN regex).
|
|
33
|
+
// 3. Merge cell-level overrides from cell.args (parsed from `-- @meta {...}` line).
|
|
34
|
+
const codeParam = findCodeParamName(tool.inputSchema) ?? 'sql';
|
|
35
|
+
const auto = buildToolArgs(tool.inputSchema, codeParam, sql, 'sql');
|
|
36
|
+
const args: Record<string, unknown> = { ...auto, ...(ctx.cell.args ?? {}) };
|
|
37
|
+
// Code param is owned by the cell content, never overridable via @meta
|
|
38
|
+
args[codeParam] = sql;
|
|
39
|
+
|
|
26
40
|
let raw: unknown;
|
|
27
41
|
try {
|
|
28
|
-
raw = await
|
|
42
|
+
raw = await canvas.callTool(srv.name, tool.name, args);
|
|
29
43
|
} catch (err) {
|
|
30
44
|
return { ok: false, error: String((err as { message?: unknown })?.message ?? err), errorKind: 'runtime', durationMs: Date.now() - startedAt };
|
|
31
45
|
}
|