@barivia/barmesh-mcp 0.5.2 → 0.5.4
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 +9 -4
- package/dist/index.js +1 -1
- package/dist/job_monitor.js +208 -0
- package/dist/shared.js +18 -2
- package/dist/tools/barmesh_results_explorer.js +3 -0
- package/dist/tools/cfd.js +2 -2
- package/dist/tools/guide.js +1 -1
- package/dist/tools/jobs.js +31 -6
- package/dist/tools/results.js +56 -7
- package/dist/tools/training_monitor.js +56 -0
- package/dist/views/src/views/barmesh-results-explorer/index.html +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -55,7 +55,8 @@ API key; otherwise the analysis calls return HTTP 403. Contact Barivia to enable
|
|
|
55
55
|
| `barmesh_datasets` | Upload / preview / list / get / subset / delete the mesh CSV. |
|
|
56
56
|
| `barmesh_mesh_convergence` | SOM fingerprint distances (async job). |
|
|
57
57
|
| `barmesh_richardson` | Richardson/GCI on scalar QoIs (async job). |
|
|
58
|
-
| `barmesh_jobs` | Poll job status / list jobs
|
|
58
|
+
| `barmesh_jobs` | Poll job status / block until terminal (`action=monitor`) / list jobs. Auto-polls CFD prepare + finalize when applicable. Reports phase, epoch/total, elapsed, ETA, and QE live during the SOM training. |
|
|
59
|
+
| `barmesh_training_monitor` | Alias for `barmesh_jobs(action=monitor)` — server-side poll with throttled snapshots until terminal (preferred after submit). |
|
|
59
60
|
| `barmesh_results` | Distances, convergence reading, and figures. `action=get` inlines headline PNGs; `action=download` saves artifacts to disk; `action=render` produces publication PDFs on demand. |
|
|
60
61
|
| `barmesh_results_explorer` | Interactive MCP App with a **figure dropdown above the plot** (or standalone localhost page) to browse every figure after a job completes. |
|
|
61
62
|
| `barmesh_send_feedback` | Send a short note or bug report to the Barivia team. |
|
|
@@ -64,7 +65,8 @@ API key; otherwise the analysis calls return HTTP 403. Contact Barivia to enable
|
|
|
64
65
|
|
|
65
66
|
- **Combined overview (`combined.png`):** barsom-style **grid of all component planes**
|
|
66
67
|
on the trained SOM — the primary headline artifact. A separate
|
|
67
|
-
`overview_distances.png` holds the KL/EMD
|
|
68
|
+
`overview_distances.png` holds the KL/EMD diagnostic mosaic (all four distance panels).
|
|
69
|
+
`plot_vol_all_meshes.png` shows volume fingerprints for every mesh in mesh_order.
|
|
68
70
|
- **Learning curve:** every job produces `learning_curve.png` (QE by epoch) for training
|
|
69
71
|
quality inspection; listed in the results explorer dropdown.
|
|
70
72
|
- **PDFs on demand:** publication vector PDFs are NOT generated by default. Render them
|
|
@@ -76,11 +78,14 @@ API key; otherwise the analysis calls return HTTP 403. Contact Barivia to enable
|
|
|
76
78
|
figures). PNG previews inline; PDF/SVG offered for download when rendered.
|
|
77
79
|
- **Uploads:** large CSVs use presigned PUT with explicit `Content-Length`; `.csv.gz` /
|
|
78
80
|
`.tsv.gz` accepted. Pin `@barivia/barmesh-mcp@0.5.2` (clear `~/.npm/_npx` if stale).
|
|
79
|
-
- **Live progress:** `barmesh_jobs(action=
|
|
80
|
-
|
|
81
|
+
- **Live progress:** `barmesh_training_monitor(job_id)` or `barmesh_jobs(action=monitor)` block
|
|
82
|
+
server-side with compact snapshots (phase, epoch, QE/TE, ETA, ordering_errors tail) until
|
|
83
|
+
terminal or `block_until_sec` (default 900). Waits for `cfd_finalize` by default. One-shot:
|
|
84
|
+
`barmesh_jobs(action=status)`.
|
|
81
85
|
|
|
82
86
|
### Migration notes
|
|
83
87
|
|
|
88
|
+
- **`barmesh_training_monitor` (0.5.3):** server-side blocking monitor with throttled snapshots — preferred after job submit instead of manual `barmesh_jobs(status)` loops. Equivalent to `barmesh_jobs(action=monitor)`.
|
|
84
89
|
- **`send_feedback` → `barmesh_send_feedback` (0.3.0):** the feedback tool was renamed so it no longer collides with the `@barivia/barsom-mcp` tool of the same name when both servers are enabled in one client. Update any direct call sites; the behavior is unchanged.
|
|
85
90
|
|
|
86
91
|
## Data format (mesh_convergence)
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{McpServer as e}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as r}from"@modelcontextprotocol/sdk/server/stdio.js";import{getUiCapability as s,registerAppResource as o,RESOURCE_MIME_TYPE as
|
|
2
|
+
import{McpServer as e}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as r}from"@modelcontextprotocol/sdk/server/stdio.js";import{getUiCapability as s,registerAppResource as o,RESOURCE_MIME_TYPE as t}from"@modelcontextprotocol/ext-apps/server";import{startVizServer as n}from"./viz-server.js";import{API_KEY as i,CLIENT_VERSION as a,apiCall as c,apiRawCall as m,loadViewHtml as l,setVizPort as d,setClientSupportsMcpApps as h}from"./shared.js";import{registerGuideTool as p}from"./tools/guide.js";import{registerDatasetsTool as b}from"./tools/datasets.js";import{registerCfdTools as f}from"./tools/cfd.js";import{registerJobsTool as u}from"./tools/jobs.js";import{registerResultsTool as _}from"./tools/results.js";import{registerResultsExplorerTool as v,RESULTS_EXPLORER_URI as g}from"./tools/barmesh_results_explorer.js";import{registerTrainingMonitorTool as y}from"./tools/training_monitor.js";import{registerFeedbackTool as j}from"./tools/feedback.js";i||(console.error("Error: BARIVIA_API_KEY not set. Set it in your MCP client config."),process.exit(1));(async function(){const i=new e({name:"barmesh",version:a},{instructions:"# Barivia barmesh — CFD mesh-convergence analytics\n\nSOM-based mesh-convergence verification: compare CFD meshes of a refinement study by the\nvolume-weighted distribution their cells form on a shared self-organizing map, plus\nclassical Richardson/GCI on scalar quantities.\n\n## Two tracks\n- barmesh_mesh_convergence: high-dimensional field comparison. Symmetric KL and\n Wasserstein-1 (EMD) distances between each mesh's SOM fingerprint and a reference,\n and between consecutive meshes. Decreasing, plateauing distances toward the finest\n mesh indicate sufficiency. Complements (does not replace) numerical uncertainty analysis.\n- barmesh_richardson: classical grid-convergence index on scalar QoIs.\n\n## Workflow (read-only first)\n1. barmesh_guide_workflow — orient and confirm your plan includes CFD tools.\n2. barmesh_prepare_mesh_data — recipe for the combined per-cell CSV (mesh_id + features + cell volume V).\n3. barmesh_datasets(action=upload) then preview.\n4. barmesh_mesh_convergence (and/or barmesh_richardson) — returns a job id.\n5. barmesh_training_monitor(job_id) or barmesh_jobs(action=monitor) — blocks with throttled snapshots until done (preferred); or barmesh_jobs(status) for one-shot polls every 10-20s.\n6. barmesh_results(action=get) — distances, convergence reading, and figures; then\n barmesh_results_explorer(job_id) to browse every figure interactively.\n\nThese tools are gated by the 'cfd' entitlement; analysis calls return 403 if your plan\ndoes not include it."});o(i,g,g,{mimeType:t},async()=>{const e=await l("barmesh-results-explorer");return{contents:[{uri:g,mimeType:t,text:e??"<html><body>Results Explorer view not built yet. Run: npm run build:views</body></html>"}]}}),p(i),v(i),b(i),f(i),u(i),y(i),_(i),j(i);try{const e=await n(c,m,l);d(e)}catch(e){process.env.BARIVIA_VIZ_PORT&&console.error("barmesh viz server failed to start:",e)}const w=i.server;w.oninitialized=()=>{const e=w.getClientCapabilities(),r=s(e);h(!!r?.mimeTypes?.includes(t))};const x=new r;await i.connect(x),console.error(`barmesh-mcp ${a} ready (API: ${process.env.BARIVIA_API_URL??"https://api.barivia.se"})`)})().catch(e=>{console.error("Fatal error starting barmesh-mcp:",e),process.exit(1)});
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side job monitor for barmesh async workflows.
|
|
3
|
+
* Polls GET /v1/jobs/:id until terminal (or block_until timeout) and emits
|
|
4
|
+
* compact throttled snapshots so agents avoid manual status poll loops.
|
|
5
|
+
*/
|
|
6
|
+
import { apiCall } from "./shared.js";
|
|
7
|
+
import { formatJobStatusText } from "./job_status_format.js";
|
|
8
|
+
import { pollCfdFinalizeIfPresent, refreshJobAfterFinalize } from "./cfd_finalize.js";
|
|
9
|
+
export const DEFAULT_BLOCK_UNTIL_SEC = 900;
|
|
10
|
+
export const DEFAULT_POLL_INTERVAL_SEC = 5;
|
|
11
|
+
export const MIN_POLL_INTERVAL_SEC = 5;
|
|
12
|
+
export const HEARTBEAT_POLLS = 1;
|
|
13
|
+
function sleep(ms) {
|
|
14
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
15
|
+
}
|
|
16
|
+
function num(data, key) {
|
|
17
|
+
const v = data[key];
|
|
18
|
+
if (v == null || Number.isNaN(Number(v)))
|
|
19
|
+
return undefined;
|
|
20
|
+
return Number(v);
|
|
21
|
+
}
|
|
22
|
+
function str(data, key) {
|
|
23
|
+
const v = data[key];
|
|
24
|
+
if (v == null || String(v) === "")
|
|
25
|
+
return undefined;
|
|
26
|
+
return String(v);
|
|
27
|
+
}
|
|
28
|
+
function tailOrderingErrors(data, n = 4) {
|
|
29
|
+
const raw = data.ordering_errors;
|
|
30
|
+
if (!Array.isArray(raw) || raw.length === 0)
|
|
31
|
+
return undefined;
|
|
32
|
+
return raw.slice(-n).map((x) => Number(x)).filter((x) => !Number.isNaN(x));
|
|
33
|
+
}
|
|
34
|
+
export function snapshotFromJob(data, elapsedSec, note) {
|
|
35
|
+
const status = String(data.status ?? "unknown");
|
|
36
|
+
const progress_pct = (data.progress ?? 0) * 100;
|
|
37
|
+
const snap = {
|
|
38
|
+
elapsed_sec: elapsedSec,
|
|
39
|
+
status,
|
|
40
|
+
progress_pct: Math.round(progress_pct * 10) / 10,
|
|
41
|
+
};
|
|
42
|
+
const phase = str(data, "progress_phase");
|
|
43
|
+
if (phase)
|
|
44
|
+
snap.phase = phase;
|
|
45
|
+
const epoch = num(data, "epoch");
|
|
46
|
+
const total = num(data, "total_epochs");
|
|
47
|
+
if (epoch != null)
|
|
48
|
+
snap.epoch = epoch;
|
|
49
|
+
if (total != null)
|
|
50
|
+
snap.total_epochs = total;
|
|
51
|
+
const qe = num(data, "quantization_error");
|
|
52
|
+
const te = num(data, "topographic_error");
|
|
53
|
+
if (qe != null)
|
|
54
|
+
snap.qe = Math.round(qe * 10_000) / 10_000;
|
|
55
|
+
if (te != null)
|
|
56
|
+
snap.te = Math.round(te * 10_000) / 10_000;
|
|
57
|
+
const eta = num(data, "training_eta_sec");
|
|
58
|
+
if (eta != null && eta > 0)
|
|
59
|
+
snap.eta_sec = Math.round(eta);
|
|
60
|
+
const tail = tailOrderingErrors(data);
|
|
61
|
+
if (tail && tail.length > 0)
|
|
62
|
+
snap.ordering_errors_tail = tail;
|
|
63
|
+
if (note)
|
|
64
|
+
snap.note = note;
|
|
65
|
+
return snap;
|
|
66
|
+
}
|
|
67
|
+
/** True when a new snapshot is worth recording (phase/epoch/progress/status change). */
|
|
68
|
+
export function shouldRecordSnapshot(prev, next) {
|
|
69
|
+
if (!prev)
|
|
70
|
+
return true;
|
|
71
|
+
if (prev.status !== next.status)
|
|
72
|
+
return true;
|
|
73
|
+
if (next.note)
|
|
74
|
+
return true;
|
|
75
|
+
if (prev.phase !== next.phase)
|
|
76
|
+
return true;
|
|
77
|
+
if (prev.epoch !== next.epoch)
|
|
78
|
+
return true;
|
|
79
|
+
if (Math.abs(prev.progress_pct - next.progress_pct) >= 1)
|
|
80
|
+
return true;
|
|
81
|
+
if (prev.qe !== next.qe || prev.te !== next.te)
|
|
82
|
+
return true;
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
export function formatSnapshotLine(s) {
|
|
86
|
+
const parts = [`[+${s.elapsed_sec}s] ${s.status} ${s.progress_pct.toFixed(1)}%`];
|
|
87
|
+
if (s.phase)
|
|
88
|
+
parts.push(`phase ${s.phase}`);
|
|
89
|
+
if (s.epoch != null && s.total_epochs != null)
|
|
90
|
+
parts.push(`epoch ${s.epoch}/${s.total_epochs}`);
|
|
91
|
+
if (s.qe != null)
|
|
92
|
+
parts.push(`QE ${s.qe.toFixed(4)}`);
|
|
93
|
+
if (s.te != null)
|
|
94
|
+
parts.push(`TE ${s.te.toFixed(4)}`);
|
|
95
|
+
if (s.eta_sec != null)
|
|
96
|
+
parts.push(`ETA ~${s.eta_sec}s`);
|
|
97
|
+
if (s.ordering_errors_tail?.length) {
|
|
98
|
+
parts.push(`ordering_errors tail [${s.ordering_errors_tail.map((x) => x.toFixed(4)).join(", ")}]`);
|
|
99
|
+
}
|
|
100
|
+
if (s.note)
|
|
101
|
+
parts.push(s.note);
|
|
102
|
+
return parts.join(" | ");
|
|
103
|
+
}
|
|
104
|
+
export function formatMonitorText(result, opts) {
|
|
105
|
+
const lines = [
|
|
106
|
+
`Job ${result.job_id} monitor (block_until=${opts.block_until_sec}s, poll=${opts.poll_interval_sec}s):`,
|
|
107
|
+
];
|
|
108
|
+
for (const s of result.snapshots)
|
|
109
|
+
lines.push(formatSnapshotLine(s));
|
|
110
|
+
lines.push("");
|
|
111
|
+
if (result.timed_out) {
|
|
112
|
+
lines.push(`Timed out before terminal state. Last status: ${result.status_text}`);
|
|
113
|
+
lines.push(`Re-run barmesh_training_monitor(job_id="${result.job_id}") or barmesh_jobs(action=monitor, job_id="${result.job_id}").`);
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
lines.push(`Terminal: ${result.status_text}`);
|
|
117
|
+
}
|
|
118
|
+
lines.push(result.suggested_next_step);
|
|
119
|
+
return lines.join("\n");
|
|
120
|
+
}
|
|
121
|
+
function suggestedNextStep(job_id, data) {
|
|
122
|
+
const status = String(data.status ?? "");
|
|
123
|
+
if (status === "completed") {
|
|
124
|
+
const finalizeId = str(data, "finalize_job_id");
|
|
125
|
+
if (finalizeId) {
|
|
126
|
+
return `Compute done; cfd_finalize may still be running. Poll barmesh_jobs(status) or re-run monitor with wait_finalize=true, then barmesh_results(action=get, job_id="${job_id}").`;
|
|
127
|
+
}
|
|
128
|
+
return `Next: barmesh_results(action=get, job_id="${job_id}") or barmesh_results_explorer(job_id="${job_id}").`;
|
|
129
|
+
}
|
|
130
|
+
if (status === "failed") {
|
|
131
|
+
const stage = str(data, "failure_stage");
|
|
132
|
+
return `Job failed${stage ? ` at ${stage}` : ""}. Read the error above before retrying.`;
|
|
133
|
+
}
|
|
134
|
+
if (status === "cancelled")
|
|
135
|
+
return `Job cancelled. Confirm with barmesh_jobs(action=status, job_id="${job_id}").`;
|
|
136
|
+
return `Still running — re-run barmesh_training_monitor(job_id="${job_id}") to continue waiting.`;
|
|
137
|
+
}
|
|
138
|
+
async function fetchTrainingLogHint(job_id, data) {
|
|
139
|
+
const status = String(data.status ?? "");
|
|
140
|
+
if (status !== "completed" && status !== "failed")
|
|
141
|
+
return undefined;
|
|
142
|
+
try {
|
|
143
|
+
await apiCall("GET", `/v1/results/${job_id}/training-log`);
|
|
144
|
+
return `Training log available at GET /v1/results/${job_id}/training-log (learning_curve.png in results after finalize).`;
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return undefined;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
export async function monitorJob(job_id, options = {}) {
|
|
151
|
+
const block_until_sec = Math.max(30, options.block_until_sec ?? DEFAULT_BLOCK_UNTIL_SEC);
|
|
152
|
+
const poll_interval_sec = Math.max(MIN_POLL_INTERVAL_SEC, options.poll_interval_sec ?? DEFAULT_POLL_INTERVAL_SEC);
|
|
153
|
+
const wait_finalize = options.wait_finalize !== false;
|
|
154
|
+
const blockMs = block_until_sec * 1000;
|
|
155
|
+
const pollMs = poll_interval_sec * 1000;
|
|
156
|
+
const start = Date.now();
|
|
157
|
+
const snapshots = [];
|
|
158
|
+
let lastSnap = null;
|
|
159
|
+
let data = {};
|
|
160
|
+
let heartbeat = 0;
|
|
161
|
+
while (Date.now() - start < blockMs) {
|
|
162
|
+
data = (await apiCall("GET", `/v1/jobs/${job_id}`));
|
|
163
|
+
const elapsedSec = Math.round((Date.now() - start) / 1000);
|
|
164
|
+
const snap = snapshotFromJob(data, elapsedSec);
|
|
165
|
+
heartbeat += 1;
|
|
166
|
+
const heartbeatDue = heartbeat >= HEARTBEAT_POLLS;
|
|
167
|
+
if (shouldRecordSnapshot(lastSnap, snap) || heartbeatDue) {
|
|
168
|
+
snapshots.push(snap);
|
|
169
|
+
lastSnap = snap;
|
|
170
|
+
heartbeat = 0;
|
|
171
|
+
}
|
|
172
|
+
const status = String(data.status ?? "");
|
|
173
|
+
if (status === "completed" || status === "failed" || status === "cancelled") {
|
|
174
|
+
break;
|
|
175
|
+
}
|
|
176
|
+
await sleep(pollMs);
|
|
177
|
+
}
|
|
178
|
+
const status = String(data.status ?? "");
|
|
179
|
+
const terminal = status === "completed" || status === "failed" || status === "cancelled";
|
|
180
|
+
const timed_out = !terminal;
|
|
181
|
+
if (terminal && status === "completed" && wait_finalize && data.finalize_job_id) {
|
|
182
|
+
const finalizeId = String(data.finalize_job_id);
|
|
183
|
+
const elapsedSec = Math.round((Date.now() - start) / 1000);
|
|
184
|
+
snapshots.push(snapshotFromJob(data, elapsedSec, `cfd_finalize ${finalizeId} started — waiting for figure render`));
|
|
185
|
+
try {
|
|
186
|
+
const { note } = await pollCfdFinalizeIfPresent(job_id, data, Math.max(0, blockMs - (Date.now() - start)));
|
|
187
|
+
data = await refreshJobAfterFinalize(job_id);
|
|
188
|
+
snapshots.push(snapshotFromJob(data, Math.round((Date.now() - start) / 1000), note ?? `cfd_finalize ${finalizeId} completed`));
|
|
189
|
+
}
|
|
190
|
+
catch (err) {
|
|
191
|
+
snapshots.push(snapshotFromJob(data, Math.round((Date.now() - start) / 1000), `cfd_finalize failed: ${err.message}`));
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const statusText = await formatJobStatusText(job_id, data);
|
|
195
|
+
const logHint = terminal ? await fetchTrainingLogHint(job_id, data) : undefined;
|
|
196
|
+
let suggested = suggestedNextStep(job_id, data);
|
|
197
|
+
if (logHint)
|
|
198
|
+
suggested += ` ${logHint}`;
|
|
199
|
+
return {
|
|
200
|
+
job_id,
|
|
201
|
+
terminal,
|
|
202
|
+
timed_out,
|
|
203
|
+
snapshots,
|
|
204
|
+
status_text: statusText,
|
|
205
|
+
data,
|
|
206
|
+
suggested_next_step: suggested,
|
|
207
|
+
};
|
|
208
|
+
}
|
package/dist/shared.js
CHANGED
|
@@ -22,7 +22,7 @@ export const FETCH_TIMEOUT_MS = parseInt(process.env.BARIVIA_FETCH_TIMEOUT_MS ??
|
|
|
22
22
|
export const MAX_RETRIES = 2;
|
|
23
23
|
export const RETRYABLE_STATUS = new Set([502, 503, 504]);
|
|
24
24
|
/** Single source of truth for the proxy version. Keep in sync with package.json on bump. */
|
|
25
|
-
export const CLIENT_VERSION = "0.5.
|
|
25
|
+
export const CLIENT_VERSION = "0.5.4";
|
|
26
26
|
export const PUBLIC_SITE_ORIGIN = "https://barivia.se";
|
|
27
27
|
/** Large per-cell CSV uploads may exceed the default fetch timeout. */
|
|
28
28
|
export const UPLOAD_DATASET_TIMEOUT_MS = 180_000;
|
|
@@ -389,7 +389,7 @@ export function getCaptionForImage(filename) {
|
|
|
389
389
|
if (base === "combined")
|
|
390
390
|
return "All feature component planes on the trained SOM in one grid — primary comparison artifact (barsom-style combined view).";
|
|
391
391
|
if (base === "overview_distances")
|
|
392
|
-
return "Diagnostic mosaic:
|
|
392
|
+
return "Diagnostic mosaic: KL and EMD vs reference and stepwise (all distance panels).";
|
|
393
393
|
if (base === "learning_curve")
|
|
394
394
|
return "Quantization error by epoch: ordering phase (steel blue) then convergence (coral).";
|
|
395
395
|
if (base === "KL_ref")
|
|
@@ -400,6 +400,8 @@ export function getCaptionForImage(filename) {
|
|
|
400
400
|
return "Wasserstein-1 (EMD) distance from each mesh to the reference. A monotone decrease toward the finest mesh indicates field-level convergence.";
|
|
401
401
|
if (base === "EMD_stepwise")
|
|
402
402
|
return "Stepwise EMD between consecutive meshes. Small, plateauing values on the finest pairs indicate the fingerprint has stopped changing.";
|
|
403
|
+
if (base === "plot_vol_all_meshes")
|
|
404
|
+
return "Volume-weighted SOM fingerprints P_vol for every mesh in mesh_order (shared color scale).";
|
|
403
405
|
if (base === "plot_vol_coarse_fine")
|
|
404
406
|
return "Volume-weighted SOM fingerprints P_vol for the coarsest and reference meshes (shared color scale).";
|
|
405
407
|
if (base.startsWith("plot_vol_steps") || base.startsWith("plot_vol_seven_steps"))
|
|
@@ -428,6 +430,12 @@ export async function loadViewHtml(viewName) {
|
|
|
428
430
|
}
|
|
429
431
|
return null;
|
|
430
432
|
}
|
|
433
|
+
export const MAX_INLINE_BYTES = parseInt(process.env.BARIVIA_MAX_INLINE_BYTES ?? "8000000", 10);
|
|
434
|
+
let _inlineAttachBytesUsed = 0;
|
|
435
|
+
/** Reset inline image byte budget (call at the start of each MCP tool response). */
|
|
436
|
+
export function resetInlineAttachBudget() {
|
|
437
|
+
_inlineAttachBytesUsed = 0;
|
|
438
|
+
}
|
|
431
439
|
export async function tryAttachImage(content, jobId, filename) {
|
|
432
440
|
if (filename.endsWith(".pdf") || filename.endsWith(".svg")) {
|
|
433
441
|
content.push({
|
|
@@ -441,6 +449,14 @@ export async function tryAttachImage(content, jobId, filename) {
|
|
|
441
449
|
content.push({ type: "text", text: `${filename}: ${caption}` });
|
|
442
450
|
try {
|
|
443
451
|
const { data: imgBuf } = await apiRawCall(`/v1/results/${jobId}/image/${filename}`);
|
|
452
|
+
if (_inlineAttachBytesUsed + imgBuf.length > MAX_INLINE_BYTES) {
|
|
453
|
+
content.push({
|
|
454
|
+
type: "text",
|
|
455
|
+
text: `(${filename} omitted — inline image budget exceeded; use barmesh_results(action=image, job_id="${jobId}", filename="${filename}") or action=download)`,
|
|
456
|
+
});
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
_inlineAttachBytesUsed += imgBuf.length;
|
|
444
460
|
content.push({
|
|
445
461
|
type: "image",
|
|
446
462
|
data: imgBuf.toString("base64"),
|
|
@@ -11,6 +11,7 @@ const FIGURE_SORT_RANK = {
|
|
|
11
11
|
EMD_ref: 12,
|
|
12
12
|
EMD_stepwise: 13,
|
|
13
13
|
learning_curve: 14,
|
|
14
|
+
plot_vol_all_meshes: 19,
|
|
14
15
|
plot_vol_coarse_fine: 20,
|
|
15
16
|
};
|
|
16
17
|
function sortRank(key) {
|
|
@@ -52,6 +53,8 @@ function labelForFigure(filename) {
|
|
|
52
53
|
return "Wasserstein (EMD) vs reference";
|
|
53
54
|
if (base === "EMD_stepwise")
|
|
54
55
|
return "Wasserstein (EMD) stepwise";
|
|
56
|
+
if (base === "plot_vol_all_meshes")
|
|
57
|
+
return "Volume fingerprint: all meshes";
|
|
55
58
|
if (base === "plot_vol_coarse_fine")
|
|
56
59
|
return "Volume fingerprint: coarse vs reference";
|
|
57
60
|
if (base.startsWith("plot_vol_"))
|
package/dist/tools/cfd.js
CHANGED
|
@@ -47,7 +47,7 @@ COMMON MISTAKES: omitting feature_columns (required); choosing a reference_mesh
|
|
|
47
47
|
const id = data.id;
|
|
48
48
|
if (id != null) {
|
|
49
49
|
const prep = data.prepare_job_id != null ? " (dataset prepare complete)" : "";
|
|
50
|
-
data.suggested_next_step = `
|
|
50
|
+
data.suggested_next_step = `Run barmesh_training_monitor(job_id="${id}") or barmesh_jobs(action=monitor, job_id="${id}")${prep}; on completion call barmesh_results(action=get, job_id="${id}").`;
|
|
51
51
|
}
|
|
52
52
|
return textResult(data);
|
|
53
53
|
});
|
|
@@ -80,7 +80,7 @@ COMMON MISTAKES: not providing h_column or n_cells_column; mixing QoIs with diff
|
|
|
80
80
|
const data = (await apiCall("POST", "/v1/cfd/richardson", body));
|
|
81
81
|
const id = data.id;
|
|
82
82
|
if (id != null)
|
|
83
|
-
data.suggested_next_step = `
|
|
83
|
+
data.suggested_next_step = `Run barmesh_training_monitor(job_id="${id}") or barmesh_jobs(action=monitor, job_id="${id}"); on completion call barmesh_results(action=get, job_id="${id}").`;
|
|
84
84
|
return textResult(data);
|
|
85
85
|
});
|
|
86
86
|
}
|
package/dist/tools/guide.js
CHANGED
|
@@ -2,7 +2,7 @@ import { registerAuditedTool } from "../audit.js";
|
|
|
2
2
|
import { apiCall, textResult } from "../shared.js";
|
|
3
3
|
const OFFLINE_GUIDE = `barmesh: CFD mesh-convergence on the Barivia API.
|
|
4
4
|
Two tracks: barmesh_mesh_convergence (SOM fingerprint distances) and barmesh_richardson (classical GCI).
|
|
5
|
-
Workflow: barmesh_prepare_mesh_data -> barmesh_datasets(upload) -> barmesh_mesh_convergence / barmesh_richardson -> barmesh_jobs
|
|
5
|
+
Workflow: barmesh_prepare_mesh_data -> barmesh_datasets(upload) -> barmesh_mesh_convergence / barmesh_richardson -> barmesh_training_monitor (or barmesh_jobs monitor/status) -> barmesh_results(get).
|
|
6
6
|
(API unreachable; this is the offline summary. Set BARIVIA_API_KEY / BARIVIA_API_URL.)`;
|
|
7
7
|
const OFFLINE_PREP = `barmesh mesh-data prep (offline summary; API unreachable):
|
|
8
8
|
Build ONE combined per-cell CSV across all meshes of the refinement study:
|
package/dist/tools/jobs.js
CHANGED
|
@@ -3,16 +3,41 @@ import { registerAuditedTool } from "../audit.js";
|
|
|
3
3
|
import { apiCall, textResult } from "../shared.js";
|
|
4
4
|
import { pollCfdFinalizeIfPresent, refreshJobAfterFinalize } from "../cfd_finalize.js";
|
|
5
5
|
import { formatJobStatusText } from "../job_status_format.js";
|
|
6
|
+
import { DEFAULT_BLOCK_UNTIL_SEC, DEFAULT_POLL_INTERVAL_SEC } from "../job_monitor.js";
|
|
7
|
+
import { runMonitor } from "./training_monitor.js";
|
|
6
8
|
export function registerJobsTool(server) {
|
|
7
|
-
registerAuditedTool(server, "barmesh_jobs", `Check job status or list jobs.
|
|
9
|
+
registerAuditedTool(server, "barmesh_jobs", `Check job status, block until terminal, or list jobs.
|
|
8
10
|
|
|
9
|
-
BEST FOR:
|
|
10
|
-
ASYNC PROTOCOL:
|
|
11
|
+
BEST FOR: action=monitor after submit (one call, throttled snapshots — preferred for agents). action=status for a single one-shot check.
|
|
12
|
+
ASYNC PROTOCOL: monitor blocks server-side until completed/failed or block_until timeout (default ${DEFAULT_BLOCK_UNTIL_SEC}s, poll every ${DEFAULT_POLL_INTERVAL_SEC}s). status is one-shot; poll every 10-20s manually if not using monitor. When status=completed, call barmesh_results(action=get, job_id=...).
|
|
11
13
|
ESCALATION: status=failed returns an error message and (when available) a failure_stage; read it before retrying.`, {
|
|
12
|
-
action: z
|
|
13
|
-
|
|
14
|
+
action: z
|
|
15
|
+
.enum(["status", "monitor", "list"])
|
|
16
|
+
.describe("status: one-shot check; monitor: block until terminal with snapshots; list: recent jobs"),
|
|
17
|
+
job_id: z.string().optional().describe("Job ID (required for status and monitor)"),
|
|
18
|
+
block_until_sec: z
|
|
19
|
+
.number()
|
|
20
|
+
.int()
|
|
21
|
+
.min(30)
|
|
22
|
+
.optional()
|
|
23
|
+
.describe(`action=monitor only: max wait seconds (default ${DEFAULT_BLOCK_UNTIL_SEC})`),
|
|
24
|
+
poll_interval_sec: z
|
|
25
|
+
.number()
|
|
26
|
+
.int()
|
|
27
|
+
.min(5)
|
|
28
|
+
.optional()
|
|
29
|
+
.describe(`action=monitor only: poll interval (default ${DEFAULT_POLL_INTERVAL_SEC})`),
|
|
30
|
+
wait_finalize: z
|
|
31
|
+
.boolean()
|
|
32
|
+
.optional()
|
|
33
|
+
.describe("action=monitor only: wait for cfd_finalize (default true)"),
|
|
14
34
|
}, async (args) => {
|
|
15
|
-
const { action, job_id } = args;
|
|
35
|
+
const { action, job_id, block_until_sec, poll_interval_sec, wait_finalize } = args;
|
|
36
|
+
if (action === "monitor") {
|
|
37
|
+
if (!job_id)
|
|
38
|
+
throw new Error("barmesh_jobs(monitor) requires job_id.");
|
|
39
|
+
return runMonitor({ job_id, block_until_sec, poll_interval_sec, wait_finalize });
|
|
40
|
+
}
|
|
16
41
|
if (action === "status") {
|
|
17
42
|
if (!job_id)
|
|
18
43
|
throw new Error("barmesh_jobs(status) requires job_id.");
|
package/dist/tools/results.js
CHANGED
|
@@ -2,8 +2,46 @@ import fs from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import { registerAuditedTool } from "../audit.js";
|
|
5
|
-
import { apiCall, apiRawCall, getWorkspaceRootAsync, sandboxPath, textResult, tryAttachImage, pollUntilComplete, POLL_STAGE_MAX_MS, } from "../shared.js";
|
|
6
|
-
const MESH_DEFAULT_FIGURES = ["combined", "overview_distances", "
|
|
5
|
+
import { apiCall, apiRawCall, getWorkspaceRootAsync, sandboxPath, textResult, tryAttachImage, resetInlineAttachBudget, pollUntilComplete, POLL_STAGE_MAX_MS, } from "../shared.js";
|
|
6
|
+
const MESH_DEFAULT_FIGURES = ["combined", "overview_distances", "plot_vol_all_meshes", "plot_vol_steps", "learning_curve"];
|
|
7
|
+
function formatMeshResultsSummary(jobId, data, summary) {
|
|
8
|
+
const label = data.label != null && data.label !== "" ? String(data.label) : null;
|
|
9
|
+
const header = label ? `Mesh convergence results for ${label} (job_id: ${jobId})` : `Mesh convergence results for job_id: ${jobId}`;
|
|
10
|
+
const grid = summary.grid ?? [];
|
|
11
|
+
const meshes = summary.meshes ?? [];
|
|
12
|
+
const stepwise = summary.stepwise ?? [];
|
|
13
|
+
const fmt = (v) => (typeof v === "number" && Number.isFinite(v) ? v.toFixed(4) : String(v ?? "N/A"));
|
|
14
|
+
const meshLines = meshes.map((m) => ` ${String(m.mesh ?? "?")} (n=${m.n_cells ?? "?"}): SKL→ref=${fmt(m.skl_to_ref)} EMD→ref=${fmt(m.emd_to_ref)}`);
|
|
15
|
+
const stepLines = stepwise.map((s) => ` ${String(s.pair ?? "?")}: EMD=${fmt(s.emd)} KL↓=${fmt(s.kl_coarsen)} KL↑=${fmt(s.kl_refine)}`);
|
|
16
|
+
const convergence = summary.convergence;
|
|
17
|
+
const emd = convergence?.emd;
|
|
18
|
+
const skl = convergence?.skl;
|
|
19
|
+
const reading = [];
|
|
20
|
+
if (emd) {
|
|
21
|
+
const plateau = emd.plateau === true ? "plateauing" : "not yet plateauing";
|
|
22
|
+
const mono = emd.monotonic_decrease === true ? "monotonic decrease toward reference" : "non-monotonic vs reference";
|
|
23
|
+
reading.push(`EMD vs ${String(summary.reference_mesh ?? "reference")}: ${mono}, ${plateau}.`);
|
|
24
|
+
}
|
|
25
|
+
if (skl) {
|
|
26
|
+
reading.push(`Symmetric KL vs reference: ${skl.plateau === true ? "plateauing" : "not yet plateauing"}.`);
|
|
27
|
+
}
|
|
28
|
+
const ord = summary.ordering_errors;
|
|
29
|
+
const conv = summary.convergence_errors;
|
|
30
|
+
const curveNote = (ord?.length ?? 0) > 0 || (conv?.length ?? 0) > 0
|
|
31
|
+
? `Training curves: ${ord?.length ?? 0} ordering + ${conv?.length ?? 0} convergence batch samples (≤1000 each in API/MCP responses).`
|
|
32
|
+
: "";
|
|
33
|
+
return [
|
|
34
|
+
header,
|
|
35
|
+
`Preset: ${String(summary.preset ?? "generic")} | Grid: ${grid.length >= 2 ? `${grid[0]}×${grid[1]}` : "N/A"} | Reference: ${String(summary.reference_mesh ?? "N/A")}`,
|
|
36
|
+
`QE: ${fmt(summary.quantization_error)} | TE: ${fmt(summary.topographic_error)} | EMD method: ${String(summary.emd_method ?? "exact")}`,
|
|
37
|
+
meshes.length > 0 ? `\nDistances vs reference:\n${meshLines.join("\n")}` : "",
|
|
38
|
+
stepLines.length > 0 ? `\nStepwise:\n${stepLines.join("\n")}` : "",
|
|
39
|
+
reading.length > 0 ? `\nConvergence reading:\n${reading.map((l) => ` ${l}`).join("\n")}` : "",
|
|
40
|
+
curveNote,
|
|
41
|
+
"\nAdvisory: SOM distances complement, not replace, numerical uncertainty analysis (use barmesh_richardson for classical GCI).",
|
|
42
|
+
"For every figure: barmesh_results_explorer(job_id) or barmesh_results(action=download). Use figures=\"none\" to skip inline images.",
|
|
43
|
+
].filter(Boolean).join("\n");
|
|
44
|
+
}
|
|
7
45
|
const TEXT_ARTIFACTS = [
|
|
8
46
|
"summary.json",
|
|
9
47
|
"distances.csv",
|
|
@@ -23,7 +61,7 @@ export function registerResultsTool(server) {
|
|
|
23
61
|
| download | Save figures and metrics to a local folder (headless / agent path). |
|
|
24
62
|
|
|
25
63
|
BEST FOR: After barmesh_jobs(action=status) shows completed (and finalize finished if defer_figures was used).
|
|
26
|
-
FIGURES: Default
|
|
64
|
+
FIGURES: Default bundle is combined.png, overview_distances.png (all KL/EMD panels), plot_vol_all_meshes.png, plot_vol_steps.png, and learning_curve.png — not redundant per-feature PNGs. action=get inlines the headline set; pass figures="all" for every artifact listed in summary.files, figures="none" for metrics only (recommended for agents).
|
|
27
65
|
PDFs ON DEMAND: vector PDFs are not produced by default. Use action=render (format=pdf) once, then action=image or action=download.
|
|
28
66
|
If GET /v1/results returns 404 shortly after status=completed, cfd_finalize may still be rendering — poll barmesh_jobs(status) until finalize_job_id completes.
|
|
29
67
|
NOT FOR: Submitting jobs.`, {
|
|
@@ -96,9 +134,18 @@ NOT FOR: Submitting jobs.`, {
|
|
|
96
134
|
const files = summary.files ?? [];
|
|
97
135
|
const isImage = (f) => /\.(png|svg|pdf)$/i.test(f);
|
|
98
136
|
let toDownload;
|
|
99
|
-
if (figures === "all"
|
|
137
|
+
if (figures === "all") {
|
|
100
138
|
toDownload = include_json ? files : files.filter(isImage);
|
|
101
139
|
}
|
|
140
|
+
else if (figures === undefined) {
|
|
141
|
+
toDownload = MESH_DEFAULT_FIGURES.flatMap((b) => files.filter((f) => f.replace(/\.[^.]+$/, "") === b || f.startsWith(`${b}.`)));
|
|
142
|
+
if (include_json) {
|
|
143
|
+
for (const t of TEXT_ARTIFACTS) {
|
|
144
|
+
if (files.includes(t) && !toDownload.includes(t))
|
|
145
|
+
toDownload.push(t);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
102
149
|
else if (Array.isArray(figures)) {
|
|
103
150
|
toDownload = figures.flatMap((key) => {
|
|
104
151
|
if (/\.(png|pdf|svg|csv|txt|json)$/i.test(key))
|
|
@@ -139,9 +186,10 @@ NOT FOR: Submitting jobs.`, {
|
|
|
139
186
|
? `Saved ${saved.length} file(s) to ${savedDir}: ${saved.join(", ")}`
|
|
140
187
|
: `No files saved. Check job_id and that the job (and cfd_finalize, if any) is completed.`);
|
|
141
188
|
}
|
|
142
|
-
|
|
189
|
+
resetInlineAttachBudget();
|
|
190
|
+
let data;
|
|
143
191
|
try {
|
|
144
|
-
|
|
192
|
+
data = (await apiCall("GET", `/v1/results/${job_id}`));
|
|
145
193
|
}
|
|
146
194
|
catch (err) {
|
|
147
195
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -154,7 +202,8 @@ NOT FOR: Submitting jobs.`, {
|
|
|
154
202
|
}
|
|
155
203
|
throw err;
|
|
156
204
|
}
|
|
157
|
-
const
|
|
205
|
+
const summary = (data.summary ?? {});
|
|
206
|
+
const content = [{ type: "text", text: formatMeshResultsSummary(job_id, data, summary) }];
|
|
158
207
|
if (figures === "none")
|
|
159
208
|
return { content };
|
|
160
209
|
const allFiles = summary.files ?? [];
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { registerAuditedTool } from "../audit.js";
|
|
3
|
+
import { textResult } from "../shared.js";
|
|
4
|
+
import { DEFAULT_BLOCK_UNTIL_SEC, DEFAULT_POLL_INTERVAL_SEC, formatMonitorText, monitorJob, } from "../job_monitor.js";
|
|
5
|
+
const MONITOR_DESCRIPTION = `Block until a barmesh_mesh_convergence or barmesh_richardson job reaches a terminal state, emitting compact progress snapshots along the way.
|
|
6
|
+
|
|
7
|
+
BEST FOR: After job submit — one call replaces manual barmesh_jobs(status) poll loops (10–20s × several minutes).
|
|
8
|
+
ASYNC PROTOCOL: Server-side poll every ~5s (configurable) until completed/failed/cancelled or block_until timeout. Snapshots include phase, epoch/total, QE/TE, ETA, and ordering_errors tail when live.
|
|
9
|
+
FINALIZE: When defer_figures is used, waits for cfd_finalize and adds a snapshot when figure render starts/completes.
|
|
10
|
+
NOT REQUIRED: barmesh_jobs(action=status) still works for one-shot checks; this tool is the agent-friendly blocking variant (barsom training_monitor parity for headless workflows).
|
|
11
|
+
ESCALATION: On timeout, re-run with a higher block_until_sec; on failed, read failure_stage in the final snapshot.`;
|
|
12
|
+
const monitorSchema = {
|
|
13
|
+
job_id: z.string().describe("Job ID from barmesh_mesh_convergence or barmesh_richardson"),
|
|
14
|
+
block_until_sec: z
|
|
15
|
+
.number()
|
|
16
|
+
.int()
|
|
17
|
+
.min(30)
|
|
18
|
+
.optional()
|
|
19
|
+
.describe(`Max seconds to wait (default ${DEFAULT_BLOCK_UNTIL_SEC}; mesh jobs often need 6–10 min)`),
|
|
20
|
+
poll_interval_sec: z
|
|
21
|
+
.number()
|
|
22
|
+
.int()
|
|
23
|
+
.min(5)
|
|
24
|
+
.optional()
|
|
25
|
+
.describe(`Seconds between status polls (default ${DEFAULT_POLL_INTERVAL_SEC}; do not go below 5)`),
|
|
26
|
+
wait_finalize: z
|
|
27
|
+
.boolean()
|
|
28
|
+
.optional()
|
|
29
|
+
.describe("When true (default), wait for cfd_finalize after compute completes before returning"),
|
|
30
|
+
};
|
|
31
|
+
async function runMonitor(args) {
|
|
32
|
+
const block_until_sec = args.block_until_sec ?? DEFAULT_BLOCK_UNTIL_SEC;
|
|
33
|
+
const poll_interval_sec = args.poll_interval_sec ?? DEFAULT_POLL_INTERVAL_SEC;
|
|
34
|
+
const result = await monitorJob(args.job_id, {
|
|
35
|
+
block_until_sec,
|
|
36
|
+
poll_interval_sec,
|
|
37
|
+
wait_finalize: args.wait_finalize,
|
|
38
|
+
});
|
|
39
|
+
const text = formatMonitorText(result, { block_until_sec, poll_interval_sec });
|
|
40
|
+
return textResult({
|
|
41
|
+
...result.data,
|
|
42
|
+
monitor: {
|
|
43
|
+
job_id: result.job_id,
|
|
44
|
+
terminal: result.terminal,
|
|
45
|
+
timed_out: result.timed_out,
|
|
46
|
+
snapshots: result.snapshots,
|
|
47
|
+
status_text: result.status_text,
|
|
48
|
+
suggested_next_step: result.suggested_next_step,
|
|
49
|
+
},
|
|
50
|
+
status_text: text,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
export function registerTrainingMonitorTool(server) {
|
|
54
|
+
registerAuditedTool(server, "barmesh_training_monitor", MONITOR_DESCRIPTION, monitorSchema, runMonitor);
|
|
55
|
+
}
|
|
56
|
+
export { runMonitor, MONITOR_DESCRIPTION, monitorSchema };
|
|
@@ -133,7 +133,7 @@ Boolean requesting whether a visible border and background is provided by the ho
|
|
|
133
133
|
- omitted: host decides border`)});m({method:c("ui/request-display-mode"),params:m({mode:Me.describe("The display mode being requested.")})});var Zf=m({mode:Me.describe("The display mode that was actually set. May differ from requested if not supported.")}).passthrough(),Cf=T([c("model"),c("app")]).describe("Tool visibility scope - who can access the tool.");m({resourceUri:l().optional(),visibility:I(Cf).optional().describe(`Who can access this tool. Default: ["model", "app"]
|
|
134
134
|
- "model": Tool visible to and callable by the agent
|
|
135
135
|
- "app": Tool callable by the app from this server only`),csp:ve().optional(),permissions:ve().optional()});m({mimeTypes:I(l()).optional().describe('Array of supported MIME types for UI resources.\nMust include `"text/html;profile=mcp-app"` for MCP Apps support.')});m({method:c("ui/download-file"),params:m({contents:I(T([qu,Fu])).describe("Resource contents to download — embedded (inline data) or linked (host fetches). Uses standard MCP resource types.")})});m({method:c("ui/message"),params:m({role:c("user").describe('Message role, currently only "user" is supported.'),content:I(ct).describe("Message content blocks (text, image, etc.).")})});m({method:c("ui/notifications/sandbox-resource-ready"),params:m({html:l().describe("HTML content to load into the inner iframe."),sandbox:l().optional().describe("Optional override for the inner iframe's sandbox attribute."),csp:Hi.optional().describe("CSP configuration from resource metadata."),permissions:Ji.optional().describe("Sandbox permissions from resource metadata.")})});var Af=m({method:c("ui/notifications/tool-result"),params:xn.describe("Standard MCP tool execution result.")}),Ku=m({toolInfo:m({id:nt.optional().describe("JSON-RPC id of the tools/call request."),tool:Fi.describe("Tool definition including name, inputSchema, etc.")}).optional().describe("Metadata of the tool call that instantiated this App."),theme:wf.optional().describe("Current color theme preference."),styles:Pf.optional().describe("Style configuration for theming the app."),displayMode:Me.optional().describe("How the UI is currently displayed."),availableDisplayModes:I(Me).optional().describe("Display modes the host supports."),containerDimensions:T([m({height:x().describe("Fixed container height in pixels.")}),m({maxHeight:T([x(),Ze()]).optional().describe("Maximum container height in pixels.")})]).and(T([m({width:x().describe("Fixed container width in pixels.")}),m({maxWidth:T([x(),Ze()]).optional().describe("Maximum container width in pixels.")})])).optional().describe(`Container dimensions. Represents the dimensions of the iframe or other
|
|
136
|
-
container holding the app. Specify either width or maxWidth, and either height or maxHeight.`),locale:l().optional().describe("User's language and region preference in BCP 47 format."),timeZone:l().optional().describe("User's timezone in IANA format."),userAgent:l().optional().describe("Host application identifier."),platform:T([c("web"),c("desktop"),c("mobile")]).optional().describe("Platform type for responsive design decisions."),deviceCapabilities:m({touch:Z().optional().describe("Whether the device supports touch input."),hover:Z().optional().describe("Whether the device supports hover interactions.")}).optional().describe("Device input capabilities."),safeAreaInsets:m({top:x().describe("Top safe area inset in pixels."),right:x().describe("Right safe area inset in pixels."),bottom:x().describe("Bottom safe area inset in pixels."),left:x().describe("Left safe area inset in pixels.")}).optional().describe("Mobile safe area boundaries in pixels.")}).passthrough(),Mf=m({method:c("ui/notifications/host-context-changed"),params:Ku.describe("Partial context update containing only changed fields.")});m({method:c("ui/update-model-context"),params:m({content:I(ct).optional().describe("Context content blocks (text, image, etc.)."),structuredContent:P(l(),C().describe("Structured content for machine-readable context data.")).optional().describe("Structured content for machine-readable context data.")})});m({method:c("ui/initialize"),params:m({appInfo:In.describe("App identification (name and version)."),appCapabilities:Rf.describe("Features and capabilities this app provides."),protocolVersion:l().describe("Protocol version this app supports.")})});var Lf=m({protocolVersion:l().describe('Negotiated protocol version string (e.g., "2025-11-21").'),hostInfo:In.describe("Host application identification and version."),hostCapabilities:Df.describe("Features and capabilities provided by the host."),hostContext:Ku.describe("Rich context about the host environment.")}).passthrough(),qf={target:"draft-2020-12"};async function vo(t,n){let r=t["~standard"];if(r.jsonSchema)return r.jsonSchema[n](qf);if(r.vendor==="zod"){let{z:o}=await il(()=>Promise.resolve().then(()=>Nm),void 0,import.meta.url);return o.toJSONSchema(t,{io:n})}throw Error(`Schema (vendor: ${r.vendor}) does not implement Standard JSON Schema (~standard.jsonSchema). Use a library that does (zod v4, ArkType, Valibot) or wrap your schema accordingly.`)}async function _o(t,n,r=""){let o=await t["~standard"].validate(n);if(o.issues){let e=o.issues.map(i=>{let a=i.path?.map(s=>typeof s=="object"?s.key:s).join(".");return a?`${a}: ${i.message}`:i.message}).join("; ");throw Error(r+e)}return o.value}class Vi extends bf{_appInfo;_capabilities;options;_hostCapabilities;_hostInfo;_hostContext;_registeredTools={};_initializedSent=!1;_assertInitialized(n){if(this._initializedSent)return;let r=`[ext-apps] App.${n}() called before connect() completed the ui/initialize handshake. Await app.connect() before calling this method, or move data loading to an ontoolresult handler.`;if(this.options?.strict)throw Error(r);console.warn(`${r}. This will throw in a future release.`)}eventSchemas={toolinput:Nf,toolinputpartial:Tf,toolresult:Af,toolcancelled:Of,hostcontextchanged:Mf};static ONE_SHOT_EVENTS=new Set(["toolinput","toolinputpartial","toolresult","toolcancelled"]);_everHadListener=new Set;_assertHandlerTiming(n){if(!Vi.ONE_SHOT_EVENTS.has(n)||this._everHadListener.has(n)||(this._everHadListener.add(n),!this._initializedSent))return;let r=`[ext-apps] "${String(n)}" handler registered after connect() completed the ui/initialize handshake. The host may have already sent this notification. Register handlers before calling app.connect().`;if(this.options?.strict)throw Error(r);console.warn(r)}setEventHandler(n,r){r&&this._assertHandlerTiming(n),super.setEventHandler(n,r)}addEventListener(n,r){this._assertHandlerTiming(n),super.addEventListener(n,r)}onEventDispatch(n,r){n==="hostcontextchanged"&&(this._hostContext={...this._hostContext,...r})}constructor(n,r={},o={autoResize:!0}){super(o),this._appInfo=n,this._capabilities=r,this.options=o,o.allowUnsafeEval||J({jitless:!0}),this.setRequestHandler(zn,e=>(console.log("Received ping:",e.params),{})),this.setEventHandler("hostcontextchanged",void 0)}registerCapabilities(n){if(this.transport)throw Error("Cannot register capabilities after transport is established");this._capabilities=_f(this._capabilities,n)}registerTool(n,r,o){if(this._registeredTools[n])throw Error(`Tool ${n} is already registered`);let e=this,i=()=>{e._initializedSent&&e._capabilities.tools?.listChanged&&e.sendToolListChanged()},a=r.inputSchema!==void 0,s={title:r.title,description:r.description,inputSchema:r.inputSchema,outputSchema:r.outputSchema,annotations:r.annotations,_meta:r._meta,enabled:!0,enable(){this.enabled=!0,i()},disable(){this.enabled=!1,i()},update(p){Object.assign(this,p),i()},remove(){e._registeredTools[n]===s&&(delete e._registeredTools[n],i())},handler:async(p,g)=>{if(!s.enabled)throw Error(`Tool ${n} is disabled`);let h;if(a){let d=s.inputSchema,b=d?await _o(d,p??{},`Invalid input for tool ${n}: `):p??{};h=await o(b,g)}else h=await o(g);return s.outputSchema&&!h.isError&&(h.structuredContent=await _o(s.outputSchema,h.structuredContent,`Invalid output for tool ${n}: `)),h}};return this._registeredTools[n]=s,!this._capabilities.tools&&!this.transport&&this.registerCapabilities({tools:{listChanged:!0}}),this.ensureToolHandlersInitialized(),i(),s}_toolHandlersInitialized=!1;ensureToolHandlersInitialized(){this._toolHandlersInitialized||(this._toolHandlersInitialized=!0,this.oncalltool=async(n,r)=>{let o=this._registeredTools[n.name];if(!o)throw Error(`Tool ${n.name} not found`);return o.handler(n.arguments,r)},this.onlisttools=async(n,r)=>({tools:await Promise.all(Object.entries(this._registeredTools).filter(([o,e])=>e.enabled).map(async([o,e])=>{let i={name:o,title:e.title,description:e.description,inputSchema:e.inputSchema?await vo(e.inputSchema,"input"):{type:"object",properties:{}}};return e.outputSchema&&(i.outputSchema=await vo(e.outputSchema,"output")),e.annotations&&(i.annotations=e.annotations),e._meta&&(i._meta=e._meta),i}))}))}async sendToolListChanged(n={}){this._assertInitialized("sendToolListChanged"),await this.notification({method:"notifications/tools/list_changed",params:n})}getHostCapabilities(){return this._hostCapabilities}getHostVersion(){return this._hostInfo}getHostContext(){return this._hostContext}get ontoolinput(){return this.getEventHandler("toolinput")}set ontoolinput(n){this.setEventHandler("toolinput",n)}get ontoolinputpartial(){return this.getEventHandler("toolinputpartial")}set ontoolinputpartial(n){this.setEventHandler("toolinputpartial",n)}get ontoolresult(){return this.getEventHandler("toolresult")}set ontoolresult(n){this.setEventHandler("toolresult",n)}get ontoolcancelled(){return this.getEventHandler("toolcancelled")}set ontoolcancelled(n){this.setEventHandler("toolcancelled",n)}get onhostcontextchanged(){return this.getEventHandler("hostcontextchanged")}set onhostcontextchanged(n){this.setEventHandler("hostcontextchanged",n)}_onteardown;get onteardown(){return this._onteardown}set onteardown(n){this.warnIfRequestHandlerReplaced("onteardown",this._onteardown,n),this._onteardown=n,this.replaceRequestHandler(Ef,(r,o)=>{if(!this._onteardown)throw Error("No onteardown handler set");return this._onteardown(r.params,o)})}_oncalltool;get oncalltool(){return this._oncalltool}set oncalltool(n){this.warnIfRequestHandlerReplaced("oncalltool",this._oncalltool,n),this._oncalltool=n,this.replaceRequestHandler(Ju,(r,o)=>{if(!this._oncalltool)throw Error("No oncalltool handler set");return this._oncalltool(r.params,o)})}_onlisttools;get onlisttools(){return this._onlisttools}set onlisttools(n){this.warnIfRequestHandlerReplaced("onlisttools",this._onlisttools,n),this._onlisttools=n,this.replaceRequestHandler(Hu,(r,o)=>{if(!this._onlisttools)throw Error("No onlisttools handler set");return this._onlisttools(r.params,o)})}assertCapabilityForMethod(n){switch(n){case"sampling/createMessage":if(!this._hostCapabilities?.sampling)throw Error(`Host does not support sampling (required for ${n})`);break}}assertRequestHandlerCapability(n){switch(n){case"tools/call":case"tools/list":if(!this._capabilities.tools)throw Error(`Client does not support tool capability (required for ${n})`);return;case"ping":case"ui/resource-teardown":return;default:throw Error(`No handler for method ${n} registered`)}}assertNotificationCapability(n){}assertTaskCapability(n){throw Error("Tasks are not supported in MCP Apps")}assertTaskHandlerCapability(n){throw Error("Task handlers are not supported in MCP Apps")}async callServerTool(n,r){if(this._assertInitialized("callServerTool"),typeof n=="string")throw Error(`callServerTool() expects an object as its first argument, but received a string ("${n}"). Did you mean: callServerTool({ name: "${n}", arguments: { ... } })?`);return await this.request({method:"tools/call",params:n},xn,{onprogress:()=>{},resetTimeoutOnProgress:!0,...r})}async readServerResource(n,r){return this._assertInitialized("readServerResource"),await this.request({method:"resources/read",params:n},Lu,r)}async listServerResources(n,r){return this._assertInitialized("listServerResources"),await this.request({method:"resources/list",params:n},Mu,r)}async createSamplingMessage(n,r){this._assertInitialized("createSamplingMessage");let o=n.tools?Wu:Bu;return await this.request({method:"sampling/createMessage",params:n},o,r)}sendMessage(n,r){return this._assertInitialized("sendMessage"),this.request({method:"ui/message",params:n},jf,r)}sendLog(n){return this.notification({method:"notifications/message",params:n})}updateModelContext(n,r){return this._assertInitialized("updateModelContext"),this.request({method:"ui/update-model-context",params:n},ji,r)}openLink(n,r){return this._assertInitialized("openLink"),this.request({method:"ui/open-link",params:n},zf,r)}sendOpenLink=this.openLink;downloadFile(n,r){return this._assertInitialized("downloadFile"),this.request({method:"ui/download-file",params:n},xf,r)}requestTeardown(n={}){return this.notification({method:"ui/notifications/request-teardown",params:n})}requestDisplayMode(n,r){return this._assertInitialized("requestDisplayMode"),this.request({method:"ui/request-display-mode",params:n},Zf,r)}sendSizeChanged(n){return this.notification({method:"ui/notifications/size-changed",params:n})}setupSizeChangedNotifications(){let n=!1,r=0,o=0,e=()=>{n||(n=!0,requestAnimationFrame(()=>{n=!1;let a=document.documentElement,s=a.style.height;a.style.height="max-content";let p=Math.ceil(a.getBoundingClientRect().height);a.style.height=s;let g=Math.ceil(window.innerWidth);(g!==r||p!==o)&&(r=g,o=p,this.sendSizeChanged({width:g,height:p}))}))};e();let i=new ResizeObserver(e);return i.observe(document.documentElement),i.observe(document.body),()=>i.disconnect()}async connect(n=new kf(window.parent,window.parent),r){if(this.transport)throw Error("App is already connected. Call close() before connecting again.");this._initializedSent=!1,await super.connect(n);try{let o=await this.request({method:"ui/initialize",params:{appCapabilities:this._capabilities,appInfo:this._appInfo,protocolVersion:$f}},Lf,r);if(o===void 0)throw Error(`Server sent invalid initialize result: ${o}`);this._hostCapabilities=o.hostCapabilities,this._hostInfo=o.hostInfo,this._hostContext=o.hostContext,await this.notification({method:"ui/notifications/initialized"}),this._initializedSent=!0,this.options?.autoResize&&this.setupSizeChangedNotifications()}catch(o){throw this.close(),o}}}let se=null,ce=null,re=null,Un=null;const bo=new Map,ft=document.getElementById("loading"),Ff=document.getElementById("app"),Gu=document.getElementById("title"),Qu=document.getElementById("subtitle"),Hf=document.getElementById("metrics"),Jf=document.getElementById("highlights"),Le=document.getElementById("figure-select"),Vf=document.getElementById("figure-title"),Bf=document.getElementById("figure-file"),Wf=document.getElementById("figure-caption"),ne=document.getElementById("main-image"),Kf=document.getElementById("figure-empty"),$o=document.getElementById("open-standalone"),Yu=document.getElementById("download-current"),yo={combined:0,overview_distances:1,KL_ref:10,KL_stepwise:11,EMD_ref:12,EMD_stepwise:13,learning_curve:14,plot_vol_coarse_fine:20};function ko(t){return t in yo?yo[t]:t.startsWith("plot_vol")?25:t.startsWith("plot_")?30:50}function Gf(t){return[...t].sort((n,r)=>ko(n.key)-ko(r.key)||n.label.localeCompare(r.label))}function Qf(t){if(t.find(r=>r.key==="combined"))return"combined";const n=t.find(r=>r.url||/\.(png|svg)$/i.test(r.filename));return n?n.key:t.find(r=>r.key==="KL_ref")?"KL_ref":t[0]?.key}function Yf(t){const n=t.replace(/\.(png|pdf|svg)$/i,"");return n==="combined"?"Component planes (overview)":n==="overview_distances"?"Distances & volume overview":n==="learning_curve"?"Learning curve (QE by epoch)":n==="KL_ref"?"KL divergence vs reference":n==="KL_stepwise"?"KL divergence stepwise":n==="EMD_ref"?"Wasserstein (EMD) vs reference":n==="EMD_stepwise"?"Wasserstein (EMD) stepwise":n==="plot_vol_coarse_fine"?"Volume: coarse vs reference":n.startsWith("plot_vol_")?"Volume: stepwise":n.startsWith("plot_")?`Component: ${n.replace(/^plot_/,"").replace(/_/g," ")}`:n.replace(/_/g," ")}function Xf(t,n){const r=new Map;for(const o of t){const e=o.match(/^(.*)\.(png|pdf|svg)$/i);if(!e)continue;const i=e[1],a=r.get(i)??new Set;a.add(e[2].toLowerCase()),r.set(i,a)}return Gf([...r.entries()].map(([o,e])=>{const i=[...e],a=e.has("png")?`${o}.png`:e.has("svg")?`${o}.svg`:`${o}.${i[0]}`,s=e.has("svg")?`${o}.svg`:e.has("pdf")?`${o}.pdf`:`${o}.png`,p=/\.(png|svg)$/i.test(a),g=i.filter(h=>h==="pdf"||h==="svg");return{key:o,label:Yf(a),filename:a,downloadFilename:s,formats:i,kind:o==="combined"?"summary":o.startsWith("plot_")&&!o.startsWith("plot_vol_")?"component":"diagnostic",caption:g.length>0?`High-quality ${g.join("/").toUpperCase()} available to download.`:void 0,url:p?`/api/results/${n}/image/${a}`:void 0,downloadUrl:`/api/results/${n}/image/${s}`}}))}function eh(t,n){const r=t.summary??{},o=(r.files??[]).filter(h=>/\.(png|pdf|svg)$/i.test(h)),e=r.grid??[],i=r.meshes??[],a=r.quantization_error!=null?Number(r.quantization_error).toFixed(4):"N/A",s=r.topographic_error!=null?Number(r.topographic_error).toFixed(4):"N/A",p=[{label:"Grid",value:e.length>=2?`${e[0]}×${e[1]}`:"N/A"},{label:"Preset",value:String(r.preset??"generic")},{label:"Reference",value:String(r.reference_mesh??"N/A")},{label:"Meshes",value:String(i.length||"N/A")},{label:"QE",value:a},{label:"TE",value:s}],g=Xf(o,n);return{type:"barmesh-results-explorer",jobId:n,title:"Mesh Convergence Results",subtitle:String(t.label??""),metrics:p,highlights:["SOM distances complement, not replace, numerical uncertainty analysis."],availableFigures:g,defaultFigureKey:Qf(g)}}function th(t){Hf.innerHTML=t.map(n=>`<div class="metric"><span class="metric-label">${n.label}</span><span class="metric-value">${n.value}</span></div>`).join("")}function nh(t){Jf.innerHTML=t.length>0?t.map(n=>`<div class="highlight">${n}</div>`).join(""):'<div class="highlight">No convergence reading available.</div>'}const rh={summary:"Overview",diagnostic:"Distances & diagnostics",component:"Component planes",artifact:"Other"};function Xu(t){const n=new Map;for(const o of t){const e=o.kind??"diagnostic",i=n.get(e)??[];i.push(o),n.set(e,i)}const r=["summary","diagnostic","component","artifact"];Le.innerHTML="";for(const o of r){const e=n.get(o);if(!e||e.length===0)continue;const i=document.createElement("optgroup");i.label=rh[o]??o;for(const a of e){const s=document.createElement("option");s.value=a.key,s.textContent=`${a.label} (${a.filename})`,a.key===ce?.key&&(s.selected=!0),i.appendChild(s)}Le.appendChild(i)}}Le.addEventListener("change",()=>{const t=se?.availableFigures.find(n=>n.key===Le.value);t&&el(t)});async function ih(t,n){const r=`${t}/${n}`,o=bo.get(r);if(o)return o;if(!re)return null;try{const i=(await re.callServerTool({name:"_barmesh_fetch_figure",arguments:{job_id:t,filename:n}})).content?.find(a=>a.type==="image");if(i){const a=`data:${i.mimeType};base64,${i.data}`;return bo.set(r,a),a}}catch{}return null}function oh(t){!re||!se||re.updateModelContext({content:[{type:"text",text:`User is viewing the "${t.label}" figure (${t.filename}) of mesh convergence job ${se.jobId}.`}]}).catch(()=>{})}function ah(t){return t.formats.length>1?` · formats: ${t.formats.join(", ")}`:""}function sh(t){const n=/\.(pdf|svg)$/i.test(t.downloadFilename);Yu.textContent=n?re?"Download PNG (preview)":`Download ${t.downloadFilename.split(".").pop()?.toUpperCase()} (high quality)`:"Download figure"}function ch(t){ne.style.display=t?"block":"none",Kf.style.display=t?"none":"block"}async function el(t){ce=t,Vf.textContent=t.label,Bf.textContent=`${t.filename}${ah(t)}`,Wf.textContent=t.caption??"",sh(t),oh(t),Le.value=t.key;let n=!1;if(t.url)ne.src=t.url,n=!0;else if(re&&se){if(t.key===se.defaultFigureKey&&Un)ne.src=Un,n=!0;else if(/\.(png|svg|jpe?g|webp)$/i.test(t.filename)){ne.removeAttribute("src");const r=await ih(se.jobId,t.filename);r&&ce?.key===t.key&&(ne.src=r,n=!0)}}else/\.(png|svg|jpe?g|webp)$/i.test(t.filename)&&ne.removeAttribute("src");ch(n),se&&Xu(se.availableFigures)}function wo(t){se=t,Gu.textContent=t.title,Qu.textContent=t.subtitle?t.subtitle:`Job ${t.jobId}`,th(t.metrics),nh(t.highlights),t.standaloneUrl&&($o.href=t.standaloneUrl,$o.style.display="inline-flex");const n=t.availableFigures.find(r=>r.key===t.defaultFigureKey)??t.availableFigures[0]??null;ce=n,Xu(t.availableFigures),n&&el(n),ft.style.display="none",Ff.style.display="block"}Yu.addEventListener("click",()=>{if(!ce)return;const t=document.createElement("a");if(ce.downloadUrl&&!re)t.href=ce.downloadUrl,t.download=ce.downloadFilename;else{const n=ne.src;if(!n)return;t.href=n,t.download=ce.filename}t.click()});ne.style.cursor="zoom-in";ne.addEventListener("click",()=>{if(!re||!ne.src)return;const t=re.getHostContext?.(),n=t?.availableDisplayModes;if(!Array.isArray(n)||!n.includes("fullscreen"))return;const r=t?.displayMode==="fullscreen"?"inline":"fullscreen";re.requestDisplayMode({mode:r}).then(o=>{ne.style.cursor=o.mode==="fullscreen"?"zoom-out":"zoom-in"}).catch(()=>{})});const tl=new URLSearchParams(window.location.search),uh=tl.get("mode")==="standalone",pt=tl.get("job_id");if(uh&&pt){const t="The local viz port is assigned per MCP session and changes when the proxy restarts. Re-run barmesh_results_explorer for a fresh URL, or set BARIVIA_VIZ_PORT for a persistent port.";(async()=>{for(let r=1;r<=3;r++)try{const o=await fetch(`/api/results/${pt}`);if(o.status===404){ft.textContent=`Results for job ${pt} were not found (HTTP 404). Compute may have finished but cfd_finalize is still rendering figures — poll barmesh_jobs(status) until finalize completes. ${t}`;return}if(!o.ok)throw new Error(`HTTP ${o.status}`);const e=await o.json();wo(eh(e,pt));return}catch(o){if(r<3){await new Promise(e=>setTimeout(e,1e3*r));continue}ft.textContent=`Could not load results from the local viz server (${o instanceof Error?o.message:"error"}). ${t}`}})()}else{const t=new Vi({name:"Mesh Convergence Results",version:"1.0.0"},{},{autoResize:!0});re=t,t.ontoolinput=n=>{const r=n?.arguments??{},o=r.job_id!=null?String(r.job_id):"";o&&(Gu.textContent="Mesh Convergence Results",Qu.textContent=`Loading job ${o}...`)},t.ontoolresult=n=>{const r=n.content?.find(e=>e.type==="image");r&&(Un=`data:${r.mimeType};base64,${r.data}`);const o=n.structuredContent;if(!o||typeof o!="object"){ft.textContent="The results explorer did not receive structured data.";return}wo(o)},t.connect()}</script>
|
|
136
|
+
container holding the app. Specify either width or maxWidth, and either height or maxHeight.`),locale:l().optional().describe("User's language and region preference in BCP 47 format."),timeZone:l().optional().describe("User's timezone in IANA format."),userAgent:l().optional().describe("Host application identifier."),platform:T([c("web"),c("desktop"),c("mobile")]).optional().describe("Platform type for responsive design decisions."),deviceCapabilities:m({touch:Z().optional().describe("Whether the device supports touch input."),hover:Z().optional().describe("Whether the device supports hover interactions.")}).optional().describe("Device input capabilities."),safeAreaInsets:m({top:x().describe("Top safe area inset in pixels."),right:x().describe("Right safe area inset in pixels."),bottom:x().describe("Bottom safe area inset in pixels."),left:x().describe("Left safe area inset in pixels.")}).optional().describe("Mobile safe area boundaries in pixels.")}).passthrough(),Mf=m({method:c("ui/notifications/host-context-changed"),params:Ku.describe("Partial context update containing only changed fields.")});m({method:c("ui/update-model-context"),params:m({content:I(ct).optional().describe("Context content blocks (text, image, etc.)."),structuredContent:P(l(),C().describe("Structured content for machine-readable context data.")).optional().describe("Structured content for machine-readable context data.")})});m({method:c("ui/initialize"),params:m({appInfo:In.describe("App identification (name and version)."),appCapabilities:Rf.describe("Features and capabilities this app provides."),protocolVersion:l().describe("Protocol version this app supports.")})});var Lf=m({protocolVersion:l().describe('Negotiated protocol version string (e.g., "2025-11-21").'),hostInfo:In.describe("Host application identification and version."),hostCapabilities:Df.describe("Features and capabilities provided by the host."),hostContext:Ku.describe("Rich context about the host environment.")}).passthrough(),qf={target:"draft-2020-12"};async function vo(t,n){let r=t["~standard"];if(r.jsonSchema)return r.jsonSchema[n](qf);if(r.vendor==="zod"){let{z:o}=await il(()=>Promise.resolve().then(()=>Nm),void 0,import.meta.url);return o.toJSONSchema(t,{io:n})}throw Error(`Schema (vendor: ${r.vendor}) does not implement Standard JSON Schema (~standard.jsonSchema). Use a library that does (zod v4, ArkType, Valibot) or wrap your schema accordingly.`)}async function _o(t,n,r=""){let o=await t["~standard"].validate(n);if(o.issues){let e=o.issues.map(i=>{let a=i.path?.map(s=>typeof s=="object"?s.key:s).join(".");return a?`${a}: ${i.message}`:i.message}).join("; ");throw Error(r+e)}return o.value}class Vi extends bf{_appInfo;_capabilities;options;_hostCapabilities;_hostInfo;_hostContext;_registeredTools={};_initializedSent=!1;_assertInitialized(n){if(this._initializedSent)return;let r=`[ext-apps] App.${n}() called before connect() completed the ui/initialize handshake. Await app.connect() before calling this method, or move data loading to an ontoolresult handler.`;if(this.options?.strict)throw Error(r);console.warn(`${r}. This will throw in a future release.`)}eventSchemas={toolinput:Nf,toolinputpartial:Tf,toolresult:Af,toolcancelled:Of,hostcontextchanged:Mf};static ONE_SHOT_EVENTS=new Set(["toolinput","toolinputpartial","toolresult","toolcancelled"]);_everHadListener=new Set;_assertHandlerTiming(n){if(!Vi.ONE_SHOT_EVENTS.has(n)||this._everHadListener.has(n)||(this._everHadListener.add(n),!this._initializedSent))return;let r=`[ext-apps] "${String(n)}" handler registered after connect() completed the ui/initialize handshake. The host may have already sent this notification. Register handlers before calling app.connect().`;if(this.options?.strict)throw Error(r);console.warn(r)}setEventHandler(n,r){r&&this._assertHandlerTiming(n),super.setEventHandler(n,r)}addEventListener(n,r){this._assertHandlerTiming(n),super.addEventListener(n,r)}onEventDispatch(n,r){n==="hostcontextchanged"&&(this._hostContext={...this._hostContext,...r})}constructor(n,r={},o={autoResize:!0}){super(o),this._appInfo=n,this._capabilities=r,this.options=o,o.allowUnsafeEval||J({jitless:!0}),this.setRequestHandler(zn,e=>(console.log("Received ping:",e.params),{})),this.setEventHandler("hostcontextchanged",void 0)}registerCapabilities(n){if(this.transport)throw Error("Cannot register capabilities after transport is established");this._capabilities=_f(this._capabilities,n)}registerTool(n,r,o){if(this._registeredTools[n])throw Error(`Tool ${n} is already registered`);let e=this,i=()=>{e._initializedSent&&e._capabilities.tools?.listChanged&&e.sendToolListChanged()},a=r.inputSchema!==void 0,s={title:r.title,description:r.description,inputSchema:r.inputSchema,outputSchema:r.outputSchema,annotations:r.annotations,_meta:r._meta,enabled:!0,enable(){this.enabled=!0,i()},disable(){this.enabled=!1,i()},update(p){Object.assign(this,p),i()},remove(){e._registeredTools[n]===s&&(delete e._registeredTools[n],i())},handler:async(p,g)=>{if(!s.enabled)throw Error(`Tool ${n} is disabled`);let h;if(a){let d=s.inputSchema,b=d?await _o(d,p??{},`Invalid input for tool ${n}: `):p??{};h=await o(b,g)}else h=await o(g);return s.outputSchema&&!h.isError&&(h.structuredContent=await _o(s.outputSchema,h.structuredContent,`Invalid output for tool ${n}: `)),h}};return this._registeredTools[n]=s,!this._capabilities.tools&&!this.transport&&this.registerCapabilities({tools:{listChanged:!0}}),this.ensureToolHandlersInitialized(),i(),s}_toolHandlersInitialized=!1;ensureToolHandlersInitialized(){this._toolHandlersInitialized||(this._toolHandlersInitialized=!0,this.oncalltool=async(n,r)=>{let o=this._registeredTools[n.name];if(!o)throw Error(`Tool ${n.name} not found`);return o.handler(n.arguments,r)},this.onlisttools=async(n,r)=>({tools:await Promise.all(Object.entries(this._registeredTools).filter(([o,e])=>e.enabled).map(async([o,e])=>{let i={name:o,title:e.title,description:e.description,inputSchema:e.inputSchema?await vo(e.inputSchema,"input"):{type:"object",properties:{}}};return e.outputSchema&&(i.outputSchema=await vo(e.outputSchema,"output")),e.annotations&&(i.annotations=e.annotations),e._meta&&(i._meta=e._meta),i}))}))}async sendToolListChanged(n={}){this._assertInitialized("sendToolListChanged"),await this.notification({method:"notifications/tools/list_changed",params:n})}getHostCapabilities(){return this._hostCapabilities}getHostVersion(){return this._hostInfo}getHostContext(){return this._hostContext}get ontoolinput(){return this.getEventHandler("toolinput")}set ontoolinput(n){this.setEventHandler("toolinput",n)}get ontoolinputpartial(){return this.getEventHandler("toolinputpartial")}set ontoolinputpartial(n){this.setEventHandler("toolinputpartial",n)}get ontoolresult(){return this.getEventHandler("toolresult")}set ontoolresult(n){this.setEventHandler("toolresult",n)}get ontoolcancelled(){return this.getEventHandler("toolcancelled")}set ontoolcancelled(n){this.setEventHandler("toolcancelled",n)}get onhostcontextchanged(){return this.getEventHandler("hostcontextchanged")}set onhostcontextchanged(n){this.setEventHandler("hostcontextchanged",n)}_onteardown;get onteardown(){return this._onteardown}set onteardown(n){this.warnIfRequestHandlerReplaced("onteardown",this._onteardown,n),this._onteardown=n,this.replaceRequestHandler(Ef,(r,o)=>{if(!this._onteardown)throw Error("No onteardown handler set");return this._onteardown(r.params,o)})}_oncalltool;get oncalltool(){return this._oncalltool}set oncalltool(n){this.warnIfRequestHandlerReplaced("oncalltool",this._oncalltool,n),this._oncalltool=n,this.replaceRequestHandler(Ju,(r,o)=>{if(!this._oncalltool)throw Error("No oncalltool handler set");return this._oncalltool(r.params,o)})}_onlisttools;get onlisttools(){return this._onlisttools}set onlisttools(n){this.warnIfRequestHandlerReplaced("onlisttools",this._onlisttools,n),this._onlisttools=n,this.replaceRequestHandler(Hu,(r,o)=>{if(!this._onlisttools)throw Error("No onlisttools handler set");return this._onlisttools(r.params,o)})}assertCapabilityForMethod(n){switch(n){case"sampling/createMessage":if(!this._hostCapabilities?.sampling)throw Error(`Host does not support sampling (required for ${n})`);break}}assertRequestHandlerCapability(n){switch(n){case"tools/call":case"tools/list":if(!this._capabilities.tools)throw Error(`Client does not support tool capability (required for ${n})`);return;case"ping":case"ui/resource-teardown":return;default:throw Error(`No handler for method ${n} registered`)}}assertNotificationCapability(n){}assertTaskCapability(n){throw Error("Tasks are not supported in MCP Apps")}assertTaskHandlerCapability(n){throw Error("Task handlers are not supported in MCP Apps")}async callServerTool(n,r){if(this._assertInitialized("callServerTool"),typeof n=="string")throw Error(`callServerTool() expects an object as its first argument, but received a string ("${n}"). Did you mean: callServerTool({ name: "${n}", arguments: { ... } })?`);return await this.request({method:"tools/call",params:n},xn,{onprogress:()=>{},resetTimeoutOnProgress:!0,...r})}async readServerResource(n,r){return this._assertInitialized("readServerResource"),await this.request({method:"resources/read",params:n},Lu,r)}async listServerResources(n,r){return this._assertInitialized("listServerResources"),await this.request({method:"resources/list",params:n},Mu,r)}async createSamplingMessage(n,r){this._assertInitialized("createSamplingMessage");let o=n.tools?Wu:Bu;return await this.request({method:"sampling/createMessage",params:n},o,r)}sendMessage(n,r){return this._assertInitialized("sendMessage"),this.request({method:"ui/message",params:n},jf,r)}sendLog(n){return this.notification({method:"notifications/message",params:n})}updateModelContext(n,r){return this._assertInitialized("updateModelContext"),this.request({method:"ui/update-model-context",params:n},ji,r)}openLink(n,r){return this._assertInitialized("openLink"),this.request({method:"ui/open-link",params:n},zf,r)}sendOpenLink=this.openLink;downloadFile(n,r){return this._assertInitialized("downloadFile"),this.request({method:"ui/download-file",params:n},xf,r)}requestTeardown(n={}){return this.notification({method:"ui/notifications/request-teardown",params:n})}requestDisplayMode(n,r){return this._assertInitialized("requestDisplayMode"),this.request({method:"ui/request-display-mode",params:n},Zf,r)}sendSizeChanged(n){return this.notification({method:"ui/notifications/size-changed",params:n})}setupSizeChangedNotifications(){let n=!1,r=0,o=0,e=()=>{n||(n=!0,requestAnimationFrame(()=>{n=!1;let a=document.documentElement,s=a.style.height;a.style.height="max-content";let p=Math.ceil(a.getBoundingClientRect().height);a.style.height=s;let g=Math.ceil(window.innerWidth);(g!==r||p!==o)&&(r=g,o=p,this.sendSizeChanged({width:g,height:p}))}))};e();let i=new ResizeObserver(e);return i.observe(document.documentElement),i.observe(document.body),()=>i.disconnect()}async connect(n=new kf(window.parent,window.parent),r){if(this.transport)throw Error("App is already connected. Call close() before connecting again.");this._initializedSent=!1,await super.connect(n);try{let o=await this.request({method:"ui/initialize",params:{appCapabilities:this._capabilities,appInfo:this._appInfo,protocolVersion:$f}},Lf,r);if(o===void 0)throw Error(`Server sent invalid initialize result: ${o}`);this._hostCapabilities=o.hostCapabilities,this._hostInfo=o.hostInfo,this._hostContext=o.hostContext,await this.notification({method:"ui/notifications/initialized"}),this._initializedSent=!0,this.options?.autoResize&&this.setupSizeChangedNotifications()}catch(o){throw this.close(),o}}}let se=null,ce=null,re=null,Un=null;const bo=new Map,ft=document.getElementById("loading"),Ff=document.getElementById("app"),Gu=document.getElementById("title"),Qu=document.getElementById("subtitle"),Hf=document.getElementById("metrics"),Jf=document.getElementById("highlights"),Le=document.getElementById("figure-select"),Vf=document.getElementById("figure-title"),Bf=document.getElementById("figure-file"),Wf=document.getElementById("figure-caption"),ne=document.getElementById("main-image"),Kf=document.getElementById("figure-empty"),$o=document.getElementById("open-standalone"),Yu=document.getElementById("download-current"),yo={combined:0,overview_distances:1,KL_ref:10,KL_stepwise:11,EMD_ref:12,EMD_stepwise:13,learning_curve:14,plot_vol_all_meshes:19,plot_vol_coarse_fine:20};function ko(t){return t in yo?yo[t]:t.startsWith("plot_vol")?25:t.startsWith("plot_")?30:50}function Gf(t){return[...t].sort((n,r)=>ko(n.key)-ko(r.key)||n.label.localeCompare(r.label))}function Qf(t){if(t.find(r=>r.key==="combined"))return"combined";const n=t.find(r=>r.url||/\.(png|svg)$/i.test(r.filename));return n?n.key:t.find(r=>r.key==="KL_ref")?"KL_ref":t[0]?.key}function Yf(t){const n=t.replace(/\.(png|pdf|svg)$/i,"");return n==="combined"?"Component planes (overview)":n==="overview_distances"?"Distances overview (KL + EMD)":n==="learning_curve"?"Learning curve (QE by epoch)":n==="KL_ref"?"KL divergence vs reference":n==="KL_stepwise"?"KL divergence stepwise":n==="EMD_ref"?"Wasserstein (EMD) vs reference":n==="EMD_stepwise"?"Wasserstein (EMD) stepwise":n==="plot_vol_all_meshes"?"Volume: all meshes":n==="plot_vol_coarse_fine"?"Volume: coarse vs reference":n.startsWith("plot_vol_")?"Volume: stepwise":n.startsWith("plot_")?`Component: ${n.replace(/^plot_/,"").replace(/_/g," ")}`:n.replace(/_/g," ")}function Xf(t,n){const r=new Map;for(const o of t){const e=o.match(/^(.*)\.(png|pdf|svg)$/i);if(!e)continue;const i=e[1],a=r.get(i)??new Set;a.add(e[2].toLowerCase()),r.set(i,a)}return Gf([...r.entries()].map(([o,e])=>{const i=[...e],a=e.has("png")?`${o}.png`:e.has("svg")?`${o}.svg`:`${o}.${i[0]}`,s=e.has("svg")?`${o}.svg`:e.has("pdf")?`${o}.pdf`:`${o}.png`,p=/\.(png|svg)$/i.test(a),g=i.filter(h=>h==="pdf"||h==="svg");return{key:o,label:Yf(a),filename:a,downloadFilename:s,formats:i,kind:o==="combined"?"summary":o.startsWith("plot_")&&!o.startsWith("plot_vol_")?"component":"diagnostic",caption:g.length>0?`High-quality ${g.join("/").toUpperCase()} available to download.`:void 0,url:p?`/api/results/${n}/image/${a}`:void 0,downloadUrl:`/api/results/${n}/image/${s}`}}))}function eh(t,n){const r=t.summary??{},o=(r.files??[]).filter(h=>/\.(png|pdf|svg)$/i.test(h)),e=r.grid??[],i=r.meshes??[],a=r.quantization_error!=null?Number(r.quantization_error).toFixed(4):"N/A",s=r.topographic_error!=null?Number(r.topographic_error).toFixed(4):"N/A",p=[{label:"Grid",value:e.length>=2?`${e[0]}×${e[1]}`:"N/A"},{label:"Preset",value:String(r.preset??"generic")},{label:"Reference",value:String(r.reference_mesh??"N/A")},{label:"Meshes",value:String(i.length||"N/A")},{label:"QE",value:a},{label:"TE",value:s}],g=Xf(o,n);return{type:"barmesh-results-explorer",jobId:n,title:"Mesh Convergence Results",subtitle:String(t.label??""),metrics:p,highlights:["SOM distances complement, not replace, numerical uncertainty analysis."],availableFigures:g,defaultFigureKey:Qf(g)}}function th(t){Hf.innerHTML=t.map(n=>`<div class="metric"><span class="metric-label">${n.label}</span><span class="metric-value">${n.value}</span></div>`).join("")}function nh(t){Jf.innerHTML=t.length>0?t.map(n=>`<div class="highlight">${n}</div>`).join(""):'<div class="highlight">No convergence reading available.</div>'}const rh={summary:"Overview",diagnostic:"Distances & diagnostics",component:"Component planes",artifact:"Other"};function Xu(t){const n=new Map;for(const o of t){const e=o.kind??"diagnostic",i=n.get(e)??[];i.push(o),n.set(e,i)}const r=["summary","diagnostic","component","artifact"];Le.innerHTML="";for(const o of r){const e=n.get(o);if(!e||e.length===0)continue;const i=document.createElement("optgroup");i.label=rh[o]??o;for(const a of e){const s=document.createElement("option");s.value=a.key,s.textContent=`${a.label} (${a.filename})`,a.key===ce?.key&&(s.selected=!0),i.appendChild(s)}Le.appendChild(i)}}Le.addEventListener("change",()=>{const t=se?.availableFigures.find(n=>n.key===Le.value);t&&el(t)});async function ih(t,n){const r=`${t}/${n}`,o=bo.get(r);if(o)return o;if(!re)return null;try{const i=(await re.callServerTool({name:"_barmesh_fetch_figure",arguments:{job_id:t,filename:n}})).content?.find(a=>a.type==="image");if(i){const a=`data:${i.mimeType};base64,${i.data}`;return bo.set(r,a),a}}catch{}return null}function oh(t){!re||!se||re.updateModelContext({content:[{type:"text",text:`User is viewing the "${t.label}" figure (${t.filename}) of mesh convergence job ${se.jobId}.`}]}).catch(()=>{})}function ah(t){return t.formats.length>1?` · formats: ${t.formats.join(", ")}`:""}function sh(t){const n=/\.(pdf|svg)$/i.test(t.downloadFilename);Yu.textContent=n?re?"Download PNG (preview)":`Download ${t.downloadFilename.split(".").pop()?.toUpperCase()} (high quality)`:"Download figure"}function ch(t){ne.style.display=t?"block":"none",Kf.style.display=t?"none":"block"}async function el(t){ce=t,Vf.textContent=t.label,Bf.textContent=`${t.filename}${ah(t)}`,Wf.textContent=t.caption??"",sh(t),oh(t),Le.value=t.key;let n=!1;if(t.url)ne.src=t.url,n=!0;else if(re&&se){if(t.key===se.defaultFigureKey&&Un)ne.src=Un,n=!0;else if(/\.(png|svg|jpe?g|webp)$/i.test(t.filename)){ne.removeAttribute("src");const r=await ih(se.jobId,t.filename);r&&ce?.key===t.key&&(ne.src=r,n=!0)}}else/\.(png|svg|jpe?g|webp)$/i.test(t.filename)&&ne.removeAttribute("src");ch(n),se&&Xu(se.availableFigures)}function wo(t){se=t,Gu.textContent=t.title,Qu.textContent=t.subtitle?t.subtitle:`Job ${t.jobId}`,th(t.metrics),nh(t.highlights),t.standaloneUrl&&($o.href=t.standaloneUrl,$o.style.display="inline-flex");const n=t.availableFigures.find(r=>r.key===t.defaultFigureKey)??t.availableFigures[0]??null;ce=n,Xu(t.availableFigures),n&&el(n),ft.style.display="none",Ff.style.display="block"}Yu.addEventListener("click",()=>{if(!ce)return;const t=document.createElement("a");if(ce.downloadUrl&&!re)t.href=ce.downloadUrl,t.download=ce.downloadFilename;else{const n=ne.src;if(!n)return;t.href=n,t.download=ce.filename}t.click()});ne.style.cursor="zoom-in";ne.addEventListener("click",()=>{if(!re||!ne.src)return;const t=re.getHostContext?.(),n=t?.availableDisplayModes;if(!Array.isArray(n)||!n.includes("fullscreen"))return;const r=t?.displayMode==="fullscreen"?"inline":"fullscreen";re.requestDisplayMode({mode:r}).then(o=>{ne.style.cursor=o.mode==="fullscreen"?"zoom-out":"zoom-in"}).catch(()=>{})});const tl=new URLSearchParams(window.location.search),uh=tl.get("mode")==="standalone",pt=tl.get("job_id");if(uh&&pt){const t="The local viz port is assigned per MCP session and changes when the proxy restarts. Re-run barmesh_results_explorer for a fresh URL, or set BARIVIA_VIZ_PORT for a persistent port.";(async()=>{for(let r=1;r<=3;r++)try{const o=await fetch(`/api/results/${pt}`);if(o.status===404){ft.textContent=`Results for job ${pt} were not found (HTTP 404). Compute may have finished but cfd_finalize is still rendering figures — poll barmesh_jobs(status) until finalize completes. ${t}`;return}if(!o.ok)throw new Error(`HTTP ${o.status}`);const e=await o.json();wo(eh(e,pt));return}catch(o){if(r<3){await new Promise(e=>setTimeout(e,1e3*r));continue}ft.textContent=`Could not load results from the local viz server (${o instanceof Error?o.message:"error"}). ${t}`}})()}else{const t=new Vi({name:"Mesh Convergence Results",version:"1.0.0"},{},{autoResize:!0});re=t,t.ontoolinput=n=>{const r=n?.arguments??{},o=r.job_id!=null?String(r.job_id):"";o&&(Gu.textContent="Mesh Convergence Results",Qu.textContent=`Loading job ${o}...`)},t.ontoolresult=n=>{const r=n.content?.find(e=>e.type==="image");r&&(Un=`data:${r.mimeType};base64,${r.data}`);const o=n.structuredContent;if(!o||typeof o!="object"){ft.textContent="The results explorer did not receive structured data.";return}wo(o)},t.connect()}</script>
|
|
137
137
|
</head>
|
|
138
138
|
<body>
|
|
139
139
|
<div id="loading">Loading mesh convergence results...</div>
|