@webmcp-auto-ui/ui 2.5.26 → 2.5.28
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/README.md +2 -2
- package/package.json +15 -3
- package/src/agent/AgentConsole.svelte +1 -21
- package/src/agent/DataServersPanel.svelte +164 -0
- package/src/agent/LLMSelector.svelte +26 -8
- package/src/agent/ModelCacheManager.svelte +359 -0
- package/src/agent/{GemmaLoader.svelte → ModelLoader.svelte} +1 -1
- package/src/agent/SettingsPanel.svelte +16 -2
- package/src/index.ts +45 -31
- package/src/widgets/WidgetRenderer.svelte +118 -115
- package/src/widgets/export-widget.ts +28 -1
- package/src/widgets/helpers/safe-image.ts +78 -0
- package/src/widgets/notebook/.gitkeep +0 -0
- package/src/widgets/notebook/chart-renderer.ts +63 -0
- package/src/widgets/notebook/compact.ts +823 -0
- package/src/widgets/notebook/document.ts +1065 -0
- package/src/widgets/notebook/editorial.ts +936 -0
- package/src/widgets/notebook/executors/.gitkeep +1 -0
- package/src/widgets/notebook/executors/index.ts +4 -0
- package/src/widgets/notebook/executors/js-worker.ts +269 -0
- package/src/widgets/notebook/executors/sql.ts +206 -0
- package/src/widgets/notebook/import-modals.ts +553 -0
- package/src/widgets/notebook/left-pane.ts +249 -0
- package/src/widgets/notebook/prose.ts +280 -0
- package/src/widgets/notebook/recipe-browser.ts +350 -0
- package/src/widgets/notebook/recipes/compact.md +124 -0
- package/src/widgets/notebook/recipes/document.md +139 -0
- package/src/widgets/notebook/recipes/editorial.md +120 -0
- package/src/widgets/notebook/recipes/workspace.md +119 -0
- package/src/widgets/notebook/resource-extractor.ts +162 -0
- package/src/widgets/notebook/share-handlers.ts +222 -0
- package/src/widgets/notebook/shared.ts +1592 -0
- package/src/widgets/notebook/workspace.ts +852 -0
- package/src/widgets/rich/cards.ts +181 -0
- package/src/widgets/rich/carousel.ts +319 -0
- package/src/widgets/rich/chart-rich.ts +386 -0
- package/src/widgets/rich/d3.ts +503 -0
- package/src/widgets/rich/data-table.ts +342 -0
- package/src/widgets/rich/gallery.ts +350 -0
- package/src/widgets/rich/grid-data.ts +173 -0
- package/src/widgets/rich/hemicycle.ts +313 -0
- package/src/widgets/rich/js-sandbox.ts +106 -0
- package/src/widgets/rich/json-viewer.ts +202 -0
- package/src/widgets/rich/log.ts +143 -0
- package/src/widgets/rich/map.ts +218 -0
- package/src/widgets/rich/profile.ts +256 -0
- package/src/widgets/rich/sankey.ts +262 -0
- package/src/widgets/rich/stat-card.ts +125 -0
- package/src/widgets/rich/timeline.ts +179 -0
- package/src/widgets/rich/trombinoscope.ts +246 -0
- package/src/widgets/simple/actions.ts +89 -0
- package/src/widgets/simple/alert.ts +100 -0
- package/src/widgets/simple/chart.ts +189 -0
- package/src/widgets/simple/code.ts +79 -0
- package/src/widgets/simple/kv.ts +68 -0
- package/src/widgets/simple/list.ts +89 -0
- package/src/widgets/simple/stat.ts +58 -0
- package/src/widgets/simple/tags.ts +125 -0
- package/src/widgets/simple/text.ts +198 -0
- package/src/wm/FloatingLayout.svelte +2 -0
- package/src/wm/LinkIndicators.svelte +8 -15
- package/src/widgets/SafeImage.svelte +0 -76
- package/src/widgets/rich/Cards.svelte +0 -39
- package/src/widgets/rich/Carousel.svelte +0 -88
- package/src/widgets/rich/Chart.svelte +0 -142
- package/src/widgets/rich/D3Widget.svelte +0 -373
- package/src/widgets/rich/DataTable.svelte +0 -62
- package/src/widgets/rich/Gallery.svelte +0 -94
- package/src/widgets/rich/GridData.svelte +0 -44
- package/src/widgets/rich/Hemicycle.svelte +0 -78
- package/src/widgets/rich/JsSandbox.svelte +0 -51
- package/src/widgets/rich/JsonViewer.svelte +0 -42
- package/src/widgets/rich/LogViewer.svelte +0 -24
- package/src/widgets/rich/MapView.svelte +0 -140
- package/src/widgets/rich/ProfileCard.svelte +0 -59
- package/src/widgets/rich/Sankey.svelte +0 -38
- package/src/widgets/rich/StatCard.svelte +0 -35
- package/src/widgets/rich/Timeline.svelte +0 -43
- package/src/widgets/rich/Trombinoscope.svelte +0 -48
- package/src/widgets/simple/ActionsBlock.svelte +0 -15
- package/src/widgets/simple/AlertBlock.svelte +0 -11
- package/src/widgets/simple/ChartBlock.svelte +0 -21
- package/src/widgets/simple/CodeBlock.svelte +0 -11
- package/src/widgets/simple/KVBlock.svelte +0 -16
- package/src/widgets/simple/ListBlock.svelte +0 -17
- package/src/widgets/simple/StatBlock.svelte +0 -14
- package/src/widgets/simple/TagsBlock.svelte +0 -15
- package/src/widgets/simple/TextBlock.svelte +0 -122
|
@@ -0,0 +1 @@
|
|
|
1
|
+
for Agent F (sql.ts, js-worker.ts, worker-runtime.ts)
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* JS executor — runs cell content in a Web Worker, with state.scope exposed
|
|
4
|
+
* as top-level variables inside the worker's async function body.
|
|
5
|
+
*
|
|
6
|
+
* Output shape detection:
|
|
7
|
+
* - Array of objects → kind: 'table'
|
|
8
|
+
* - Array of scalars → kind: 'value'
|
|
9
|
+
* - Vega / Vega-Lite spec → kind: 'chart'
|
|
10
|
+
* - undefined / null → kind: 'empty'
|
|
11
|
+
* - Otherwise → kind: 'value'
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { CellExecutor, CellExecContext, CellResult } from '../shared.js';
|
|
15
|
+
|
|
16
|
+
export interface JsExecutorOptions {
|
|
17
|
+
/** Timeout (ms). Default 5000 */
|
|
18
|
+
timeoutMs?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function isChartSpec(v: any): boolean {
|
|
22
|
+
if (!v || typeof v !== 'object') return false;
|
|
23
|
+
// Vega
|
|
24
|
+
if (Array.isArray(v.marks)) return true;
|
|
25
|
+
// Vega-Lite
|
|
26
|
+
if (v.mark && (typeof v.mark === 'string' || typeof v.mark === 'object')) return true;
|
|
27
|
+
if (v.layer && Array.isArray(v.layer)) return true;
|
|
28
|
+
if (v.data && (v.encoding || v.mark)) return true;
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Convert the worker's raw log entries ({level, args}) into a flat string[]
|
|
34
|
+
* suitable for CellResult.logs. Each entry becomes one line, with
|
|
35
|
+
* `[warn]` / `[error]` prefixes for non-log levels (used for color coding).
|
|
36
|
+
*/
|
|
37
|
+
function normalizeLogs(raw: unknown): string[] | undefined {
|
|
38
|
+
if (!Array.isArray(raw) || raw.length === 0) return undefined;
|
|
39
|
+
const out: string[] = [];
|
|
40
|
+
for (const entry of raw) {
|
|
41
|
+
if (!entry || typeof entry !== 'object') continue;
|
|
42
|
+
const level = (entry as any).level as string | undefined;
|
|
43
|
+
const args = (entry as any).args as unknown[] | undefined;
|
|
44
|
+
const body = Array.isArray(args)
|
|
45
|
+
? args.map((a) => {
|
|
46
|
+
if (a == null) return String(a);
|
|
47
|
+
if (typeof a === 'object') { try { return JSON.stringify(a); } catch { return String(a); } }
|
|
48
|
+
return String(a);
|
|
49
|
+
}).join(' ')
|
|
50
|
+
: '';
|
|
51
|
+
const prefix = level === 'warn' ? '[warn] ' : level === 'error' ? '[error] ' : '';
|
|
52
|
+
out.push(prefix + body);
|
|
53
|
+
}
|
|
54
|
+
return out.length ? out : undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function toResult(value: unknown, durationMs: number): CellResult {
|
|
58
|
+
if (value === undefined || value === null) {
|
|
59
|
+
return { ok: true, kind: 'empty', durationMs };
|
|
60
|
+
}
|
|
61
|
+
if (Array.isArray(value)) {
|
|
62
|
+
if (value.length > 0 && typeof value[0] === 'object' && value[0] !== null && !Array.isArray(value[0])) {
|
|
63
|
+
const columns = Object.keys(value[0] as Record<string, unknown>);
|
|
64
|
+
return {
|
|
65
|
+
ok: true,
|
|
66
|
+
kind: 'table',
|
|
67
|
+
rows: value as Record<string, unknown>[],
|
|
68
|
+
columns,
|
|
69
|
+
rowCount: value.length,
|
|
70
|
+
durationMs,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
return { ok: true, kind: 'value', value, durationMs };
|
|
74
|
+
}
|
|
75
|
+
if (typeof value === 'object') {
|
|
76
|
+
if (isChartSpec(value)) {
|
|
77
|
+
return { ok: true, kind: 'chart', spec: value, durationMs };
|
|
78
|
+
}
|
|
79
|
+
return { ok: true, kind: 'value', value, durationMs };
|
|
80
|
+
}
|
|
81
|
+
return { ok: true, kind: 'value', value, durationMs };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Build the worker source inline so the executor ships as a single module
|
|
86
|
+
* without relying on bundler-specific `new URL(..., import.meta.url)` resolution.
|
|
87
|
+
*/
|
|
88
|
+
function buildWorkerBlobUrl(): string {
|
|
89
|
+
const src = `
|
|
90
|
+
const origLog = self.console && self.console.log ? self.console.log.bind(self.console) : null;
|
|
91
|
+
const origWarn = self.console && self.console.warn ? self.console.warn.bind(self.console) : null;
|
|
92
|
+
const origErr = self.console && self.console.error ? self.console.error.bind(self.console) : null;
|
|
93
|
+
|
|
94
|
+
self.addEventListener('message', async (e) => {
|
|
95
|
+
const data = e.data || {};
|
|
96
|
+
const code = data.code || '';
|
|
97
|
+
const scope = data.scope || {};
|
|
98
|
+
const logs = [];
|
|
99
|
+
self.console = {
|
|
100
|
+
log: function () { logs.push({ level: 'log', args: [].slice.call(arguments) }); origLog && origLog.apply(null, arguments); },
|
|
101
|
+
warn: function () { logs.push({ level: 'warn', args: [].slice.call(arguments) }); origWarn && origWarn.apply(null, arguments); },
|
|
102
|
+
error:function () { logs.push({ level: 'error', args: [].slice.call(arguments) }); origErr && origErr.apply(null, arguments); },
|
|
103
|
+
info: function () { logs.push({ level: 'log', args: [].slice.call(arguments) }); origLog && origLog.apply(null, arguments); },
|
|
104
|
+
debug:function () { logs.push({ level: 'log', args: [].slice.call(arguments) }); origLog && origLog.apply(null, arguments); },
|
|
105
|
+
};
|
|
106
|
+
try {
|
|
107
|
+
const keys = Object.keys(scope);
|
|
108
|
+
const values = keys.map(function (k) { return scope[k]; });
|
|
109
|
+
const fn = new Function(...keys, 'return (async () => {\\n' + code + '\\n})();');
|
|
110
|
+
const result = await fn.apply(null, values);
|
|
111
|
+
self.postMessage({ ok: true, result: result, logs: logs });
|
|
112
|
+
} catch (err) {
|
|
113
|
+
const msg = String((err && err.message) || err);
|
|
114
|
+
const isSyntax = (err instanceof SyntaxError) || /SyntaxError/.test(msg);
|
|
115
|
+
self.postMessage({ ok: false, error: msg, errorKind: isSyntax ? 'syntax' : 'runtime', logs: logs });
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
`;
|
|
119
|
+
const blob = new Blob([src], { type: 'application/javascript' });
|
|
120
|
+
return URL.createObjectURL(blob);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Cache the blob URL across executor calls — workers themselves are disposable
|
|
124
|
+
// but recreating the blob on each run wastes object URLs.
|
|
125
|
+
// Trade-off: we never auto-revoke since the cached URL stays useful for the
|
|
126
|
+
// lifetime of the page. `disposeJsWorkerCache()` is exposed for explicit
|
|
127
|
+
// cleanup (HMR, teardown) when a caller wants to release the object URL.
|
|
128
|
+
let cachedWorkerUrl: string | null = null;
|
|
129
|
+
function getWorkerUrl(): string {
|
|
130
|
+
if (cachedWorkerUrl) return cachedWorkerUrl;
|
|
131
|
+
cachedWorkerUrl = buildWorkerBlobUrl();
|
|
132
|
+
return cachedWorkerUrl;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Revoke the cached worker blob URL. Safe to call multiple times.
|
|
136
|
+
* The next executor call will create a fresh blob URL on demand. */
|
|
137
|
+
export function disposeJsWorkerCache(): void {
|
|
138
|
+
if (cachedWorkerUrl) {
|
|
139
|
+
try { URL.revokeObjectURL(cachedWorkerUrl); } catch { /* ignore */ }
|
|
140
|
+
cachedWorkerUrl = null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Best-effort structured-clone check: filter out values that can't be posted
|
|
146
|
+
* to a worker (functions, DOM nodes, etc.). Replaces them with undefined so
|
|
147
|
+
* the cell can still reference the scope variable without crashing postMessage.
|
|
148
|
+
*/
|
|
149
|
+
function sanitizeScope(scope: Record<string, unknown>): Record<string, unknown> {
|
|
150
|
+
const out: Record<string, unknown> = {};
|
|
151
|
+
for (const [k, v] of Object.entries(scope ?? {})) {
|
|
152
|
+
if (typeof v === 'function') continue;
|
|
153
|
+
if (v && typeof v === 'object') {
|
|
154
|
+
// Quick cheap check: Nodes, Windows, etc.
|
|
155
|
+
const ctor = (v as any).constructor?.name;
|
|
156
|
+
if (ctor === 'Window' || ctor === 'HTMLDocument' || (v as any).nodeType != null) continue;
|
|
157
|
+
}
|
|
158
|
+
out[k] = v;
|
|
159
|
+
}
|
|
160
|
+
return out;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function createJsExecutor(opts?: JsExecutorOptions): CellExecutor {
|
|
164
|
+
const timeoutMs = opts?.timeoutMs ?? 5_000;
|
|
165
|
+
|
|
166
|
+
return async (ctx: CellExecContext) => {
|
|
167
|
+
const startedAt = Date.now();
|
|
168
|
+
|
|
169
|
+
if (typeof Worker === 'undefined' || typeof Blob === 'undefined' || typeof URL === 'undefined') {
|
|
170
|
+
return {
|
|
171
|
+
ok: false,
|
|
172
|
+
error: 'Web Worker not available in this environment',
|
|
173
|
+
errorKind: 'runtime',
|
|
174
|
+
durationMs: Date.now() - startedAt,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const code = ctx.cell.content ?? '';
|
|
179
|
+
if (!code.trim()) {
|
|
180
|
+
return { ok: true, kind: 'empty', durationMs: Date.now() - startedAt };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const scope = sanitizeScope(ctx.scope ?? {});
|
|
184
|
+
const worker = new Worker(getWorkerUrl());
|
|
185
|
+
|
|
186
|
+
return new Promise<CellResult>((resolve) => {
|
|
187
|
+
let done = false;
|
|
188
|
+
const finish = (r: CellResult) => {
|
|
189
|
+
if (done) return;
|
|
190
|
+
done = true;
|
|
191
|
+
clearTimeout(timer);
|
|
192
|
+
ctx.signal.removeEventListener('abort', onAbort);
|
|
193
|
+
try {
|
|
194
|
+
worker.terminate();
|
|
195
|
+
} catch {
|
|
196
|
+
/* noop */
|
|
197
|
+
}
|
|
198
|
+
resolve(r);
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const timer = setTimeout(() => {
|
|
202
|
+
finish({
|
|
203
|
+
ok: false,
|
|
204
|
+
error: `JS cell timed out after ${timeoutMs}ms`,
|
|
205
|
+
errorKind: 'timeout',
|
|
206
|
+
durationMs: Date.now() - startedAt,
|
|
207
|
+
});
|
|
208
|
+
}, timeoutMs);
|
|
209
|
+
|
|
210
|
+
const onAbort = () => {
|
|
211
|
+
finish({
|
|
212
|
+
ok: false,
|
|
213
|
+
error: 'aborted',
|
|
214
|
+
errorKind: 'timeout',
|
|
215
|
+
durationMs: Date.now() - startedAt,
|
|
216
|
+
});
|
|
217
|
+
};
|
|
218
|
+
if (ctx.signal.aborted) {
|
|
219
|
+
onAbort();
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
ctx.signal.addEventListener('abort', onAbort, { once: true });
|
|
223
|
+
|
|
224
|
+
worker.addEventListener('message', (e: MessageEvent) => {
|
|
225
|
+
const msg = e.data as
|
|
226
|
+
| { ok: true; result: unknown; logs?: unknown[] }
|
|
227
|
+
| { ok: false; error: string; errorKind?: 'syntax' | 'runtime'; logs?: unknown[] };
|
|
228
|
+
const durationMs = Date.now() - startedAt;
|
|
229
|
+
if (!msg || typeof msg !== 'object') {
|
|
230
|
+
finish({ ok: false, error: 'Invalid worker response', errorKind: 'runtime', durationMs });
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
const logs = normalizeLogs((msg as any).logs);
|
|
234
|
+
if (msg.ok) {
|
|
235
|
+
const base = toResult((msg as any).result, durationMs);
|
|
236
|
+
finish(logs ? { ...base, logs } as CellResult : base);
|
|
237
|
+
} else {
|
|
238
|
+
finish({
|
|
239
|
+
ok: false,
|
|
240
|
+
error: (msg as any).error || 'Unknown error',
|
|
241
|
+
errorKind: (msg as any).errorKind === 'syntax' ? 'syntax' : 'runtime',
|
|
242
|
+
durationMs,
|
|
243
|
+
logs,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
worker.addEventListener('error', (e: ErrorEvent) => {
|
|
249
|
+
finish({
|
|
250
|
+
ok: false,
|
|
251
|
+
error: e.message || 'Worker error',
|
|
252
|
+
errorKind: 'runtime',
|
|
253
|
+
durationMs: Date.now() - startedAt,
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
worker.postMessage({ code, scope });
|
|
259
|
+
} catch (err: any) {
|
|
260
|
+
finish({
|
|
261
|
+
ok: false,
|
|
262
|
+
error: `Failed to post scope to worker: ${String(err?.message ?? err)}`,
|
|
263
|
+
errorKind: 'runtime',
|
|
264
|
+
durationMs: Date.now() - startedAt,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
});
|
|
268
|
+
};
|
|
269
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/**
|
|
3
|
+
* SQL executor for notebook cells.
|
|
4
|
+
*
|
|
5
|
+
* Finds a SQL-capable tool on the connected data servers via auto-pattern,
|
|
6
|
+
* calls it via postMessage, parses the result into a `table` CellResult.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { callToolViaPostMessage } from '@webmcp-auto-ui/core';
|
|
10
|
+
import type { CellExecutor, CellExecContext, DataServerDescriptor } from '../shared.js';
|
|
11
|
+
|
|
12
|
+
export interface SqlExecutorOptions {
|
|
13
|
+
/** Timeout per query (ms). Default 30000 */
|
|
14
|
+
timeoutMs?: number;
|
|
15
|
+
/** Max rows to keep in result (truncate beyond). Default 1000 */
|
|
16
|
+
maxRows?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const PATTERN_PRIMARY = /^.*query_sql$/i;
|
|
20
|
+
const PATTERN_FALLBACK = /^(query|run|execute)(_sql)?$/i;
|
|
21
|
+
|
|
22
|
+
function findSqlTool(servers: DataServerDescriptor[]): string | null {
|
|
23
|
+
// Priority 1: *_query_sql or query_sql
|
|
24
|
+
for (const srv of servers) {
|
|
25
|
+
for (const t of srv.tools ?? []) {
|
|
26
|
+
if (PATTERN_PRIMARY.test(t.name)) return t.name;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
// Priority 2: query / run / execute (with optional _sql)
|
|
30
|
+
for (const srv of servers) {
|
|
31
|
+
for (const t of srv.tools ?? []) {
|
|
32
|
+
if (PATTERN_FALLBACK.test(t.name)) return t.name;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Extract the first text content from an MCP tool result.
|
|
40
|
+
*/
|
|
41
|
+
function extractText(result: any): string | null {
|
|
42
|
+
if (!result) return null;
|
|
43
|
+
const content = result.content ?? result;
|
|
44
|
+
if (!Array.isArray(content)) {
|
|
45
|
+
if (typeof content === 'string') return content;
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
for (const item of content) {
|
|
49
|
+
if (item && item.type === 'text' && typeof item.text === 'string') {
|
|
50
|
+
return item.text;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function tryParseJson(text: string): unknown {
|
|
57
|
+
try {
|
|
58
|
+
return JSON.parse(text);
|
|
59
|
+
} catch {
|
|
60
|
+
return text; // not JSON, return raw
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function createSqlExecutor(
|
|
65
|
+
getServers: () => DataServerDescriptor[],
|
|
66
|
+
opts?: SqlExecutorOptions
|
|
67
|
+
): CellExecutor {
|
|
68
|
+
const timeoutMs = opts?.timeoutMs ?? 30_000;
|
|
69
|
+
const maxRows = opts?.maxRows ?? 1000;
|
|
70
|
+
|
|
71
|
+
return async (ctx: CellExecContext) => {
|
|
72
|
+
const startedAt = Date.now();
|
|
73
|
+
const servers = getServers();
|
|
74
|
+
|
|
75
|
+
const toolName = findSqlTool(servers);
|
|
76
|
+
if (!toolName) {
|
|
77
|
+
return {
|
|
78
|
+
ok: false,
|
|
79
|
+
error: 'No SQL tool found on connected servers',
|
|
80
|
+
errorKind: 'schema',
|
|
81
|
+
durationMs: Date.now() - startedAt,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const sql = (ctx.cell.content ?? '').trim();
|
|
86
|
+
if (!sql) {
|
|
87
|
+
return { ok: true, kind: 'empty', durationMs: Date.now() - startedAt };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Wrap the tool call with the cell's AbortSignal so an external abort
|
|
91
|
+
// rejects the promise even if callToolViaPostMessage doesn't support signals.
|
|
92
|
+
const callPromise = callToolViaPostMessage(toolName, { sql }, { timeout: timeoutMs });
|
|
93
|
+
|
|
94
|
+
let raceResult: any;
|
|
95
|
+
try {
|
|
96
|
+
raceResult = await new Promise((resolve, reject) => {
|
|
97
|
+
let settled = false;
|
|
98
|
+
const onAbort = () => {
|
|
99
|
+
if (settled) return;
|
|
100
|
+
settled = true;
|
|
101
|
+
reject(new Error('aborted'));
|
|
102
|
+
};
|
|
103
|
+
if (ctx.signal.aborted) {
|
|
104
|
+
onAbort();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
ctx.signal.addEventListener('abort', onAbort, { once: true });
|
|
108
|
+
callPromise.then(
|
|
109
|
+
(v) => {
|
|
110
|
+
if (settled) return;
|
|
111
|
+
settled = true;
|
|
112
|
+
ctx.signal.removeEventListener('abort', onAbort);
|
|
113
|
+
resolve(v);
|
|
114
|
+
},
|
|
115
|
+
(err) => {
|
|
116
|
+
if (settled) return;
|
|
117
|
+
settled = true;
|
|
118
|
+
ctx.signal.removeEventListener('abort', onAbort);
|
|
119
|
+
reject(err);
|
|
120
|
+
}
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
} catch (err: any) {
|
|
124
|
+
const durationMs = Date.now() - startedAt;
|
|
125
|
+
const msg = String(err?.message ?? err);
|
|
126
|
+
const isTimeout = /timed out|aborted/i.test(msg);
|
|
127
|
+
return {
|
|
128
|
+
ok: false,
|
|
129
|
+
error: msg,
|
|
130
|
+
errorKind: isTimeout ? 'timeout' : 'runtime',
|
|
131
|
+
durationMs,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const durationMs = Date.now() - startedAt;
|
|
136
|
+
|
|
137
|
+
// Unwrap MCP content array → text → JSON.
|
|
138
|
+
const text = extractText(raceResult);
|
|
139
|
+
const parsed = text != null ? tryParseJson(text) : raceResult;
|
|
140
|
+
|
|
141
|
+
// Error shape returned inside the tool result
|
|
142
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed) && 'error' in (parsed as any) && (parsed as any).error) {
|
|
143
|
+
return {
|
|
144
|
+
ok: false,
|
|
145
|
+
error: String((parsed as any).error),
|
|
146
|
+
errorKind: 'runtime',
|
|
147
|
+
durationMs,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// {rows, columns} shape
|
|
152
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed) && Array.isArray((parsed as any).rows)) {
|
|
153
|
+
let rows: Record<string, unknown>[] = (parsed as any).rows;
|
|
154
|
+
const columns: string[] = Array.isArray((parsed as any).columns)
|
|
155
|
+
? (parsed as any).columns
|
|
156
|
+
: rows.length > 0 && typeof rows[0] === 'object'
|
|
157
|
+
? Object.keys(rows[0] as Record<string, unknown>)
|
|
158
|
+
: [];
|
|
159
|
+
let truncated = false;
|
|
160
|
+
if (rows.length > maxRows) {
|
|
161
|
+
rows = rows.slice(0, maxRows);
|
|
162
|
+
truncated = true;
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
ok: true,
|
|
166
|
+
kind: 'table',
|
|
167
|
+
rows,
|
|
168
|
+
columns,
|
|
169
|
+
rowCount: (parsed as any).rows.length,
|
|
170
|
+
truncated: truncated || undefined,
|
|
171
|
+
durationMs,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Array of objects
|
|
176
|
+
if (Array.isArray(parsed)) {
|
|
177
|
+
if (parsed.length === 0) {
|
|
178
|
+
return { ok: true, kind: 'table', rows: [], columns: [], rowCount: 0, durationMs };
|
|
179
|
+
}
|
|
180
|
+
const first = parsed[0];
|
|
181
|
+
if (first && typeof first === 'object' && !Array.isArray(first)) {
|
|
182
|
+
let rows = parsed as Record<string, unknown>[];
|
|
183
|
+
const columns = Object.keys(first as Record<string, unknown>);
|
|
184
|
+
const rowCount = rows.length;
|
|
185
|
+
let truncated = false;
|
|
186
|
+
if (rows.length > maxRows) {
|
|
187
|
+
rows = rows.slice(0, maxRows);
|
|
188
|
+
truncated = true;
|
|
189
|
+
}
|
|
190
|
+
return {
|
|
191
|
+
ok: true,
|
|
192
|
+
kind: 'table',
|
|
193
|
+
rows,
|
|
194
|
+
columns,
|
|
195
|
+
rowCount,
|
|
196
|
+
truncated: truncated || undefined,
|
|
197
|
+
durationMs,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
return { ok: true, kind: 'value', value: parsed, durationMs };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Anything else — a scalar or object that isn't tabular
|
|
204
|
+
return { ok: true, kind: 'value', value: parsed, durationMs };
|
|
205
|
+
};
|
|
206
|
+
}
|