@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.
@@ -32,7 +32,7 @@
32
32
  }
33
33
  </script>
34
34
 
35
- <div class="flex items-center gap-2 w-full">
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="flex-1 h-9 px-3 rounded-lg border border-border2 bg-surface2 text-sm text-text1
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="h-9 px-3 rounded-lg border border-accent2/40 bg-accent2/10 text-accent2
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="h-9 px-3 rounded-lg border border-accent/40 bg-accent/10 text-accent
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 = (globalThis as unknown as { __multiMcp?: { multiClient: McpMultiClient } }).__multiMcp?.multiClient;
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 '&#x25B6;'} 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 '&#x23F1;'} <span class="text-text1">{active.result.durationMs ?? '—'}{active.result.durationMs != null ? 'ms' : ''}</span>
104
+ </span>
105
+ <span class="text-text2">
106
+ {@html '&#x25FC;'} <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 '&#x25D0;'} running</span>
111
+ {:else if active.result.status === 'done'}
112
+ <span class="text-teal">{@html '&#x2713;'} 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 '&#x21BB;'}</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 '&#x25B6;'}</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 { callToolViaPostMessage } from '@webmcp-auto-ui/core';
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
- function findSqlTool(servers: DataServerDescriptor[]): DataServerTool | null {
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 tool = findSqlTool(getServers());
21
- if (!tool) {
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 callToolViaPostMessage(tool.name, args);
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 { callToolViaPostMessage } from '@webmcp-auto-ui/core';
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 callToolViaPostMessage(`${srv.name}_list_recipes`, {});
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 callToolViaPostMessage(
243
- `${r.serverName}_get_recipe`,
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) ?? '' };