@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.
- package/package.json +15 -2
- package/src/agent/DiagnosticModal.svelte +126 -50
- package/src/agent/EphemeralBubble.svelte +13 -3
- package/src/agent/MCPserversList.svelte +147 -0
- package/src/agent/McpConnector.svelte +10 -1
- package/src/agent/RecipeBrowser.svelte +384 -0
- package/src/agent/RemoteMCPserversDemo.svelte +5 -121
- package/src/agent/ToolBrowser.svelte +133 -0
- package/src/agent/WebMCPserversList.svelte +2 -0
- package/src/agent/useAgentLoop.svelte.ts +396 -0
- package/src/base/chat-inline.svelte +64 -0
- package/src/base/dialog-content.svelte +3 -1
- package/src/components/HeaderControls.svelte +78 -0
- package/src/index.ts +13 -35
- package/src/stores/canvas.svelte.ts +0 -6
- package/src/widgets/SafeImage.svelte +67 -0
- package/src/widgets/WidgetRenderer.svelte +153 -78
- package/src/widgets/notebook/executors/index.ts +0 -1
- package/src/widgets/notebook/executors/sql.ts +32 -182
- package/src/widgets/notebook/import-modal-api.ts +237 -0
- package/src/widgets/notebook/import-modal.svelte +738 -0
- package/src/widgets/notebook/left-pane.ts +1 -1
- package/src/widgets/notebook/notebook.svelte +75 -0
- package/src/widgets/notebook/notebook.ts +38 -73
- package/src/widgets/notebook/prose.ts +6 -3
- package/src/widgets/notebook/shared.ts +68 -49
- package/src/widgets/rich/cards.svelte +74 -0
- package/src/widgets/rich/carousel.svelte +126 -0
- package/src/widgets/rich/chart-rich.svelte +221 -0
- package/src/widgets/rich/chat-input.svelte +52 -0
- package/src/widgets/rich/data-table.svelte +132 -0
- package/src/widgets/rich/gallery.svelte +115 -0
- package/src/widgets/rich/grid-data.svelte +85 -0
- package/src/widgets/rich/hemicycle.svelte +95 -0
- package/src/widgets/rich/js-sandbox.svelte +67 -0
- package/src/widgets/rich/json-viewer.svelte +82 -0
- package/src/widgets/rich/log.svelte +62 -0
- package/src/widgets/rich/profile.svelte +91 -0
- package/src/widgets/rich/sankey.svelte +73 -0
- package/src/widgets/rich/stat-card.svelte +60 -0
- package/src/widgets/rich/timeline.svelte +95 -0
- package/src/widgets/rich/trombinoscope.svelte +87 -0
- package/src/widgets/simple/actions.svelte +36 -0
- package/src/widgets/simple/alert.svelte +52 -0
- package/src/widgets/simple/chart.svelte +38 -0
- package/src/widgets/simple/code.svelte +30 -0
- package/src/widgets/simple/kv.svelte +31 -0
- package/src/widgets/simple/list.svelte +35 -0
- package/src/widgets/simple/stat.svelte +36 -0
- package/src/widgets/simple/tags.svelte +34 -0
- package/src/widgets/simple/text.svelte +130 -0
- package/src/widgets/helpers/safe-image.ts +0 -78
- package/src/widgets/notebook/import-modals.ts +0 -560
- package/src/widgets/notebook/recipe-browser.ts +0 -350
- package/src/widgets/rich/cards.ts +0 -181
- package/src/widgets/rich/carousel.ts +0 -319
- package/src/widgets/rich/chart-rich.ts +0 -386
- package/src/widgets/rich/d3.ts +0 -503
- package/src/widgets/rich/data-table.ts +0 -342
- package/src/widgets/rich/gallery.ts +0 -350
- package/src/widgets/rich/grid-data.ts +0 -173
- package/src/widgets/rich/hemicycle.ts +0 -313
- package/src/widgets/rich/js-sandbox.ts +0 -122
- package/src/widgets/rich/json-viewer.ts +0 -202
- package/src/widgets/rich/log.ts +0 -143
- package/src/widgets/rich/map.ts +0 -218
- package/src/widgets/rich/profile.ts +0 -256
- package/src/widgets/rich/sankey.ts +0 -257
- package/src/widgets/rich/stat-card.ts +0 -125
- package/src/widgets/rich/timeline.ts +0 -179
- package/src/widgets/rich/trombinoscope.ts +0 -246
- package/src/widgets/simple/actions.ts +0 -89
- package/src/widgets/simple/alert.ts +0 -100
- package/src/widgets/simple/chart.ts +0 -189
- package/src/widgets/simple/code.ts +0 -79
- package/src/widgets/simple/kv.ts +0 -68
- package/src/widgets/simple/list.ts +0 -89
- package/src/widgets/simple/stat.ts +0 -58
- package/src/widgets/simple/tags.ts +0 -125
- package/src/widgets/simple/text.ts +0 -198
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// ---------------------------------------------------------------------------
|
|
8
8
|
|
|
9
9
|
import { callToolViaPostMessage } from '@webmcp-auto-ui/core';
|
|
10
|
-
import { openRecipeViewerModal, openToolViewerModal, type ImportedRecipe } from './import-
|
|
10
|
+
import { openRecipeViewerModal, openToolViewerModal, type ImportedRecipe } from './import-modal-api.js';
|
|
11
11
|
import type { NotebookCell, NotebookState, DataServerDescriptor } from './shared.js';
|
|
12
12
|
|
|
13
13
|
export interface LeftPaneHandlers {
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
<svelte:options customElement={{ tag: 'auto-notebook', shadow: 'none' }} />
|
|
2
|
+
|
|
3
|
+
<script lang="ts">
|
|
4
|
+
import { onMount, onDestroy } from 'svelte';
|
|
5
|
+
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Types (mirror shared.ts without importing it — avoids circular deps)
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
|
|
10
|
+
export interface NotebookCell {
|
|
11
|
+
id: string;
|
|
12
|
+
type: 'md' | 'sql' | 'js';
|
|
13
|
+
content: string;
|
|
14
|
+
status?: 'idle' | 'running' | 'done' | 'error' | 'stale';
|
|
15
|
+
hideSource?: boolean;
|
|
16
|
+
hideResult?: boolean;
|
|
17
|
+
lastResult?: unknown;
|
|
18
|
+
lastMs?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface NotebookData {
|
|
22
|
+
id?: string;
|
|
23
|
+
title?: string;
|
|
24
|
+
kicker?: string;
|
|
25
|
+
mode?: 'edit' | 'view';
|
|
26
|
+
autoRun?: boolean;
|
|
27
|
+
hideLiveToggle?: boolean;
|
|
28
|
+
cells?: NotebookCell[];
|
|
29
|
+
/** MCP servers for SQL execution (array of {name, url}) */
|
|
30
|
+
servers?: Array<{ name: string; url?: string }>;
|
|
31
|
+
/** Legacy flat field — also accepted */
|
|
32
|
+
mcpUrl?: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface Props {
|
|
36
|
+
data?: NotebookData | null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let { data = null }: Props = $props();
|
|
40
|
+
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// DOM ref and vanilla cleanup
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
|
|
45
|
+
let container: HTMLDivElement;
|
|
46
|
+
let cleanup: (() => void) | null = null;
|
|
47
|
+
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// Mount / unmount
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
onMount(async () => {
|
|
53
|
+
if (!container) return;
|
|
54
|
+
|
|
55
|
+
// Lazy-import to avoid pulling 900 lines into the Svelte bundle at parse time.
|
|
56
|
+
const { render } = await import('./notebook.js');
|
|
57
|
+
|
|
58
|
+
const plainData: Record<string, unknown> = data
|
|
59
|
+
? (data as Record<string, unknown>)
|
|
60
|
+
: {};
|
|
61
|
+
|
|
62
|
+
cleanup = await render(container, plainData);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
onDestroy(() => {
|
|
66
|
+
cleanup?.();
|
|
67
|
+
cleanup = null;
|
|
68
|
+
});
|
|
69
|
+
</script>
|
|
70
|
+
|
|
71
|
+
<!--
|
|
72
|
+
The vanilla render() writes directly into this div.
|
|
73
|
+
No Svelte template content needed — the notebook manages its own DOM.
|
|
74
|
+
-->
|
|
75
|
+
<div bind:this={container}></div>
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
addImportedCells, registerExecutor, collectDataServers,
|
|
12
12
|
autosize, openShareModal, registerHistoryObserver,
|
|
13
13
|
renderCellLogs,
|
|
14
|
-
createPublishControls, autoConnectFrontmatterServers,
|
|
14
|
+
createPublishControls, autoConnectFrontmatterServers,
|
|
15
15
|
createRuntimeOverlay, effectiveResult, cellRuntimeStatus,
|
|
16
16
|
lastRefreshedAt, bootstrapLiveRefresh, fmtRelTime, preserveScrollAround,
|
|
17
17
|
type NotebookState, type NotebookCell, type CellResult, type CellExecContext,
|
|
@@ -20,10 +20,11 @@ import {
|
|
|
20
20
|
import { renderChart } from './chart-renderer.js';
|
|
21
21
|
import { dispatchShare } from './share-handlers.js';
|
|
22
22
|
import { renderProse, mountEditableProse } from './prose.js';
|
|
23
|
-
import { openAddMdModal, openAddRecipeModal } from './import-
|
|
23
|
+
import { openAddMdModal, openAddRecipeModal } from './import-modal-api.js';
|
|
24
24
|
import { extractCellsFromRecipe, extractCellFromMarkdown } from './resource-extractor.js';
|
|
25
25
|
import { mountLeftPane } from './left-pane.js';
|
|
26
|
-
import {
|
|
26
|
+
import { highlightCode } from '../../primitives/markdown-renderer.js';
|
|
27
|
+
import { createSqlExecutor } from './executors/sql.js';
|
|
27
28
|
|
|
28
29
|
export async function render(container: HTMLElement, data: Record<string, unknown>): Promise<() => void> {
|
|
29
30
|
injectStyles();
|
|
@@ -45,7 +46,7 @@ export async function render(container: HTMLElement, data: Record<string, unknow
|
|
|
45
46
|
|
|
46
47
|
// --- register executors -------------------------------------------------
|
|
47
48
|
registerExecutor(state, 'js', jsExecutor);
|
|
48
|
-
registerExecutor(state, 'sql',
|
|
49
|
+
registerExecutor(state, 'sql', createSqlExecutor(() => collectDataServers(data)));
|
|
49
50
|
|
|
50
51
|
container.classList.add('nb-root');
|
|
51
52
|
container.classList.toggle('nb-view-mode', state.mode === 'view');
|
|
@@ -170,15 +171,21 @@ export async function render(container: HTMLElement, data: Record<string, unknow
|
|
|
170
171
|
state,
|
|
171
172
|
data,
|
|
172
173
|
overlay,
|
|
173
|
-
MultiMcpBridgeCtor: MultiMcpBridge as any,
|
|
174
174
|
onCellChange: (cellId) => {
|
|
175
175
|
const node = cellsEl.querySelector(`[data-id="${cellId}"]`) as HTMLElement | null;
|
|
176
|
-
if (!node) {
|
|
176
|
+
if (!node) {
|
|
177
|
+
const restore = preserveScrollAround(cellsEl);
|
|
178
|
+
renderCells();
|
|
179
|
+
restore();
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
177
182
|
const idx = state.cells.findIndex((c) => c.id === cellId);
|
|
178
183
|
if (idx < 0) return;
|
|
184
|
+
const restore = preserveScrollAround(cellsEl);
|
|
179
185
|
const fresh = renderCell(state.cells[idx], state, overlay, rerender);
|
|
180
186
|
fresh.addEventListener('focusin', () => { lastActiveIdx = idx; });
|
|
181
187
|
node.replaceWith(fresh);
|
|
188
|
+
restore();
|
|
182
189
|
},
|
|
183
190
|
onTick: () => {
|
|
184
191
|
renderLiveBadge();
|
|
@@ -286,13 +293,10 @@ export async function render(container: HTMLElement, data: Record<string, unknow
|
|
|
286
293
|
},
|
|
287
294
|
});
|
|
288
295
|
|
|
289
|
-
// Auto-connect data servers declared in the recipe frontmatter (data.servers)
|
|
296
|
+
// Auto-connect data servers declared in the recipe frontmatter (data.servers).
|
|
297
|
+
// The notebook reads MCP state passively from globalThis.__multiMcp (singleton).
|
|
290
298
|
autoConnectFrontmatterServers(data, () => pane.setServers(collectDataServers(data)));
|
|
291
299
|
|
|
292
|
-
// Start a persistent MCP bridge so the sql executor can find tools in edit mode
|
|
293
|
-
// too (not just when live-refresh is running in view mode).
|
|
294
|
-
const mcpBridgeCleanup = bootstrapMcpBridge({ data, MultiMcpBridgeCtor: MultiMcpBridge as any });
|
|
295
|
-
|
|
296
300
|
// Keep pane servers in sync with canvas changes
|
|
297
301
|
let canvasUnsub: (() => void) | null = null;
|
|
298
302
|
try {
|
|
@@ -319,7 +323,6 @@ export async function render(container: HTMLElement, data: Record<string, unknow
|
|
|
319
323
|
pane.destroy();
|
|
320
324
|
publishCleanup();
|
|
321
325
|
liveCleanup?.();
|
|
322
|
-
mcpBridgeCleanup();
|
|
323
326
|
};
|
|
324
327
|
}
|
|
325
328
|
|
|
@@ -361,54 +364,6 @@ async function jsExecutor(ctx: CellExecContext): Promise<CellResult> {
|
|
|
361
364
|
}
|
|
362
365
|
}
|
|
363
366
|
|
|
364
|
-
function makeSqlExecutor(data: Record<string, unknown>) {
|
|
365
|
-
return async function sqlExecutor(ctx: CellExecContext): Promise<CellResult> {
|
|
366
|
-
const start = Date.now();
|
|
367
|
-
const sql = ctx.cell.content;
|
|
368
|
-
const servers = collectDataServers(data);
|
|
369
|
-
const candidates: string[] = [];
|
|
370
|
-
for (const srv of servers) {
|
|
371
|
-
for (const t of srv.tools ?? []) candidates.push(t.name);
|
|
372
|
-
}
|
|
373
|
-
const precise = candidates.find((n) => /^.*query_sql$/i.test(n));
|
|
374
|
-
const loose = precise ?? candidates.find((n) => /^(query|run|execute)(_sql)?$/i.test(n));
|
|
375
|
-
const toolName = precise ?? loose;
|
|
376
|
-
if (!toolName) {
|
|
377
|
-
return {
|
|
378
|
-
ok: false,
|
|
379
|
-
error: 'No SQL tool available on connected servers (looked for *query_sql or query/run/execute).',
|
|
380
|
-
errorKind: 'schema',
|
|
381
|
-
durationMs: Date.now() - start,
|
|
382
|
-
};
|
|
383
|
-
}
|
|
384
|
-
try {
|
|
385
|
-
const res: any = await callToolViaPostMessage(toolName, { sql });
|
|
386
|
-
const text = res?.content?.find?.((c: any) => c.type === 'text')?.text ?? '';
|
|
387
|
-
const durationMs = Date.now() - start;
|
|
388
|
-
let parsed: any = null;
|
|
389
|
-
try { parsed = JSON.parse(text); } catch { /* not JSON */ }
|
|
390
|
-
if (parsed) {
|
|
391
|
-
const rows: any[] = Array.isArray(parsed) ? parsed
|
|
392
|
-
: Array.isArray(parsed?.rows) ? parsed.rows
|
|
393
|
-
: Array.isArray(parsed?.data) ? parsed.data
|
|
394
|
-
: Array.isArray(parsed?.results) ? parsed.results
|
|
395
|
-
: [];
|
|
396
|
-
if (rows.length && rows.every((r) => r && typeof r === 'object')) {
|
|
397
|
-
const columns = Array.isArray(parsed?.columns)
|
|
398
|
-
? parsed.columns.map(String)
|
|
399
|
-
: Array.from(new Set(rows.flatMap((r) => Object.keys(r))));
|
|
400
|
-
return { ok: true, kind: 'table', rows, columns, rowCount: rows.length, durationMs };
|
|
401
|
-
}
|
|
402
|
-
return { ok: true, kind: 'value', value: parsed, durationMs };
|
|
403
|
-
}
|
|
404
|
-
if (!text) return { ok: true, kind: 'empty', durationMs };
|
|
405
|
-
return { ok: true, kind: 'value', value: text, durationMs };
|
|
406
|
-
} catch (err: any) {
|
|
407
|
-
return { ok: false, error: String(err?.message ?? err), errorKind: 'runtime', durationMs: Date.now() - start };
|
|
408
|
-
}
|
|
409
|
-
};
|
|
410
|
-
}
|
|
411
|
-
|
|
412
367
|
// ---------------------------------------------------------------------------
|
|
413
368
|
// Cell rendering — prose + code share the unified flow, same DnD handle
|
|
414
369
|
// ---------------------------------------------------------------------------
|
|
@@ -482,15 +437,25 @@ function renderCell(cell: NotebookCell, state: NotebookState, overlay: RuntimeOv
|
|
|
482
437
|
|
|
483
438
|
const body = document.createElement('div');
|
|
484
439
|
body.className = 'nbe-code-body' + (cell.hideSource ? ' nbe-hidden' : '');
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
440
|
+
if (state.mode === 'view') {
|
|
441
|
+
const lang = cell.type === 'js' ? 'javascript' : cell.type;
|
|
442
|
+
const pre = document.createElement('pre');
|
|
443
|
+
pre.className = 'hljs-pre nb-code-view';
|
|
444
|
+
pre.innerHTML = `<code class="hljs language-${lang}">${highlightCode(cell.content, lang)}</code>`;
|
|
445
|
+
body.appendChild(pre);
|
|
446
|
+
} else {
|
|
447
|
+
const ta = document.createElement('textarea');
|
|
448
|
+
ta.className = 'nb-code-edit';
|
|
449
|
+
ta.value = cell.content;
|
|
450
|
+
ta.rows = 1;
|
|
451
|
+
ta.spellcheck = false;
|
|
452
|
+
ta.addEventListener('input', () => { cell.content = ta.value; autosize(ta); cell.status = 'stale'; });
|
|
453
|
+
body.appendChild(ta);
|
|
454
|
+
requestAnimationFrame(() => requestAnimationFrame(() => autosize(ta)));
|
|
455
|
+
const ro = new ResizeObserver(() => autosize(ta));
|
|
456
|
+
ro.observe(ta);
|
|
457
|
+
}
|
|
492
458
|
codeCell.appendChild(body);
|
|
493
|
-
requestAnimationFrame(() => requestAnimationFrame(() => autosize(ta)));
|
|
494
459
|
|
|
495
460
|
if (!cell.hideResult) {
|
|
496
461
|
const res = document.createElement('div');
|
|
@@ -669,7 +634,7 @@ function injectLayoutStyles(): void {
|
|
|
669
634
|
.nbe-kicker-input:focus { border-color: var(--color-border); background: var(--color-bg); color: var(--color-text1); }
|
|
670
635
|
.nb-root.nb-view-mode .nbe-kicker-input { pointer-events: none; }
|
|
671
636
|
.nbe-title {
|
|
672
|
-
font-family: 'EB Garamond', Georgia, serif;
|
|
637
|
+
font-family: var(--font-serif, 'EB Garamond', Georgia, serif);
|
|
673
638
|
font-size: 30px; font-weight: 500;
|
|
674
639
|
margin: 0 0 12px;
|
|
675
640
|
letter-spacing: -0.01em; line-height: 1.2;
|
|
@@ -691,7 +656,7 @@ function injectLayoutStyles(): void {
|
|
|
691
656
|
.nbe-del-abs:hover { opacity: 1 !important; }
|
|
692
657
|
|
|
693
658
|
.nbe-prose {
|
|
694
|
-
font-family: 'EB Garamond', Georgia, serif;
|
|
659
|
+
font-family: var(--font-serif, 'EB Garamond', Georgia, serif);
|
|
695
660
|
font-size: 17px; line-height: 1.7;
|
|
696
661
|
color: var(--color-text1);
|
|
697
662
|
max-width: 620px;
|
|
@@ -700,7 +665,7 @@ function injectLayoutStyles(): void {
|
|
|
700
665
|
}
|
|
701
666
|
.nbe-prose-render h1, .nbe-prose-render h2, .nbe-prose-render h3,
|
|
702
667
|
.nbe-prose-render h4, .nbe-prose-render h5, .nbe-prose-render h6 {
|
|
703
|
-
font-family: 'EB Garamond', Georgia, serif;
|
|
668
|
+
font-family: var(--font-serif, 'EB Garamond', Georgia, serif);
|
|
704
669
|
font-weight: 600; letter-spacing: -0.01em;
|
|
705
670
|
margin: 0.6em 0 0.3em;
|
|
706
671
|
}
|
|
@@ -788,7 +753,7 @@ function injectLayoutStyles(): void {
|
|
|
788
753
|
font-variant-numeric: tabular-nums;
|
|
789
754
|
}
|
|
790
755
|
.nbe-result-table thead th {
|
|
791
|
-
font-family: 'EB Garamond', Georgia, serif;
|
|
756
|
+
font-family: var(--font-serif, 'EB Garamond', Georgia, serif);
|
|
792
757
|
font-size: 12.5px; font-weight: 600;
|
|
793
758
|
letter-spacing: 0.02em;
|
|
794
759
|
color: var(--color-text2);
|
|
@@ -921,7 +886,7 @@ function injectLayoutStyles(): void {
|
|
|
921
886
|
.nb-empty-icon { font-size: 22px; line-height: 1; }
|
|
922
887
|
.nb-empty-body { flex: 1; min-width: 0; }
|
|
923
888
|
.nb-empty-title {
|
|
924
|
-
font-family: 'EB Garamond', Georgia, serif;
|
|
889
|
+
font-family: var(--font-serif, 'EB Garamond', Georgia, serif);
|
|
925
890
|
font-weight: 600; font-size: 14px; color: var(--color-text1);
|
|
926
891
|
}
|
|
927
892
|
.nb-empty-desc {
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
// No external dependencies.
|
|
6
6
|
// ---------------------------------------------------------------------------
|
|
7
7
|
|
|
8
|
+
import { highlightCode } from '../../primitives/markdown-renderer.js';
|
|
9
|
+
|
|
8
10
|
const BLOCK_TAGS = new Set(['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'li', 'blockquote', 'pre', 'hr', 'br']);
|
|
9
11
|
const INLINE_TAGS = new Set(['strong', 'em', 'code', 'a', 'mark', 's', 'u']);
|
|
10
12
|
const ALLOWED_TAGS = new Set([...BLOCK_TAGS, ...INLINE_TAGS]);
|
|
@@ -88,8 +90,9 @@ export function renderProse(content: string): string {
|
|
|
88
90
|
i++;
|
|
89
91
|
}
|
|
90
92
|
i++; // skip closing fence
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
+
const langKey = (lang || 'plaintext').toLowerCase();
|
|
94
|
+
const highlighted = highlightCode(code.join('\n'), langKey);
|
|
95
|
+
out.push(`<pre class="hljs-pre"><code class="hljs language-${escapeHtml(langKey)}">${highlighted}</code></pre>`);
|
|
93
96
|
continue;
|
|
94
97
|
}
|
|
95
98
|
|
|
@@ -601,7 +604,7 @@ export function renderMarkdownWithInjectButtons(
|
|
|
601
604
|
<span class="nb-md-fence-lang">${escapeHtml(lang)}</span>
|
|
602
605
|
<button type="button" class="nb-md-fence-inject">↳ inject</button>
|
|
603
606
|
</div>
|
|
604
|
-
<pre><code class="language-${escapeHtml(lang)}">${
|
|
607
|
+
<pre class="hljs-pre"><code class="hljs language-${escapeHtml(lang)}">${highlightCode(content, lang)}</code></pre>
|
|
605
608
|
`;
|
|
606
609
|
const btn = block.querySelector('.nb-md-fence-inject') as HTMLButtonElement;
|
|
607
610
|
const handler = () => onInject({ lang, content });
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
// Used by the four notebook layout renderers (compact/workspace/document/editorial)
|
|
5
5
|
// ---------------------------------------------------------------------------
|
|
6
6
|
|
|
7
|
+
import { installMultiMcpBridge } from '@webmcp-auto-ui/core';
|
|
8
|
+
|
|
7
9
|
export const NB_PUBLISH_HOST: string = (() => {
|
|
8
10
|
try {
|
|
9
11
|
const override = (import.meta as any)?.env?.PUBLIC_NB_HOST;
|
|
@@ -409,35 +411,30 @@ export function createBridgeSqlRunner(bridge: {
|
|
|
409
411
|
* a bridge-backed runner, fire runAutoRefresh. Safe to call from any layout at
|
|
410
412
|
* mount time when `state.autoRun && state.mode === 'view'`. Returns a cleanup.
|
|
411
413
|
*
|
|
412
|
-
*
|
|
413
|
-
*
|
|
414
|
+
* Reuses the global singleton bridge on `globalThis.__multiMcp` (installed via
|
|
415
|
+
* `installMultiMcpBridge` from @webmcp-auto-ui/core) — never creates a parallel
|
|
416
|
+
* bridge. If no singleton exists yet we install it here, and only the installer
|
|
417
|
+
* is allowed to stop it on cleanup.
|
|
414
418
|
*/
|
|
415
419
|
export interface BootstrapLiveRefreshOptions {
|
|
416
420
|
state: NotebookState;
|
|
417
421
|
data: Record<string, unknown>;
|
|
418
422
|
overlay: RuntimeOverlay;
|
|
419
|
-
MultiMcpBridgeCtor: new (opts: { getCanvas: () => unknown }) => {
|
|
420
|
-
start(): void;
|
|
421
|
-
stop(): void;
|
|
422
|
-
waitForEnabledServers(timeoutMs?: number): Promise<void>;
|
|
423
|
-
connectedServers(): string[];
|
|
424
|
-
hasServer(name: string): boolean;
|
|
425
|
-
callTool(serverName: string, toolName: string, args: unknown): Promise<unknown>;
|
|
426
|
-
multiClient: unknown;
|
|
427
|
-
};
|
|
428
423
|
onCellChange?: (cellId: string) => void;
|
|
429
424
|
onTick?: (overlay: RuntimeOverlay) => void;
|
|
430
425
|
timeoutMs?: number;
|
|
431
426
|
}
|
|
432
427
|
|
|
433
428
|
export function bootstrapLiveRefresh(opts: BootstrapLiveRefreshOptions): () => void {
|
|
434
|
-
const { state, data, overlay,
|
|
429
|
+
const { state, data, overlay, onCellChange, onTick, timeoutMs } = opts;
|
|
435
430
|
const ac = new AbortController();
|
|
431
|
+
let weCreatedBridge = false;
|
|
432
|
+
let bridgeRef: any = null;
|
|
436
433
|
|
|
437
434
|
void (async () => {
|
|
438
435
|
try {
|
|
439
436
|
autoConnectFrontmatterServers(data);
|
|
440
|
-
const canvas:
|
|
437
|
+
const canvas: any = (globalThis as { __canvasVanilla?: unknown; canvasVanilla?: unknown })
|
|
441
438
|
.__canvasVanilla ?? (globalThis as { canvasVanilla?: unknown }).canvasVanilla;
|
|
442
439
|
if (!canvas) {
|
|
443
440
|
overlay.error = 'No canvas available';
|
|
@@ -445,8 +442,12 @@ export function bootstrapLiveRefresh(opts: BootstrapLiveRefreshOptions): () => v
|
|
|
445
442
|
onTick?.(overlay);
|
|
446
443
|
return;
|
|
447
444
|
}
|
|
448
|
-
const
|
|
449
|
-
bridge
|
|
445
|
+
const existing = (globalThis as any).__multiMcp;
|
|
446
|
+
const bridge = existing ?? installMultiMcpBridge({ getCanvas: () => canvas });
|
|
447
|
+
weCreatedBridge = !existing;
|
|
448
|
+
bridgeRef = bridge;
|
|
449
|
+
// `installMultiMcpBridge` already starts the bridge; a pre-existing
|
|
450
|
+
// singleton is assumed to be running.
|
|
450
451
|
await bridge.waitForEnabledServers(timeoutMs ?? 5000);
|
|
451
452
|
|
|
452
453
|
const runner = createBridgeSqlRunner(bridge, () => {
|
|
@@ -463,7 +464,16 @@ export function bootstrapLiveRefresh(opts: BootstrapLiveRefreshOptions): () => v
|
|
|
463
464
|
}
|
|
464
465
|
})();
|
|
465
466
|
|
|
466
|
-
return () => {
|
|
467
|
+
return () => {
|
|
468
|
+
ac.abort();
|
|
469
|
+
if (weCreatedBridge && bridgeRef && typeof bridgeRef.stop === 'function') {
|
|
470
|
+
try { bridgeRef.stop(); } catch { /* ignore */ }
|
|
471
|
+
try {
|
|
472
|
+
const g: any = globalThis as any;
|
|
473
|
+
if (g.__multiMcp === bridgeRef) g.__multiMcp = undefined;
|
|
474
|
+
} catch { /* ignore */ }
|
|
475
|
+
}
|
|
476
|
+
};
|
|
467
477
|
}
|
|
468
478
|
|
|
469
479
|
export function registerExecutor(state: NotebookState, type: CellType, fn: CellExecutor): void {
|
|
@@ -855,35 +865,6 @@ export function createPublishControls(state: NotebookState, opts: PublishControl
|
|
|
855
865
|
};
|
|
856
866
|
}
|
|
857
867
|
|
|
858
|
-
/**
|
|
859
|
-
* Start a persistent MCP bridge that connects declared data servers and keeps
|
|
860
|
-
* their tool/recipe metadata populated on the canvas store. Independent of
|
|
861
|
-
* live-refresh — this runs even in edit mode, so the sql executor can discover
|
|
862
|
-
* `*_query_sql` tools as soon as the bridge is connected.
|
|
863
|
-
*
|
|
864
|
-
* Returns a cleanup function. Caller is expected to start this once at mount
|
|
865
|
-
* and call cleanup on unmount.
|
|
866
|
-
*/
|
|
867
|
-
export function bootstrapMcpBridge(opts: {
|
|
868
|
-
data: Record<string, unknown>;
|
|
869
|
-
MultiMcpBridgeCtor: new (opts: { getCanvas: () => unknown }) => {
|
|
870
|
-
start(): void;
|
|
871
|
-
stop(): void;
|
|
872
|
-
};
|
|
873
|
-
}): () => void {
|
|
874
|
-
try {
|
|
875
|
-
autoConnectFrontmatterServers(opts.data);
|
|
876
|
-
const canvas: unknown = (globalThis as { __canvasVanilla?: unknown; canvasVanilla?: unknown })
|
|
877
|
-
.__canvasVanilla ?? (globalThis as { canvasVanilla?: unknown }).canvasVanilla;
|
|
878
|
-
if (!canvas) return () => { /* no-op */ };
|
|
879
|
-
const bridge = new opts.MultiMcpBridgeCtor({ getCanvas: () => canvas });
|
|
880
|
-
bridge.start();
|
|
881
|
-
return () => { try { bridge.stop(); } catch { /* ignore */ } };
|
|
882
|
-
} catch {
|
|
883
|
-
return () => { /* no-op */ };
|
|
884
|
-
}
|
|
885
|
-
}
|
|
886
|
-
|
|
887
868
|
/**
|
|
888
869
|
* Auto-connect any data servers declared in recipe frontmatter (`data.servers`)
|
|
889
870
|
* to the shared canvas store. No-op / no-throw if the canvas store is absent.
|
|
@@ -906,7 +887,10 @@ export function autoConnectFrontmatterServers(
|
|
|
906
887
|
if (existing) {
|
|
907
888
|
if (existing.enabled === false) canvas.setDataServerEnabled?.(name, true);
|
|
908
889
|
} else {
|
|
909
|
-
|
|
890
|
+
// Never register a server without a URL — downstream connect logic
|
|
891
|
+
// requires it and would silently fail otherwise.
|
|
892
|
+
if (!url) continue;
|
|
893
|
+
canvas.addDataServer({ name: String(name), url: String(url) });
|
|
910
894
|
}
|
|
911
895
|
}
|
|
912
896
|
refresh?.();
|
|
@@ -1391,6 +1375,31 @@ textarea.nb-md-edit {
|
|
|
1391
1375
|
background: none; border: none; padding: 0; text-decoration: underline;
|
|
1392
1376
|
}
|
|
1393
1377
|
.nb-undo-toast-undo:hover { filter: brightness(1.15); }
|
|
1378
|
+
|
|
1379
|
+
/* ─── Syntax highlighting (highlight.js tokens, app-palette-driven) ─── */
|
|
1380
|
+
pre.hljs-pre {
|
|
1381
|
+
background: var(--color-surface2);
|
|
1382
|
+
border: 1px solid var(--color-border);
|
|
1383
|
+
border-radius: 6px;
|
|
1384
|
+
padding: 12px 14px;
|
|
1385
|
+
margin: 0;
|
|
1386
|
+
overflow-x: auto;
|
|
1387
|
+
font-family: var(--font-mono, 'IBM Plex Mono', monospace);
|
|
1388
|
+
font-size: 12.5px;
|
|
1389
|
+
line-height: 1.6;
|
|
1390
|
+
}
|
|
1391
|
+
pre.hljs-pre code.hljs { background: transparent; padding: 0; color: var(--color-text1); display: block; }
|
|
1392
|
+
.nb-code-view { margin: 0; }
|
|
1393
|
+
.hljs-comment, .hljs-quote { color: var(--color-text2); font-style: italic; }
|
|
1394
|
+
.hljs-keyword, .hljs-selector-tag, .hljs-literal, .hljs-meta-keyword { color: var(--color-accent); font-weight: 500; }
|
|
1395
|
+
.hljs-string, .hljs-regexp, .hljs-addition, .hljs-attribute, .hljs-meta-string { color: var(--color-teal); }
|
|
1396
|
+
.hljs-number, .hljs-symbol, .hljs-bullet, .hljs-link { color: var(--color-amber); }
|
|
1397
|
+
.hljs-title, .hljs-section, .hljs-name, .hljs-selector-id, .hljs-selector-class { color: var(--color-accent2, var(--color-accent)); font-weight: 500; }
|
|
1398
|
+
.hljs-variable, .hljs-template-variable, .hljs-attr, .hljs-built_in, .hljs-builtin-name { color: var(--color-accent); }
|
|
1399
|
+
.hljs-tag, .hljs-type, .hljs-params { color: var(--color-text1); }
|
|
1400
|
+
.hljs-emphasis { font-style: italic; }
|
|
1401
|
+
.hljs-strong { font-weight: 600; }
|
|
1402
|
+
.hljs-deletion { color: var(--color-accent2, #fa6d7c); }
|
|
1394
1403
|
`;
|
|
1395
1404
|
|
|
1396
1405
|
// ---------------------------------------------------------------------------
|
|
@@ -1534,9 +1543,19 @@ export function deleteCellWithConfirm(
|
|
|
1534
1543
|
const idx = state.cells.findIndex((c) => c.id === cell.id);
|
|
1535
1544
|
if (idx < 0) return;
|
|
1536
1545
|
const label = labelFor(cell);
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1546
|
+
let snapshotCell: NotebookCell;
|
|
1547
|
+
try { snapshotCell = structuredClone(cell); }
|
|
1548
|
+
catch {
|
|
1549
|
+
snapshotCell = JSON.parse(JSON.stringify(cell, (_k, v) => {
|
|
1550
|
+
if (v === null || v === undefined) return v;
|
|
1551
|
+
const t = typeof v;
|
|
1552
|
+
if (t === 'function' || t === 'symbol' || t === 'bigint') return undefined;
|
|
1553
|
+
if (t !== 'object') return v;
|
|
1554
|
+
if (typeof Node !== 'undefined' && v instanceof Node) return undefined;
|
|
1555
|
+
if (typeof Window !== 'undefined' && v instanceof Window) return undefined;
|
|
1556
|
+
return v;
|
|
1557
|
+
}));
|
|
1558
|
+
}
|
|
1540
1559
|
// Manually push a history entry so we have a direct reference to remove on undo.
|
|
1541
1560
|
const entry: HistoryEntry = {
|
|
1542
1561
|
ts: Date.now(),
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<svelte:options customElement={{ tag: 'auto-cards', shadow: 'none' }} />
|
|
2
|
+
|
|
3
|
+
<script lang="ts">
|
|
4
|
+
import SafeImage from '../SafeImage.svelte';
|
|
5
|
+
|
|
6
|
+
export interface CardItem {
|
|
7
|
+
title: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
subtitle?: string;
|
|
10
|
+
image?: string;
|
|
11
|
+
tags?: string[];
|
|
12
|
+
href?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface CardsData {
|
|
16
|
+
title?: string;
|
|
17
|
+
cards?: CardItem[];
|
|
18
|
+
minCardWidth?: string;
|
|
19
|
+
gap?: string;
|
|
20
|
+
emptyMessage?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface Props {
|
|
24
|
+
data?: CardsData | null;
|
|
25
|
+
oncardclick?: (c: CardItem) => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let { data = {}, oncardclick }: Props = $props();
|
|
29
|
+
|
|
30
|
+
const cards = $derived.by<CardItem[]>(() => {
|
|
31
|
+
if (Array.isArray(data?.cards) && (data!.cards as unknown[]).length) return data!.cards as CardItem[];
|
|
32
|
+
return [];
|
|
33
|
+
});
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<div class="bg-surface border border-border rounded-lg p-3 md:p-4 font-sans">
|
|
37
|
+
{#if data?.title}<h3 class="text-sm font-semibold text-text1 mb-3">{data.title}</h3>{/if}
|
|
38
|
+
{#if cards.length === 0}
|
|
39
|
+
<p class="text-text2 text-sm">{data?.emptyMessage ?? 'No items'}</p>
|
|
40
|
+
{:else}
|
|
41
|
+
<div
|
|
42
|
+
class="grid"
|
|
43
|
+
style="grid-template-columns: repeat(auto-fill, minmax({data?.minCardWidth ?? '180px'}, 1fr)); gap: {data?.gap ?? '1rem'};"
|
|
44
|
+
>
|
|
45
|
+
{#each cards as card}
|
|
46
|
+
<div
|
|
47
|
+
class="bg-surface2 border border-border rounded-lg overflow-hidden hover:border-border2 transition-all {oncardclick ? 'cursor-pointer' : ''}"
|
|
48
|
+
role={oncardclick ? 'button' : undefined}
|
|
49
|
+
tabindex={oncardclick ? 0 : undefined}
|
|
50
|
+
aria-label={oncardclick ? card.title : undefined}
|
|
51
|
+
title={oncardclick ? 'Double-cliquez pour interagir' : undefined}
|
|
52
|
+
ondblclick={() => oncardclick?.(card)}
|
|
53
|
+
onkeydown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); oncardclick?.(card); } }}
|
|
54
|
+
>
|
|
55
|
+
{#if card.image}
|
|
56
|
+
<SafeImage src={card.image} alt={card.title} class="w-full h-32 object-cover" hideOnError />
|
|
57
|
+
{/if}
|
|
58
|
+
<div class="p-3">
|
|
59
|
+
<div class="font-semibold text-sm text-text1 leading-tight">{card.title}</div>
|
|
60
|
+
{#if card.subtitle}<div class="text-xs text-text2 mt-0.5">{card.subtitle}</div>{/if}
|
|
61
|
+
{#if card.description}<div class="text-xs text-text2 mt-1.5 leading-relaxed">{card.description}</div>{/if}
|
|
62
|
+
{#if card.tags?.length}
|
|
63
|
+
<div class="flex gap-1 flex-wrap mt-2">
|
|
64
|
+
{#each card.tags as tag}
|
|
65
|
+
<span class="text-[10px] font-mono px-1.5 py-0.5 rounded bg-surface2 text-text2">{tag}</span>
|
|
66
|
+
{/each}
|
|
67
|
+
</div>
|
|
68
|
+
{/if}
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
{/each}
|
|
72
|
+
</div>
|
|
73
|
+
{/if}
|
|
74
|
+
</div>
|