@webmcp-auto-ui/ui 2.5.36 → 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 +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 +2 -0
- 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 +3 -3
- package/src/widgets/notebook/notebook.svelte +0 -1
- package/src/widgets/notebook/notebook.ts +5 -36
- package/src/widgets/notebook/share-handlers.ts +71 -1
- package/src/widgets/notebook/shared.ts +81 -69
|
@@ -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>
|
|
@@ -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,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) ?? '' };
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// Collapsed by default.
|
|
7
7
|
// ---------------------------------------------------------------------------
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import { canvas } from '@webmcp-auto-ui/sdk/canvas';
|
|
10
10
|
import { openRecipeViewerModal, openToolViewerModal, type ImportedRecipe } from './import-modal-api.js';
|
|
11
11
|
import type { NotebookCell, NotebookState, DataServerDescriptor } from './shared.js';
|
|
12
12
|
|
|
@@ -73,7 +73,7 @@ export function mountLeftPane(
|
|
|
73
73
|
section.innerHTML = `
|
|
74
74
|
<header class="nb-lp-srv-head">
|
|
75
75
|
<span class="nb-lp-srv-dot"></span>
|
|
76
|
-
<span class="nb-lp-srv-name">${escapeHtml(srv.name)}</span>
|
|
76
|
+
<span class="nb-lp-srv-name">${escapeHtml(srv.serverName ?? srv.label ?? srv.name)}</span>
|
|
77
77
|
</header>
|
|
78
78
|
<div class="nb-lp-srv-groups">
|
|
79
79
|
${srv.recipes?.length ? `
|
|
@@ -134,7 +134,7 @@ export function mountLeftPane(
|
|
|
134
134
|
}
|
|
135
135
|
if (!imported.body) {
|
|
136
136
|
try {
|
|
137
|
-
const res: any = await
|
|
137
|
+
const res: any = await canvas.callTool(srv.name, 'get_recipe', { name: r.name, id: r.name });
|
|
138
138
|
const text = res?.content?.find?.((c: any) => c.type === 'text')?.text;
|
|
139
139
|
if (text) {
|
|
140
140
|
let body = text;
|
|
@@ -36,6 +36,8 @@ export async function render(container: HTMLElement, data: Record<string, unknow
|
|
|
36
36
|
mode: (data.mode as any) ?? 'edit',
|
|
37
37
|
cells: data.cells as any,
|
|
38
38
|
autoRun: (data as any).autoRun === true,
|
|
39
|
+
publishedSlug: (data as any).publishedSlug,
|
|
40
|
+
publishedToken: (data as any).publishedToken,
|
|
39
41
|
});
|
|
40
42
|
|
|
41
43
|
// Live mode runtime overlay (created lazily). Never mutates state.
|
|
@@ -54,7 +56,6 @@ export async function render(container: HTMLElement, data: Record<string, unknow
|
|
|
54
56
|
<div class="nbe-leftpane-slot"></div>
|
|
55
57
|
<div class="nbe-shell">
|
|
56
58
|
<div class="nbe-kicker">
|
|
57
|
-
<span class="nbe-live-toggle-slot"></span>
|
|
58
59
|
<div class="nb-mode-switch" style="margin-left:auto;">
|
|
59
60
|
<button class="nb-mode-edit nb-on">edit</button>
|
|
60
61
|
<button class="nb-mode-view">view</button>
|
|
@@ -104,24 +105,6 @@ export async function render(container: HTMLElement, data: Record<string, unknow
|
|
|
104
105
|
});
|
|
105
106
|
}
|
|
106
107
|
|
|
107
|
-
const hideLiveToggle = (data as any).hideLiveToggle === true;
|
|
108
|
-
|
|
109
|
-
function renderLiveToggle() {
|
|
110
|
-
const slot = shell.querySelector('.nbe-live-toggle-slot') as HTMLElement;
|
|
111
|
-
if (hideLiveToggle) { slot.innerHTML = ''; return; }
|
|
112
|
-
if (state.mode === 'edit') {
|
|
113
|
-
const checked = state.autoRun === true ? 'checked' : '';
|
|
114
|
-
slot.innerHTML = `<label class="nbe-live-toggle" title="Re-execute SQL cells against connected servers when this notebook is opened in view mode."><input type="checkbox" ${checked} />Live data</label>`;
|
|
115
|
-
const cb = slot.querySelector('input[type=checkbox]') as HTMLInputElement;
|
|
116
|
-
cb.addEventListener('change', () => {
|
|
117
|
-
state.autoRun = cb.checked;
|
|
118
|
-
rerender();
|
|
119
|
-
});
|
|
120
|
-
} else {
|
|
121
|
-
slot.innerHTML = '';
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
108
|
function renderLiveBadge() {
|
|
126
109
|
const slot = shell.querySelector('.nbe-live-badge-slot') as HTMLElement;
|
|
127
110
|
if (state.mode === 'view' && state.autoRun === true) {
|
|
@@ -194,7 +177,6 @@ export async function render(container: HTMLElement, data: Record<string, unknow
|
|
|
194
177
|
function rerender() {
|
|
195
178
|
const restore = preserveScrollAround(cellsEl);
|
|
196
179
|
mountHistoryPanel(historyPanel, state, (snap) => { restoreCellFromSnapshot(state, snap); rerender(); });
|
|
197
|
-
renderLiveToggle();
|
|
198
180
|
renderLiveBadge();
|
|
199
181
|
renderEmptyState();
|
|
200
182
|
renderCells();
|
|
@@ -221,9 +203,8 @@ export async function render(container: HTMLElement, data: Record<string, unknow
|
|
|
221
203
|
rerender();
|
|
222
204
|
});
|
|
223
205
|
} else if (which === 'recipe') {
|
|
224
|
-
const mcpServers = (
|
|
225
|
-
.map((s
|
|
226
|
-
.filter((s: any) => s.name);
|
|
206
|
+
const mcpServers = collectDataServers(data)
|
|
207
|
+
.map((s) => ({ name: s.name, url: s.url }));
|
|
227
208
|
openAddRecipeModal({
|
|
228
209
|
mcpServers,
|
|
229
210
|
scope: 'data',
|
|
@@ -287,7 +268,7 @@ export async function render(container: HTMLElement, data: Record<string, unknow
|
|
|
287
268
|
});
|
|
288
269
|
|
|
289
270
|
// Auto-connect data servers declared in the recipe frontmatter (data.servers).
|
|
290
|
-
// The notebook reads MCP state passively from
|
|
271
|
+
// The notebook reads MCP state passively from canvas.dataServers.
|
|
291
272
|
autoConnectFrontmatterServers(data, () => pane.setServers(collectDataServers(data)));
|
|
292
273
|
|
|
293
274
|
// Keep pane servers in sync with canvas changes
|
|
@@ -801,18 +782,6 @@ function injectLayoutStyles(): void {
|
|
|
801
782
|
.nbe-toast.nbe-toast-in { opacity: 1; transform: translateX(-50%) translateY(0); }
|
|
802
783
|
.nbe-toast.nbe-toast-error { color: var(--color-accent2); border-color: var(--color-accent2); }
|
|
803
784
|
|
|
804
|
-
/* Live mode — discreet toggle in header (edit mode only) */
|
|
805
|
-
.nbe-live-toggle {
|
|
806
|
-
display: inline-flex; align-items: center; gap: 6px;
|
|
807
|
-
font-family: var(--font-mono, 'IBM Plex Mono', monospace);
|
|
808
|
-
font-size: 10.5px; color: var(--color-text2);
|
|
809
|
-
letter-spacing: 0.06em; text-transform: uppercase;
|
|
810
|
-
cursor: pointer; user-select: none;
|
|
811
|
-
padding: 2px 7px; border: 1px solid var(--color-border); border-radius: 4px;
|
|
812
|
-
}
|
|
813
|
-
.nbe-live-toggle:hover { color: var(--color-text1); border-color: var(--color-border2); }
|
|
814
|
-
.nbe-live-toggle input { margin: 0; cursor: pointer; }
|
|
815
|
-
|
|
816
785
|
/* Title row + Live badge (view mode + autoRun) */
|
|
817
786
|
.nbe-title-row { display: flex; align-items: baseline; gap: 12px; flex-wrap: wrap; }
|
|
818
787
|
.nbe-title-row .nbe-title { flex: 1; min-width: 0; }
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// ---------------------------------------------------------------------------
|
|
6
6
|
|
|
7
7
|
import { encode, buildShortUrl } from '@webmcp-auto-ui/sdk';
|
|
8
|
+
import { canvasVanilla } from '@webmcp-auto-ui/sdk/canvas-vanilla';
|
|
8
9
|
import type { NotebookState, NotebookCell } from './shared.js';
|
|
9
10
|
|
|
10
11
|
// ---------------------------------------------------------------------------
|
|
@@ -27,8 +28,23 @@ export async function shareAsMarkdown(state: NotebookState): Promise<void> {
|
|
|
27
28
|
triggerDownload(blob, sanitizeFilename(state.title || 'notebook') + '.md');
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Serialize a notebook state as a HyperSkill standalone markdown:
|
|
33
|
+
* ---
|
|
34
|
+
* title: "..."
|
|
35
|
+
* description: "..."
|
|
36
|
+
* servers:
|
|
37
|
+
* - name: foo
|
|
38
|
+
* url: https://...
|
|
39
|
+
* ---
|
|
40
|
+
* <body with ```sql / ```js fenced cells>
|
|
41
|
+
*
|
|
42
|
+
* Re-parsable via @webmcp-auto-ui/core::parseFrontmatter + @webmcp-auto-ui/sdk::parseBody.
|
|
43
|
+
*/
|
|
44
|
+
export function serializeToMarkdown(state: NotebookState): string {
|
|
45
|
+
const fm = buildFrontmatter(state);
|
|
31
46
|
const parts: string[] = [];
|
|
47
|
+
if (fm) parts.push(fm);
|
|
32
48
|
if (state.title) parts.push(`# ${state.title}`, '');
|
|
33
49
|
for (const cell of state.cells) {
|
|
34
50
|
if (cell.type === 'md') {
|
|
@@ -46,6 +62,60 @@ function serializeToMarkdown(state: NotebookState): string {
|
|
|
46
62
|
return parts.join('\n').trim() + '\n';
|
|
47
63
|
}
|
|
48
64
|
|
|
65
|
+
/**
|
|
66
|
+
* Emit YAML frontmatter for HyperSkill format. Reads connected MCP servers from
|
|
67
|
+
* the canvas store. Returns '' when nothing useful to declare (no title, no
|
|
68
|
+
* description, no servers) — caller can skip prepending.
|
|
69
|
+
*/
|
|
70
|
+
function buildFrontmatter(state: NotebookState): string {
|
|
71
|
+
const title = (state.title || '').trim();
|
|
72
|
+
const description = extractDescription(state);
|
|
73
|
+
const servers = collectEnabledServers();
|
|
74
|
+
if (!title && !description && servers.length === 0) return '';
|
|
75
|
+
|
|
76
|
+
const lines: string[] = ['---'];
|
|
77
|
+
if (title) lines.push(`title: ${yamlQuote(title)}`);
|
|
78
|
+
if (description) lines.push(`description: ${yamlQuote(description)}`);
|
|
79
|
+
if (servers.length > 0) {
|
|
80
|
+
lines.push('servers:');
|
|
81
|
+
for (const s of servers) {
|
|
82
|
+
lines.push(` - name: ${yamlQuote(s.name)}`);
|
|
83
|
+
lines.push(` url: ${yamlQuote(s.url)}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
lines.push('---', '');
|
|
87
|
+
return lines.join('\n');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function extractDescription(state: NotebookState): string {
|
|
91
|
+
for (const cell of state.cells) {
|
|
92
|
+
if (cell.type !== 'md') continue;
|
|
93
|
+
const text = stripHtml(cell.content).trim();
|
|
94
|
+
if (!text) continue;
|
|
95
|
+
// First non-heading line of the first md cell.
|
|
96
|
+
const lines = text.split('\n').map((l) => l.trim()).filter(Boolean);
|
|
97
|
+
const prose = lines.find((l) => !/^#{1,6}\s/.test(l) && !/^[-*]\s/.test(l));
|
|
98
|
+
if (prose) return prose.slice(0, 200);
|
|
99
|
+
}
|
|
100
|
+
return '';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function collectEnabledServers(): { name: string; url: string }[] {
|
|
104
|
+
try {
|
|
105
|
+
const servers = canvasVanilla.dataServers ?? [];
|
|
106
|
+
return servers
|
|
107
|
+
.filter((s: any) => s?.enabled && s?.url && s?.name && s.name !== 'autoui' && s.kind !== 'ui' && s.kind !== 'webmcp')
|
|
108
|
+
.map((s: any) => ({ name: String(s.name), url: String(s.url) }));
|
|
109
|
+
} catch {
|
|
110
|
+
return [];
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Quote a YAML scalar safely. Conservative: always double-quote. */
|
|
115
|
+
function yamlQuote(s: string): string {
|
|
116
|
+
return '"' + s.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n') + '"';
|
|
117
|
+
}
|
|
118
|
+
|
|
49
119
|
function stripHtml(s: string): string {
|
|
50
120
|
if (typeof document === 'undefined') return s;
|
|
51
121
|
const d = document.createElement('div');
|