@webmcp-auto-ui/ui 2.5.31 → 2.5.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/package.json +15 -2
  2. package/src/agent/DiagnosticModal.svelte +126 -50
  3. package/src/agent/EphemeralBubble.svelte +13 -3
  4. package/src/agent/MCPserversList.svelte +147 -0
  5. package/src/agent/McpConnector.svelte +10 -1
  6. package/src/agent/RecipeBrowser.svelte +384 -0
  7. package/src/agent/RemoteMCPserversDemo.svelte +5 -121
  8. package/src/agent/ToolBrowser.svelte +133 -0
  9. package/src/agent/WebMCPserversList.svelte +2 -0
  10. package/src/agent/useAgentLoop.svelte.ts +396 -0
  11. package/src/base/chat-inline.svelte +64 -0
  12. package/src/base/dialog-content.svelte +3 -1
  13. package/src/components/HeaderControls.svelte +78 -0
  14. package/src/index.ts +13 -35
  15. package/src/stores/canvas.svelte.ts +0 -6
  16. package/src/widgets/SafeImage.svelte +67 -0
  17. package/src/widgets/WidgetRenderer.svelte +153 -78
  18. package/src/widgets/notebook/executors/index.ts +0 -1
  19. package/src/widgets/notebook/executors/sql.ts +32 -182
  20. package/src/widgets/notebook/import-modal-api.ts +237 -0
  21. package/src/widgets/notebook/import-modal.svelte +738 -0
  22. package/src/widgets/notebook/left-pane.ts +1 -1
  23. package/src/widgets/notebook/notebook.svelte +75 -0
  24. package/src/widgets/notebook/notebook.ts +38 -73
  25. package/src/widgets/notebook/prose.ts +6 -3
  26. package/src/widgets/notebook/shared.ts +68 -49
  27. package/src/widgets/rich/cards.svelte +74 -0
  28. package/src/widgets/rich/carousel.svelte +126 -0
  29. package/src/widgets/rich/chart-rich.svelte +221 -0
  30. package/src/widgets/rich/chat-input.svelte +52 -0
  31. package/src/widgets/rich/data-table.svelte +132 -0
  32. package/src/widgets/rich/gallery.svelte +115 -0
  33. package/src/widgets/rich/grid-data.svelte +85 -0
  34. package/src/widgets/rich/hemicycle.svelte +95 -0
  35. package/src/widgets/rich/js-sandbox.svelte +67 -0
  36. package/src/widgets/rich/json-viewer.svelte +82 -0
  37. package/src/widgets/rich/log.svelte +62 -0
  38. package/src/widgets/rich/profile.svelte +91 -0
  39. package/src/widgets/rich/sankey.svelte +73 -0
  40. package/src/widgets/rich/stat-card.svelte +60 -0
  41. package/src/widgets/rich/timeline.svelte +95 -0
  42. package/src/widgets/rich/trombinoscope.svelte +87 -0
  43. package/src/widgets/simple/actions.svelte +36 -0
  44. package/src/widgets/simple/alert.svelte +52 -0
  45. package/src/widgets/simple/chart.svelte +38 -0
  46. package/src/widgets/simple/code.svelte +30 -0
  47. package/src/widgets/simple/kv.svelte +31 -0
  48. package/src/widgets/simple/list.svelte +35 -0
  49. package/src/widgets/simple/stat.svelte +36 -0
  50. package/src/widgets/simple/tags.svelte +34 -0
  51. package/src/widgets/simple/text.svelte +130 -0
  52. package/src/widgets/helpers/safe-image.ts +0 -78
  53. package/src/widgets/notebook/import-modals.ts +0 -560
  54. package/src/widgets/notebook/recipe-browser.ts +0 -350
  55. package/src/widgets/rich/cards.ts +0 -181
  56. package/src/widgets/rich/carousel.ts +0 -319
  57. package/src/widgets/rich/chart-rich.ts +0 -386
  58. package/src/widgets/rich/d3.ts +0 -503
  59. package/src/widgets/rich/data-table.ts +0 -342
  60. package/src/widgets/rich/gallery.ts +0 -350
  61. package/src/widgets/rich/grid-data.ts +0 -173
  62. package/src/widgets/rich/hemicycle.ts +0 -313
  63. package/src/widgets/rich/js-sandbox.ts +0 -122
  64. package/src/widgets/rich/json-viewer.ts +0 -202
  65. package/src/widgets/rich/log.ts +0 -143
  66. package/src/widgets/rich/map.ts +0 -218
  67. package/src/widgets/rich/profile.ts +0 -256
  68. package/src/widgets/rich/sankey.ts +0 -257
  69. package/src/widgets/rich/stat-card.ts +0 -125
  70. package/src/widgets/rich/timeline.ts +0 -179
  71. package/src/widgets/rich/trombinoscope.ts +0 -246
  72. package/src/widgets/simple/actions.ts +0 -89
  73. package/src/widgets/simple/alert.ts +0 -100
  74. package/src/widgets/simple/chart.ts +0 -189
  75. package/src/widgets/simple/code.ts +0 -79
  76. package/src/widgets/simple/kv.ts +0 -68
  77. package/src/widgets/simple/list.ts +0 -89
  78. package/src/widgets/simple/stat.ts +0 -58
  79. package/src/widgets/simple/tags.ts +0 -125
  80. package/src/widgets/simple/text.ts +0 -198
@@ -0,0 +1,396 @@
1
+ /**
2
+ * useAgentLoop — shared composable for agent loop setup.
3
+ *
4
+ * Centralises the duplicated provider selection, Gemma WASM lifecycle,
5
+ * HawkProvider routing, smart LLM defaults, and TokenTracker wiring that
6
+ * was copy-pasted across template, recipes, boilerplate, and showcase.
7
+ *
8
+ * Usage (Svelte 5, .svelte.ts files or <script lang="ts"> blocks):
9
+ *
10
+ * const agent = createAgentLoop({
11
+ * chatApiBase: `${base}/api/chat`,
12
+ * hawkApiBase: `${base}/api/hawk`, // optional
13
+ * enabledProviders: ['remote', 'wasm'], // optional, defaults to all
14
+ * });
15
+ *
16
+ * // Read reactive state
17
+ * agent.gemmaStatus // 'idle' | 'loading' | 'ready' | 'error'
18
+ * agent.gemmaProgress // 0–100
19
+ * agent.tokenMetrics // { inputTokens, outputTokens, … }
20
+ *
21
+ * // Use inside runAgentLoop
22
+ * const result = await runAgentLoop(msg, {
23
+ * provider: agent.getProvider(),
24
+ * ...agent.runOptions(),
25
+ * callbacks: { ... },
26
+ * });
27
+ */
28
+
29
+ import { canvas } from '@webmcp-auto-ui/sdk/canvas';
30
+ import {
31
+ RemoteLLMProvider,
32
+ WasmProvider,
33
+ HawkProvider,
34
+ LocalLLMProvider,
35
+ TokenTracker,
36
+ } from '@webmcp-auto-ui/agent';
37
+ import type { LLMProvider, AgentCallbacks } from '@webmcp-auto-ui/agent';
38
+
39
+ // ── Types ──────────────────────────────────────────────────────────────────
40
+
41
+ export type GemmaStatus = 'idle' | 'loading' | 'ready' | 'error';
42
+
43
+ export type EnabledProvider = 'remote' | 'wasm' | 'hawk' | 'local';
44
+
45
+ /**
46
+ * Options for a custom LLM provider registered via `customProviders`.
47
+ * The factory receives the same progress/status callbacks that useAgentLoop
48
+ * wires up for Gemma — so Gemma-style loading UI works out of the box.
49
+ */
50
+ export interface CustomProviderOpts {
51
+ model: string;
52
+ onProgress: (progress: number, status: string, loadedBytes?: number, totalBytes?: number) => void;
53
+ onStatusChange: (status: GemmaStatus) => void;
54
+ }
55
+
56
+ export interface UseAgentLoopOptions {
57
+ /** Base URL for the remote (Claude) proxy, e.g. `${base}/api/chat` */
58
+ chatApiBase: string;
59
+ /** Base URL for the Hawk proxy, e.g. `${base}/api/hawk` (optional) */
60
+ hawkApiBase?: string;
61
+ /** Restrict which providers can be selected. Defaults to all. */
62
+ enabledProviders?: EnabledProvider[];
63
+ /** Ollama base URL (only used when 'local' provider is enabled) */
64
+ localUrl?: string;
65
+ /** Ollama model name override */
66
+ localModel?: string;
67
+ /** Gemma contextSize for WasmProvider (default 32768) */
68
+ gemmaContextSize?: number;
69
+ /**
70
+ * Custom provider registrations. Each entry maps a model-name prefix to a
71
+ * factory. The factory receives the standard progress/status callbacks so
72
+ * the GemmaLoader UI works transparently (e.g. TransformersProvider).
73
+ *
74
+ * Example (flex):
75
+ * customProviders: [{ prefix: 'transformers-', factory: (opts) => new TransformersProvider(opts) }]
76
+ */
77
+ customProviders?: Array<{ prefix: string; factory: (opts: CustomProviderOpts) => LLMProvider }>;
78
+ /**
79
+ * Optional observer whose callbacks are merged (via `mergeCallbacks`) into
80
+ * every `runAgentLoop` call. Designed to accept a `TraceObserver` directly.
81
+ *
82
+ * Example (flex):
83
+ * observer: traceObserver
84
+ */
85
+ observer?: { callbacks: Partial<AgentCallbacks> };
86
+ }
87
+
88
+ export interface AgentRunOptions {
89
+ /** Schema sanitize option (smart default from LLM) */
90
+ schemaSanitize: boolean;
91
+ /** Schema flatten option (smart default from LLM) */
92
+ schemaFlatten: boolean;
93
+ /** Max tool result length */
94
+ maxResultLength: number;
95
+ /** Whether to truncate tool results */
96
+ truncateResults: boolean;
97
+ /** Compress history length (false = disabled) */
98
+ compressHistory: false | number;
99
+ /** Whether prompt caching is enabled */
100
+ cacheEnabled: boolean;
101
+ /** LLM temperature */
102
+ temperature: number;
103
+ }
104
+
105
+ // ── Factory ────────────────────────────────────────────────────────────────
106
+
107
+ export function createAgentLoop(options: UseAgentLoopOptions) {
108
+ const {
109
+ chatApiBase,
110
+ hawkApiBase,
111
+ enabledProviders,
112
+ localUrl = 'http://localhost:11434',
113
+ localModel = 'llama3.2',
114
+ gemmaContextSize = 32_768,
115
+ customProviders = [],
116
+ observer,
117
+ } = options;
118
+
119
+ const canUse = (p: EnabledProvider) =>
120
+ !enabledProviders || enabledProviders.includes(p);
121
+
122
+ // ── Provider singletons ──────────────────────────────────────────────────
123
+ const remoteProvider = new RemoteLLMProvider({ proxyUrl: chatApiBase });
124
+ let hawkProvider: HawkProvider | null = null;
125
+
126
+ // ── Gemma state ──────────────────────────────────────────────────────────
127
+ let gemmaProvider = $state<WasmProvider | null>(null);
128
+ let gemmaStatus = $state<GemmaStatus>('idle');
129
+ let gemmaProgress = $state(0);
130
+ let gemmaElapsed = $state(0);
131
+ let gemmaLoadedMB = $state(0);
132
+ let gemmaTotalMB = $state(0);
133
+ let gemmaLoadStart = 0;
134
+ let gemmaTimerInterval: ReturnType<typeof setInterval> | null = null;
135
+
136
+ // ── Smart defaults state ─────────────────────────────────────────────────
137
+ let schemaSanitize = $state(true);
138
+ let schemaFlatten = $state(false);
139
+ let maxResultLength = $state(10_000);
140
+ let truncateResults = $state(false);
141
+ let compressHistoryEnabled = $state(false);
142
+ let compressPreview = $state(500);
143
+ let cacheEnabled = $state(true);
144
+ let temperature = $state(1.0);
145
+
146
+ // ── TokenTracker ─────────────────────────────────────────────────────────
147
+ const tokenTracker = new TokenTracker();
148
+ let tokenMetrics = $state(tokenTracker.metrics);
149
+ tokenTracker.subscribe((m) => { tokenMetrics = m; });
150
+
151
+ // ── Internal helpers ─────────────────────────────────────────────────────
152
+ function _startGemmaTimer() {
153
+ gemmaLoadStart = Date.now();
154
+ gemmaElapsed = 0;
155
+ if (gemmaTimerInterval) clearInterval(gemmaTimerInterval);
156
+ gemmaTimerInterval = setInterval(() => {
157
+ gemmaElapsed = Math.floor((Date.now() - gemmaLoadStart) / 1000);
158
+ }, 1000);
159
+ }
160
+
161
+ function _stopGemmaTimer() {
162
+ if (gemmaTimerInterval) { clearInterval(gemmaTimerInterval); gemmaTimerInterval = null; }
163
+ }
164
+
165
+ function _makeWasmProvider(model: string): WasmProvider {
166
+ return new WasmProvider({
167
+ // WasmProviderOptions.model is typed as WasmModelId; cast via unknown for safety
168
+ model: model as any,
169
+ contextSize: gemmaContextSize,
170
+ onProgress: (p, _s, loaded, total) => {
171
+ gemmaProgress = p * 100;
172
+ if (loaded) gemmaLoadedMB = Math.round(loaded / 1048576 * 100) / 100;
173
+ if (total) gemmaTotalMB = Math.round(total / 1048576 * 100) / 100;
174
+ },
175
+ onStatusChange: (s: GemmaStatus) => {
176
+ gemmaStatus = s;
177
+ if (s === 'loading') _startGemmaTimer();
178
+ if (s === 'ready' || s === 'error') _stopGemmaTimer();
179
+ },
180
+ });
181
+ }
182
+
183
+ // ── Public API ───────────────────────────────────────────────────────────
184
+
185
+ return {
186
+ // Gemma reactive state
187
+ get gemmaProvider() { return gemmaProvider; },
188
+ get gemmaStatus() { return gemmaStatus; },
189
+ get gemmaProgress() { return gemmaProgress; },
190
+ get gemmaElapsed() { return gemmaElapsed; },
191
+ get gemmaLoadedMB() { return gemmaLoadedMB; },
192
+ get gemmaTotalMB() { return gemmaTotalMB; },
193
+
194
+ // TokenTracker
195
+ get tokenTracker() { return tokenTracker; },
196
+ get tokenMetrics() { return tokenMetrics; },
197
+
198
+ // Smart defaults (read/write)
199
+ get schemaSanitize() { return schemaSanitize; },
200
+ set schemaSanitize(v: boolean) { schemaSanitize = v; },
201
+ get schemaFlatten() { return schemaFlatten; },
202
+ set schemaFlatten(v: boolean) { schemaFlatten = v; },
203
+ get maxResultLength() { return maxResultLength; },
204
+ set maxResultLength(v: number) { maxResultLength = v; },
205
+ get truncateResults() { return truncateResults; },
206
+ set truncateResults(v: boolean) { truncateResults = v; },
207
+ get compressHistoryEnabled() { return compressHistoryEnabled; },
208
+ set compressHistoryEnabled(v: boolean) { compressHistoryEnabled = v; },
209
+ get compressPreview() { return compressPreview; },
210
+ set compressPreview(v: number) { compressPreview = v; },
211
+ get cacheEnabled() { return cacheEnabled; },
212
+ set cacheEnabled(v: boolean) { cacheEnabled = v; },
213
+ get temperature() { return temperature; },
214
+ set temperature(v: number) { temperature = v; },
215
+
216
+ /**
217
+ * Apply smart defaults for the current `canvas.llm` value.
218
+ * Call this imperatively when the LLM changes (e.g. in a `$effect`).
219
+ */
220
+ applySmartDefaults() {
221
+ const llm = canvas.llm;
222
+ const isGemma = llm.startsWith('gemma');
223
+ const isLocal = llm === 'local';
224
+ schemaSanitize = isLocal ? true : !isGemma;
225
+ schemaFlatten = isGemma || isLocal;
226
+ truncateResults = isGemma || isLocal;
227
+ compressHistoryEnabled = isLocal;
228
+ if (isGemma) {
229
+ maxResultLength = 2_000;
230
+ temperature = 0.7;
231
+ cacheEnabled = false;
232
+ } else if (isLocal) {
233
+ maxResultLength = 3_000;
234
+ } else {
235
+ maxResultLength = 10_000;
236
+ temperature = 1.0;
237
+ cacheEnabled = true;
238
+ }
239
+ },
240
+
241
+ /**
242
+ * Resolve the LLM provider for the current `canvas.llm`.
243
+ * Manages WasmProvider singleton lifecycle (create / swap model).
244
+ */
245
+ getProvider(): LLMProvider {
246
+ const llm = canvas.llm;
247
+
248
+ // Hawk (server-side proxy)
249
+ if (canUse('hawk') && llm.startsWith('hawk-')) {
250
+ const model = llm.slice(5);
251
+ if (!hawkProvider || hawkProvider.model !== model) {
252
+ hawkProvider = new HawkProvider({ proxyUrl: hawkApiBase ?? `${chatApiBase}/hawk`, model });
253
+ }
254
+ return hawkProvider;
255
+ }
256
+
257
+ // Local Ollama
258
+ if (canUse('local') && llm === 'local') {
259
+ return new LocalLLMProvider({ baseUrl: localUrl, model: localModel });
260
+ }
261
+
262
+ // Gemma WASM
263
+ if (canUse('wasm') && (llm === 'gemma-e2b' || llm === 'gemma-e4b')) {
264
+ if (gemmaProvider && gemmaProvider.model !== llm) {
265
+ this.unloadGemma();
266
+ }
267
+ if (!gemmaProvider) {
268
+ gemmaProvider = _makeWasmProvider(llm);
269
+ }
270
+ return gemmaProvider;
271
+ }
272
+
273
+ // Custom providers (e.g. TransformersProvider registered by the host app)
274
+ for (const { prefix, factory } of customProviders) {
275
+ if (llm.startsWith(prefix)) {
276
+ // Custom providers share the gemma progress/status slots so GemmaLoader
277
+ // works transparently without extra state in the host component.
278
+ if (gemmaProvider && (gemmaProvider as unknown as { model?: string }).model !== llm) {
279
+ this.unloadGemma();
280
+ }
281
+ if (!gemmaProvider) {
282
+ const provider = factory({
283
+ model: llm,
284
+ onProgress: (p, _s, loaded, total) => {
285
+ gemmaProgress = p * 100;
286
+ if (loaded) gemmaLoadedMB = Math.round(loaded / 1048576 * 100) / 100;
287
+ if (total) gemmaTotalMB = Math.round(total / 1048576 * 100) / 100;
288
+ },
289
+ onStatusChange: (s: GemmaStatus) => {
290
+ gemmaStatus = s;
291
+ if (s === 'loading') _startGemmaTimer();
292
+ if (s === 'ready' || s === 'error') _stopGemmaTimer();
293
+ },
294
+ });
295
+ // Store under gemmaProvider slot so lifecycle (unload, status) works
296
+ gemmaProvider = provider as unknown as WasmProvider;
297
+ }
298
+ return gemmaProvider as unknown as LLMProvider;
299
+ }
300
+ }
301
+
302
+ // Remote Claude (default)
303
+ remoteProvider.setModel(llm as Parameters<typeof remoteProvider.setModel>[0]);
304
+ return remoteProvider;
305
+ },
306
+
307
+ /**
308
+ * Initialize Gemma (or a custom WASM-style provider) if `canvas.llm` matches
309
+ * and status is idle. Idempotent — safe to call on every LLM change.
310
+ */
311
+ initGemmaIfNeeded() {
312
+ const llm = canvas.llm;
313
+ if (gemmaStatus !== 'idle') return;
314
+ const isGemma = llm === 'gemma-e2b' || llm === 'gemma-e4b';
315
+ const isCustom = customProviders.some(({ prefix }) => llm.startsWith(prefix));
316
+ if (isGemma || isCustom) {
317
+ const p = this.getProvider();
318
+ // Both WasmProvider and custom providers expose initialize()
319
+ (p as unknown as { initialize?: () => void }).initialize?.();
320
+ }
321
+ },
322
+
323
+ /** Destroy and reset the current WasmProvider instance (or custom provider stored in the same slot). */
324
+ unloadGemma() {
325
+ (gemmaProvider as unknown as { destroy?: () => void })?.destroy?.();
326
+ gemmaProvider = null;
327
+ gemmaStatus = 'idle';
328
+ gemmaProgress = 0;
329
+ _stopGemmaTimer();
330
+ },
331
+
332
+ /**
333
+ * Merge the observer's callbacks (if any) with the host component's own callbacks.
334
+ * Call this to produce the final `callbacks` object passed to `runAgentLoop`:
335
+ *
336
+ * callbacks: agent.mergeCallbacks({ onWidget, onLLMResponse, ... })
337
+ *
338
+ * Each observer callback is invoked AFTER the host callback so host logic
339
+ * (e.g. agentLogs push) runs first.
340
+ */
341
+ mergeCallbacks(userCallbacks: Partial<AgentCallbacks>): Partial<AgentCallbacks> {
342
+ if (!observer) return userCallbacks;
343
+ const obs = observer.callbacks;
344
+ const merged: Partial<AgentCallbacks> = { ...userCallbacks };
345
+ // For each key in observer.callbacks, wrap with the user callback if present.
346
+ (Object.keys(obs) as Array<keyof AgentCallbacks>).forEach((key) => {
347
+ const obsFn = obs[key] as ((...args: unknown[]) => unknown) | undefined;
348
+ if (!obsFn) return;
349
+ const userFn = userCallbacks[key] as ((...args: unknown[]) => unknown) | undefined;
350
+ if (userFn) {
351
+ (merged as Record<string, unknown>)[key] = (...args: unknown[]) => {
352
+ const r = (userFn as (...a: unknown[]) => unknown)(...args);
353
+ (obsFn as (...a: unknown[]) => unknown)(...args);
354
+ return r;
355
+ };
356
+ } else {
357
+ (merged as Record<string, unknown>)[key] = obsFn;
358
+ }
359
+ });
360
+ return merged;
361
+ },
362
+
363
+ /**
364
+ * Return the agent loop options derived from current smart defaults.
365
+ * Spread into `runAgentLoop` options alongside app-specific overrides.
366
+ */
367
+ runOptions(): AgentRunOptions {
368
+ return {
369
+ schemaSanitize,
370
+ schemaFlatten,
371
+ maxResultLength,
372
+ truncateResults,
373
+ compressHistory: compressHistoryEnabled ? compressPreview : false,
374
+ cacheEnabled,
375
+ temperature,
376
+ };
377
+ },
378
+
379
+ /** Record an LLM response into the TokenTracker (use in onLLMResponse callback). */
380
+ recordTokens(response: { usage?: Record<string, number>; stats?: { totalTokens: number } }, latencyMs: number) {
381
+ const isWasm = canvas.llm?.startsWith('gemma') || canvas.llm?.startsWith('transformers-') || false;
382
+ if (response.usage) {
383
+ tokenTracker.record(response.usage as any, latencyMs, isWasm);
384
+ } else if (response.stats) {
385
+ tokenTracker.recordEstimate(0, response.stats.totalTokens * 4, latencyMs);
386
+ }
387
+ },
388
+
389
+ /** Clean up intervals on component destroy. */
390
+ destroy() {
391
+ _stopGemmaTimer();
392
+ },
393
+ };
394
+ }
395
+
396
+ export type AgentLoop = ReturnType<typeof createAgentLoop>;
@@ -0,0 +1,64 @@
1
+ <script lang="ts">
2
+ interface Props {
3
+ placeholder?: string;
4
+ disabled?: boolean;
5
+ value?: string;
6
+ }
7
+
8
+ let { placeholder = 'Type a message...', disabled = false, value = $bindable('') }: Props = $props();
9
+
10
+ let inputEl = $state<HTMLInputElement | null>(null);
11
+
12
+ function handleSubmit() {
13
+ const text = value?.trim();
14
+ if (!text || disabled) return;
15
+ inputEl?.dispatchEvent(new CustomEvent('submit', { detail: text, bubbles: true }));
16
+ value = '';
17
+ }
18
+
19
+ function handleKeydown(e: KeyboardEvent) {
20
+ if (e.key === 'Enter' && !e.shiftKey) {
21
+ e.preventDefault();
22
+ handleSubmit();
23
+ }
24
+ }
25
+
26
+ function handleStop() {
27
+ inputEl?.dispatchEvent(new CustomEvent('stop', { bubbles: true }));
28
+ }
29
+ </script>
30
+
31
+ <div class="flex items-center gap-2 w-full">
32
+ <input
33
+ bind:this={inputEl}
34
+ bind:value
35
+ type="text"
36
+ {placeholder}
37
+ {disabled}
38
+ onkeydown={handleKeydown}
39
+ class="flex-1 h-9 px-3 rounded-lg border border-border2 bg-surface2 text-sm text-text1
40
+ placeholder:text-text2/40 focus:outline-none focus:border-accent/50
41
+ disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
42
+ />
43
+ {#if disabled}
44
+ <button
45
+ type="button"
46
+ onclick={handleStop}
47
+ class="h-9 px-3 rounded-lg border border-accent2/40 bg-accent2/10 text-accent2
48
+ text-xs font-mono hover:bg-accent2/20 transition-colors flex-shrink-0"
49
+ >
50
+ stop
51
+ </button>
52
+ {:else}
53
+ <button
54
+ type="button"
55
+ onclick={handleSubmit}
56
+ disabled={!value?.trim()}
57
+ class="h-9 px-3 rounded-lg border border-accent/40 bg-accent/10 text-accent
58
+ text-xs font-mono hover:bg-accent/20 disabled:opacity-40 disabled:cursor-not-allowed
59
+ transition-colors flex-shrink-0"
60
+ >
61
+ send
62
+ </button>
63
+ {/if}
64
+ </div>
@@ -4,10 +4,11 @@
4
4
 
5
5
  type Props = {
6
6
  class?: string;
7
+ style?: string;
7
8
  children?: import('svelte').Snippet;
8
9
  };
9
10
 
10
- let { class: className, children }: Props = $props();
11
+ let { class: className, style, children }: Props = $props();
11
12
  </script>
12
13
 
13
14
  <Dialog.Portal>
@@ -17,6 +18,7 @@
17
18
  'fixed left-1/2 top-1/2 z-50 -translate-x-1/2 -translate-y-1/2 bg-surface border border-border2 rounded-xl shadow-2xl max-w-lg w-full mx-4 p-6',
18
19
  className
19
20
  )}
21
+ {style}
20
22
  >
21
23
  {@render children?.()}
22
24
  </Dialog.Content>
@@ -0,0 +1,78 @@
1
+ <script lang="ts">
2
+ import { onMount } from 'svelte';
3
+ import { getTheme } from '../theme/ThemeProvider.svelte';
4
+ import { toggleUIScale, getUIScale, initUIScale, type UIScale } from '../theme/scale.js';
5
+ import { THEME_MAP } from '../theme/tokens.js';
6
+
7
+ let { compact = false }: { compact?: boolean } = $props();
8
+
9
+ // Theme: prefer ThemeProvider context if available; fallback to dataset-driven
10
+ // toggle so apps without a provider (notebook-viewer) still work.
11
+ const themeCtx = getTheme();
12
+
13
+ let mode = $state<'light' | 'dark'>('light');
14
+ let scale = $state<UIScale>(1);
15
+
16
+ function fallbackToggleTheme() {
17
+ const root = document.documentElement;
18
+ const next = root.dataset.theme === 'dark' ? 'light' : 'dark';
19
+ root.dataset.theme = next;
20
+ try { localStorage.setItem('webmcp-theme', next); } catch {}
21
+ const tokens = THEME_MAP[next];
22
+ if (tokens) for (const [k, v] of Object.entries(tokens)) root.style.setProperty(`--${k}`, v);
23
+ mode = next;
24
+ }
25
+
26
+ function onToggleTheme() {
27
+ if (themeCtx) {
28
+ themeCtx.toggle();
29
+ mode = themeCtx.mode;
30
+ } else {
31
+ fallbackToggleTheme();
32
+ }
33
+ }
34
+
35
+ function onToggleScale() {
36
+ scale = toggleUIScale();
37
+ }
38
+
39
+ onMount(() => {
40
+ initUIScale();
41
+ scale = getUIScale();
42
+ if (themeCtx) {
43
+ mode = themeCtx.mode;
44
+ } else {
45
+ mode = (document.documentElement.dataset.theme === 'dark') ? 'dark' : 'light';
46
+ }
47
+ });
48
+
49
+ // Keep `mode` in sync when the provider updates (e.g. from another component).
50
+ $effect(() => {
51
+ if (themeCtx) mode = themeCtx.mode;
52
+ });
53
+ </script>
54
+
55
+ <div class="header-controls" class:compact>
56
+ <button type="button" class="hc-btn" onclick={onToggleTheme} aria-label="Toggle theme"
57
+ title={mode === 'dark' ? 'Switch to light' : 'Switch to dark'}>
58
+ {mode === 'dark' ? '☀' : '☾'}
59
+ </button>
60
+ <button type="button" class="hc-btn" onclick={onToggleScale} aria-label="Toggle UI scale"
61
+ title={scale === 1 ? 'Scale UI up (2×)' : 'Reset UI scale'}>
62
+ {scale === 1 ? '2×' : '1×'}
63
+ </button>
64
+ </div>
65
+
66
+ <style>
67
+ .header-controls { display: inline-flex; gap: 4px; align-items: center; }
68
+ .hc-btn {
69
+ display: inline-flex; align-items: center; justify-content: center;
70
+ height: 28px; min-width: 32px; padding: 0 8px;
71
+ border: 1px solid var(--border2, var(--border, rgba(127,127,127,0.25)));
72
+ background: transparent; color: var(--text2, currentColor);
73
+ border-radius: 6px; cursor: pointer; font-size: 12px; line-height: 1;
74
+ font-family: inherit; transition: background 0.15s ease, color 0.15s ease;
75
+ }
76
+ .hc-btn:hover { background: rgba(127,127,127,0.1); color: var(--text1, currentColor); }
77
+ .compact .hc-btn { height: 24px; min-width: 28px; padding: 0 6px; font-size: 11px; }
78
+ </style>
package/src/index.ts CHANGED
@@ -7,6 +7,7 @@ export { DARK_TOKENS, LIGHT_TOKENS, THEME_MAP } from './theme/tokens.js';
7
7
  export type { ThemeMode, ThemeOverrides, ThemeTokens } from './theme/tokens.js';
8
8
  export { getUIScale, setUIScale, toggleUIScale, initUIScale, isUIScaled } from './theme/scale.js';
9
9
  export type { UIScale, UIScaleKey } from './theme/scale.js';
10
+ export { default as HeaderControls } from './components/HeaderControls.svelte';
10
11
 
11
12
  // Primitives
12
13
  export { default as Card } from './primitives/Card.svelte';
@@ -18,47 +19,20 @@ export { default as MarkdownView } from './primitives/MarkdownView.svelte';
18
19
  export { default as CodeView } from './primitives/CodeView.svelte';
19
20
  export { renderMarkdown, highlightCode, createMarkdownRenderer } from './primitives/markdown-renderer.js';
20
21
 
21
- // Simple widgets (vanilla renderers) contract: render(container, data): () => void
22
- export { render as renderStat } from './widgets/simple/stat.js';
23
- export { render as renderKv } from './widgets/simple/kv.js';
24
- export { render as renderList } from './widgets/simple/list.js';
25
- export { render as renderChart } from './widgets/simple/chart.js';
26
- export { render as renderAlert } from './widgets/simple/alert.js';
27
- export { render as renderCode } from './widgets/simple/code.js';
28
- export { render as renderText } from './widgets/simple/text.js';
29
- export { render as renderActions } from './widgets/simple/actions.js';
30
- export { render as renderTags } from './widgets/simple/tags.js';
22
+ // Widgets are shipped as Svelte 5 custom elements import the widget file
23
+ // side-effect to register its tag (e.g. `import '@webmcp-auto-ui/ui/widgets/simple/stat.svelte';`
24
+ // then use `<auto-stat data={spec}></auto-stat>`). `WidgetRenderer` does this for you.
31
25
 
32
- // Rich widgets (vanilla renderers)
33
- export { render as renderStatCard } from './widgets/rich/stat-card.js';
34
- export { render as renderDataTable } from './widgets/rich/data-table.js';
35
- export { render as renderTimeline } from './widgets/rich/timeline.js';
36
- export { render as renderProfile } from './widgets/rich/profile.js';
37
- export { render as renderTrombinoscope } from './widgets/rich/trombinoscope.js';
38
- export { render as renderJsonViewer } from './widgets/rich/json-viewer.js';
39
- export { render as renderHemicycle } from './widgets/rich/hemicycle.js';
40
- export { render as renderChartRich } from './widgets/rich/chart-rich.js';
41
- export { render as renderCards } from './widgets/rich/cards.js';
42
- export { render as renderGridData } from './widgets/rich/grid-data.js';
43
- export { render as renderSankey } from './widgets/rich/sankey.js';
44
- export { render as renderMap } from './widgets/rich/map.js';
45
- export { render as renderD3 } from './widgets/rich/d3.js';
46
- export { render as renderJsSandbox } from './widgets/rich/js-sandbox.js';
47
- export { render as renderLog } from './widgets/rich/log.js';
48
- export { render as renderGallery } from './widgets/rich/gallery.js';
49
- export { render as renderCarousel } from './widgets/rich/carousel.js';
50
-
51
- // Notebook widget renderer (vanilla)
52
- export { render as renderNotebook } from './widgets/notebook/notebook.js';
53
- export { render as renderRecipeBrowserWidget } from './widgets/notebook/recipe-browser.js';
26
+ // Notebook vanilla renderer (kept — notebook is wrapped by a custom element that
27
+ // delegates to the legacy vanilla code; full Svelte rewrite is Phase 3).
28
+ export { render as renderNotebook } from './widgets/notebook/notebook.js';
54
29
  // Notebook types (optional public API)
55
30
  export type { NotebookState, NotebookCell } from './widgets/notebook/shared.js';
56
31
  // Notebook cell extractors (for hosts that build notebooks from recipes/tools)
57
32
  export { extractCellsFromRecipe, extractCellsFromTool, extractCellFromMarkdown, extractCellFromFence } from './widgets/notebook/resource-extractor.js';
58
33
 
59
- // Safe image helper (URL validation + error fallback)
60
- export { createSafeImage } from './widgets/helpers/safe-image.js';
61
- export type { SafeImageOptions } from './widgets/helpers/safe-image.js';
34
+ // Safe image Svelte component prefer this in .svelte code over the legacy helper.
35
+ export { default as SafeImage } from './widgets/SafeImage.svelte';
62
36
 
63
37
  // Widget export utility
64
38
  export { exportWidget, getExportFormats, exportWidgetAs } from './widgets/export-widget.js';
@@ -108,8 +82,12 @@ export { default as ChatPanel } from './agent/ChatPanel.svelte';
108
82
  export type { ChatFeedItem, ChatBubble, ChatBlock } from './agent/ChatPanel.svelte';
109
83
  export { default as AgentConsole } from './agent/AgentConsole.svelte';
110
84
  export { default as SettingsPanel } from './agent/SettingsPanel.svelte';
85
+ export { default as MCPserversList } from './agent/MCPserversList.svelte';
86
+ /** @deprecated Use MCPserversList instead. Alias kept for backward compatibility. */
111
87
  export { default as RemoteMCPserversDemo } from './agent/RemoteMCPserversDemo.svelte';
112
88
  export { default as WebMCPserversList } from './agent/WebMCPserversList.svelte';
89
+ export { default as RecipeBrowser } from './agent/RecipeBrowser.svelte';
90
+ export { default as ToolBrowser } from './agent/ToolBrowser.svelte';
113
91
  export { default as EphemeralBubble } from './agent/EphemeralBubble.svelte';
114
92
  export { default as TokenBubble } from './agent/TokenBubble.svelte';
115
93
  export { default as DiagnosticModal } from './agent/DiagnosticModal.svelte';
@@ -75,7 +75,6 @@ function createSvelteCanvas() {
75
75
  // Setters
76
76
  setMode: canvasVanilla.setMode.bind(canvasVanilla),
77
77
  setLlm: canvasVanilla.setLlm.bind(canvasVanilla),
78
- setMcpUrl: canvasVanilla.setMcpUrl.bind(canvasVanilla),
79
78
  setGenerating: canvasVanilla.setGenerating.bind(canvasVanilla),
80
79
 
81
80
  // Block actions
@@ -91,11 +90,6 @@ function createSvelteCanvas() {
91
90
  updateMsg: canvasVanilla.updateMsg.bind(canvasVanilla),
92
91
  clearMessages: canvasVanilla.clearMessages.bind(canvasVanilla),
93
92
 
94
- // MCP
95
- setMcpConnecting: canvasVanilla.setMcpConnecting.bind(canvasVanilla),
96
- setMcpConnected: canvasVanilla.setMcpConnected.bind(canvasVanilla),
97
- setMcpError: canvasVanilla.setMcpError.bind(canvasVanilla),
98
-
99
93
  // Theme
100
94
  get themeOverrides() { return themeOverrides; },
101
95
  setThemeOverrides: canvasVanilla.setThemeOverrides.bind(canvasVanilla),