@barivia/barmesh-mcp 0.8.8 → 0.8.10
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 +7 -4
- package/dist/audit.js +9 -3
- package/dist/index.js +1 -1
- package/dist/job_monitor.js +19 -1
- package/dist/shared.js +46 -9
- package/dist/tools/barmesh_results_explorer.js +1 -0
- package/dist/tools/cfd.js +34 -36
- package/dist/tools/feedback.js +6 -1
- package/dist/tools/guide.js +5 -20
- package/dist/tools/jobs.js +3 -2
- package/dist/tools/training_monitor.js +21 -3
- package/dist/ui-delivery.js +41 -8
- package/dist/views/src/views/barmesh-results-explorer/index.html +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -44,14 +44,16 @@ These complement, and do not replace, conventional numerical uncertainty analysi
|
|
|
44
44
|
```
|
|
45
45
|
|
|
46
46
|
Access to the analysis tools requires the **`cfd`** (or **`all_plus_cfd`**) entitlement on your
|
|
47
|
-
API key; otherwise the analysis calls return HTTP 403.
|
|
47
|
+
API key; otherwise the analysis calls return HTTP 403. **`barmesh_guide_workflow`** /
|
|
48
|
+
**`barmesh_prepare_mesh_data`** also redact their API bodies to an upgrade stub when the key
|
|
49
|
+
is not CFD-entitled (method/prep details are not returned). Contact Barivia to enable it.
|
|
48
50
|
|
|
49
51
|
## Tools
|
|
50
52
|
|
|
51
53
|
| Tool | Purpose |
|
|
52
54
|
|------|---------|
|
|
53
|
-
| `barmesh_guide_workflow` | Workflow + tool map (
|
|
54
|
-
| `barmesh_prepare_mesh_data` |
|
|
55
|
+
| `barmesh_guide_workflow` | Workflow + tool map from the API (full text when CFD-entitled). Call first. |
|
|
56
|
+
| `barmesh_prepare_mesh_data` | Mesh CSV recipe from the API (full text when CFD-entitled). |
|
|
55
57
|
| `barmesh_datasets` | Upload / preview / list / get / subset / delete the mesh CSV. |
|
|
56
58
|
| `barmesh_mesh_convergence` | SOM fingerprint distances (async job). |
|
|
57
59
|
| `barmesh_richardson` | Richardson/GCI on scalar QoIs (async job). |
|
|
@@ -118,4 +120,5 @@ Parquet staging is supported by the API but not yet exposed as an MCP upload for
|
|
|
118
120
|
| `BARIVIA_WORKSPACE_ROOT` | workspace/cwd | Root for resolving relative `file_path` uploads. |
|
|
119
121
|
| `BARIVIA_ENFORCE_WORKSPACE_SANDBOX` | `1` | Restrict uploads to the workspace; set `0` to allow absolute paths. |
|
|
120
122
|
| `BARIVIA_VIZ_PORT` | ephemeral | Fixed localhost port for standalone App pages (otherwise OS-assigned per session). |
|
|
121
|
-
| `BARIVIA_UI_DELIVERY` | `auto` | Client-capability override for App tools: `auto` \| `apps` \| `localhost` \| `inline`. Responses always lead with a standalone localhost URL. |
|
|
123
|
+
| `BARIVIA_UI_DELIVERY` | `auto` | Client-capability override for App tools: `auto` \| `apps` \| `localhost` \| `inline` \| `text_only`. Responses always lead with a standalone localhost URL. |
|
|
124
|
+
| `BARIVIA_UI_PREFER_LOCALHOST` | (off) | Set `1` to force `localhost` under `auto` even when the host advertises MCP Apps. |
|
package/dist/audit.js
CHANGED
|
@@ -88,11 +88,17 @@ export async function runMcpToolAudit(tool, action, args, handler) {
|
|
|
88
88
|
throw err;
|
|
89
89
|
}
|
|
90
90
|
}
|
|
91
|
-
export function registerAuditedTool(server, name, description, schema, handler) {
|
|
92
|
-
|
|
91
|
+
export function registerAuditedTool(server, name, description, schema, handler, annotations) {
|
|
92
|
+
const cb = (async (args) => {
|
|
93
93
|
const rec = args;
|
|
94
94
|
const action = typeof rec.action === "string" && rec.action.length > 0 ? rec.action : "default";
|
|
95
95
|
// One trace per tool invocation (every apiCall inside shares one trace_id).
|
|
96
96
|
return runWithTrace(() => runMcpToolAudit(name, action, rec, () => handler(args)));
|
|
97
|
-
})
|
|
97
|
+
});
|
|
98
|
+
if (annotations) {
|
|
99
|
+
server.tool(name, description, schema, annotations, cb);
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
server.tool(name, description, schema, cb);
|
|
103
|
+
}
|
|
98
104
|
}
|
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
|
|
2
|
+
import{McpServer as e}from"@modelcontextprotocol/sdk/server/mcp.js";import{StdioServerTransport as o}from"@modelcontextprotocol/sdk/server/stdio.js";import{getUiCapability as r,registerAppResource as s,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 m,apiRawCall as l,loadViewHtml as c,setVizPort as p,setClientSupportsMcpApps as h}from"./shared.js";import{registerGuideTool as d}from"./tools/guide.js";import{registerDatasetsTool as _}from"./tools/datasets.js";import{registerCfdTools as b}from"./tools/cfd.js";import{registerJobsTool as u}from"./tools/jobs.js";import{registerResultsTool as f}from"./tools/results.js";import{registerResultsExplorerTool as v,RESULTS_EXPLORER_URI as y}from"./tools/barmesh_results_explorer.js";import{registerTrainingMonitorTool as I,TRAINING_MONITOR_URI as w}from"./tools/training_monitor.js";import{registerFeedbackTool as g}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\n## Bootstrap (do this first)\n\nCall `barmesh_guide_workflow` (plan-scoped) and `barmesh_prepare_mesh_data` before upload/submit. Method details and mesh-prep recipe come from the API when your key has the CFD entitlement.\n\n## Tracks (names only)\n\n- `barmesh_mesh_convergence` — field-level mesh comparison (SOM fingerprints)\n- `barmesh_richardson` — classical Richardson/GCI on scalar QoIs\n\n## Workflow (short)\n\n1. `barmesh_guide_workflow`\n2. `barmesh_prepare_mesh_data`\n3. `barmesh_datasets(upload)` → preview\n4. Submit mesh_convergence and/or richardson → job_id\n5. `barmesh_training_monitor` or `barmesh_jobs(monitor/status)`\n6. `barmesh_results(get)` → `barmesh_results_explorer`\n\nInventory: `barmesh_jobs(inventory)` or list datasets+jobs. Set description/tags on upload/submit.\n\n## UI / outages\n\nTool results lead with a localhost viz URL and `ui_delivery` — follow `hint_for_agent`. Env: `BARIVIA_UI_DELIVERY` (auto|apps|localhost|inline|text_only), `BARIVIA_UI_PREFER_LOCALHOST=1` (opt-in localhost even when Apps advertised), `BARIVIA_VIZ_PORT`.\nOn `api_unreachable`: one calm line; honor `retry_after_sec`; use `barmesh_api_health`; keep reviewing last good figures. `barmesh_send_feedback` queues deferred drafts when the API is down."});s(i,y,y,{mimeType:t},async()=>{const e=await c("barmesh-results-explorer");return{contents:[{uri:y,mimeType:t,text:e??"<html><body>Results Explorer view not built yet. Run: npm run build:views</body></html>"}]}}),s(i,w,w,{mimeType:t},async()=>{const e=await c("barmesh-training-monitor");return{contents:[{uri:w,mimeType:t,text:e??"<html><body>Training Monitor view not built yet. Run: npm run build:views</body></html>"}]}}),d(i),v(i),_(i),b(i),u(i),I(i),f(i),g(i);try{const e=await n(m,l,c);p(e)}catch(e){process.env.BARIVIA_VIZ_PORT&&console.error("barmesh viz server failed to start:",e)}const A=i.server;A.oninitialized=()=>{const e=A.getClientCapabilities(),o=r(e);h(!!o?.mimeTypes?.includes(t))};const j=new o;await i.connect(j),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)});
|
package/dist/job_monitor.js
CHANGED
|
@@ -11,6 +11,9 @@ export const DEFAULT_BLOCK_UNTIL_SEC = 900;
|
|
|
11
11
|
export const DEFAULT_POLL_INTERVAL_SEC = 5;
|
|
12
12
|
export const MIN_POLL_INTERVAL_SEC = 5;
|
|
13
13
|
export const HEARTBEAT_POLLS = 1;
|
|
14
|
+
/** After this many unchanged polls, gently raise interval (never below MIN_POLL). */
|
|
15
|
+
export const IDLE_BACKOFF_AFTER_POLLS = 8;
|
|
16
|
+
export const MAX_POLL_INTERVAL_SEC = 15;
|
|
14
17
|
function sleep(ms) {
|
|
15
18
|
return new Promise((r) => setTimeout(r, ms));
|
|
16
19
|
}
|
|
@@ -219,6 +222,7 @@ export async function monitorJob(job_id, options = {}) {
|
|
|
219
222
|
return buildPostHocReview(job_id, data, suggested);
|
|
220
223
|
}
|
|
221
224
|
let heartbeat = 0;
|
|
225
|
+
let idleStreak = 0;
|
|
222
226
|
while (Date.now() - start < blockMs) {
|
|
223
227
|
if (snapshots.length === 0) {
|
|
224
228
|
/* first fetch already done above */
|
|
@@ -230,6 +234,15 @@ export async function monitorJob(job_id, options = {}) {
|
|
|
230
234
|
const snap = snapshotFromJob(data, elapsedSec);
|
|
231
235
|
heartbeat += 1;
|
|
232
236
|
const heartbeatDue = heartbeat >= HEARTBEAT_POLLS;
|
|
237
|
+
const unchanged = lastSnap != null &&
|
|
238
|
+
lastSnap.status === snap.status &&
|
|
239
|
+
lastSnap.progress_pct === snap.progress_pct &&
|
|
240
|
+
lastSnap.phase === snap.phase &&
|
|
241
|
+
lastSnap.epoch === snap.epoch;
|
|
242
|
+
if (unchanged)
|
|
243
|
+
idleStreak += 1;
|
|
244
|
+
else
|
|
245
|
+
idleStreak = 0;
|
|
233
246
|
if (shouldRecordSnapshot(lastSnap, snap) || heartbeatDue) {
|
|
234
247
|
snapshots.push(snap);
|
|
235
248
|
lastSnap = snap;
|
|
@@ -239,7 +252,12 @@ export async function monitorJob(job_id, options = {}) {
|
|
|
239
252
|
if (status === "completed" || status === "failed" || status === "cancelled") {
|
|
240
253
|
break;
|
|
241
254
|
}
|
|
242
|
-
|
|
255
|
+
let waitMs = pollMs;
|
|
256
|
+
if (idleStreak >= IDLE_BACKOFF_AFTER_POLLS) {
|
|
257
|
+
const steps = Math.floor((idleStreak - IDLE_BACKOFF_AFTER_POLLS) / 2);
|
|
258
|
+
waitMs = Math.min(pollMs + steps * 5000, MAX_POLL_INTERVAL_SEC * 1000);
|
|
259
|
+
}
|
|
260
|
+
await sleep(Math.max(MIN_POLL_INTERVAL_SEC * 1000, waitMs));
|
|
243
261
|
}
|
|
244
262
|
const status = String(data.status ?? "");
|
|
245
263
|
const terminal = status === "completed" || status === "failed" || status === "cancelled";
|
package/dist/shared.js
CHANGED
|
@@ -27,15 +27,42 @@ export const RETRY_BASE_MS = parseInt(process.env.BARIVIA_RETRY_BASE_MS ?? "2000
|
|
|
27
27
|
export const RETRYABLE_STATUS = new Set([502, 503, 504]);
|
|
28
28
|
/** Cool-down hint after the burst is exhausted — agents should wait before another round. */
|
|
29
29
|
export const UNAVAILABLE_RETRY_AFTER_SEC = 30;
|
|
30
|
-
/**
|
|
31
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Delay between burst retries. Keeps the flat RETRY_BASE_MS default (short gateway
|
|
32
|
+
* blips); when the server sends Retry-After / retry_after_sec, sleep at least that long.
|
|
33
|
+
*/
|
|
34
|
+
export function retryDelayMs(_attempt, retryAfterMs) {
|
|
35
|
+
if (retryAfterMs != null && Number.isFinite(retryAfterMs) && retryAfterMs > 0) {
|
|
36
|
+
return Math.max(RETRY_BASE_MS, Math.floor(retryAfterMs));
|
|
37
|
+
}
|
|
32
38
|
return RETRY_BASE_MS;
|
|
33
39
|
}
|
|
40
|
+
/** Parse Retry-After header (delta-seconds) and/or JSON body retry_after_sec → ms. */
|
|
41
|
+
export function parseRetryAfterMs(resp, bodyText) {
|
|
42
|
+
const header = resp.headers.get("Retry-After");
|
|
43
|
+
if (header) {
|
|
44
|
+
const sec = parseInt(header.trim(), 10);
|
|
45
|
+
if (!Number.isNaN(sec) && sec >= 0)
|
|
46
|
+
return sec * 1000;
|
|
47
|
+
}
|
|
48
|
+
if (bodyText) {
|
|
49
|
+
try {
|
|
50
|
+
const j = JSON.parse(bodyText);
|
|
51
|
+
const sec = Number(j.retry_after_sec);
|
|
52
|
+
if (Number.isFinite(sec) && sec >= 0)
|
|
53
|
+
return sec * 1000;
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
/* not JSON */
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
34
61
|
function newRequestId() {
|
|
35
62
|
return randomUUID();
|
|
36
63
|
}
|
|
37
64
|
/** Single source of truth for the proxy version. Keep in sync with package.json on bump. */
|
|
38
|
-
export const CLIENT_VERSION = "0.8.
|
|
65
|
+
export const CLIENT_VERSION = "0.8.10";
|
|
39
66
|
export const PUBLIC_SITE_ORIGIN = "https://barivia.se";
|
|
40
67
|
/** Large per-cell CSV uploads may exceed the default fetch timeout. */
|
|
41
68
|
export const UPLOAD_DATASET_TIMEOUT_MS = 180_000;
|
|
@@ -148,7 +175,11 @@ export function suggestAfterCfdSubmit(jobId, prepSuffix = "") {
|
|
|
148
175
|
export function suggestAfterRender(parentJobId, fmt) {
|
|
149
176
|
return `Figures rendered as ${fmt}. Download with barmesh_results(action=download, job_id="${parentJobId}") or barmesh_results(action=image, job_id="${parentJobId}", filename=<figure>.${fmt}).`;
|
|
150
177
|
}
|
|
151
|
-
/**
|
|
178
|
+
/**
|
|
179
|
+
* Streaming SHA-256 of a file without reading it into memory.
|
|
180
|
+
* Used by tools/datasets.ts upload as Idempotency-Key = sha256(name+body) — do not
|
|
181
|
+
* invent a second key path here.
|
|
182
|
+
*/
|
|
152
183
|
export async function streamFileSha256(srcPath) {
|
|
153
184
|
return new Promise((resolve, reject) => {
|
|
154
185
|
const h = createHash("sha256");
|
|
@@ -707,7 +738,7 @@ export async function apiCall(method, pathPart, body, extraHeaders, requestTimeo
|
|
|
707
738
|
const text = await resp.text();
|
|
708
739
|
if (!resp.ok) {
|
|
709
740
|
if (attempt < MAX_RETRIES && isTransientError(null, resp.status)) {
|
|
710
|
-
const delayMs = retryDelayMs(attempt);
|
|
741
|
+
const delayMs = retryDelayMs(attempt, parseRetryAfterMs(resp, text));
|
|
711
742
|
logWarn("API retry", {
|
|
712
743
|
rid: requestId,
|
|
713
744
|
action: method,
|
|
@@ -729,7 +760,10 @@ export async function apiCall(method, pathPart, body, extraHeaders, requestTimeo
|
|
|
729
760
|
catch (err) {
|
|
730
761
|
lastError = err;
|
|
731
762
|
if (attempt < MAX_RETRIES && isTransientError(err)) {
|
|
732
|
-
const
|
|
763
|
+
const errRetryMs = typeof err.retryAfterSec === "number"
|
|
764
|
+
? err.retryAfterSec * 1000
|
|
765
|
+
: undefined;
|
|
766
|
+
const delayMs = retryDelayMs(attempt, errRetryMs);
|
|
733
767
|
logWarn("API retry", {
|
|
734
768
|
rid: requestId,
|
|
735
769
|
action: method,
|
|
@@ -764,8 +798,9 @@ export async function apiRawCall(pathPart, requestTimeoutMs) {
|
|
|
764
798
|
try {
|
|
765
799
|
const resp = await fetchWithTimeout(url, { method: "GET", headers: { Authorization: `Bearer ${API_KEY}`, "X-Request-ID": requestId, traceparent: _traceparentHeader() } }, effectiveTimeout);
|
|
766
800
|
if (!resp.ok) {
|
|
801
|
+
const text = await resp.text();
|
|
767
802
|
if (attempt < MAX_RETRIES && isTransientError(null, resp.status)) {
|
|
768
|
-
const delayMs = retryDelayMs(attempt);
|
|
803
|
+
const delayMs = retryDelayMs(attempt, parseRetryAfterMs(resp, text));
|
|
769
804
|
logWarn("API retry", {
|
|
770
805
|
rid: requestId,
|
|
771
806
|
path: pathPart,
|
|
@@ -777,7 +812,6 @@ export async function apiRawCall(pathPart, requestTimeoutMs) {
|
|
|
777
812
|
await new Promise((r) => setTimeout(r, delayMs));
|
|
778
813
|
continue;
|
|
779
814
|
}
|
|
780
|
-
const text = await resp.text();
|
|
781
815
|
throwApiError(resp.status, text, requestId);
|
|
782
816
|
}
|
|
783
817
|
const arrayBuf = await resp.arrayBuffer();
|
|
@@ -789,7 +823,10 @@ export async function apiRawCall(pathPart, requestTimeoutMs) {
|
|
|
789
823
|
catch (err) {
|
|
790
824
|
lastError = err;
|
|
791
825
|
if (attempt < MAX_RETRIES && isTransientError(err)) {
|
|
792
|
-
const
|
|
826
|
+
const errRetryMs = typeof err.retryAfterSec === "number"
|
|
827
|
+
? err.retryAfterSec * 1000
|
|
828
|
+
: undefined;
|
|
829
|
+
const delayMs = retryDelayMs(attempt, errRetryMs);
|
|
793
830
|
logWarn("API retry", {
|
|
794
831
|
rid: requestId,
|
|
795
832
|
path: pathPart,
|
|
@@ -163,6 +163,7 @@ async function handleResultsExplorer(job_id) {
|
|
|
163
163
|
jobId: job_id,
|
|
164
164
|
jobStatus: "completed",
|
|
165
165
|
inlineImageAttached,
|
|
166
|
+
uiResourceUri: RESULTS_EXPLORER_URI,
|
|
166
167
|
});
|
|
167
168
|
appendUiDeliveryContent(content, uiDelivery, {
|
|
168
169
|
linkLabel: "Open results explorer",
|
package/dist/tools/cfd.js
CHANGED
|
@@ -5,34 +5,33 @@ import { pollCfdPrepareIfPresent } from "../cfd_prepare.js";
|
|
|
5
5
|
export function registerCfdTools(server) {
|
|
6
6
|
registerAuditedTool(server, "barmesh_mesh_convergence", `Run SOM-based mesh-convergence analysis on an uploaded combined per-cell CSV.
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
COMMON MISTAKES: omitting feature_columns (required); choosing a reference_mesh label that is not present; forgetting the cell-volume column at upload time; multi-topology studies (uniform U + graded G meshes) without explicit mesh_order — equal cell counts sort U-then-G and invent bogus stepwise pairs (U160|G20). Pass mesh_order coarse→fine interleaved by refinement (U20,G20,U40,G40,…) or use preset=cavity which defaults interleaved when U/G labels are detected.`, {
|
|
8
|
+
BEST FOR: Field-level mesh-refinement comparison (not a single scalar QoI).
|
|
9
|
+
NOT FOR: Classical GCI on scalars — use barmesh_richardson.
|
|
10
|
+
ASYNC: Returns job_id; poll barmesh_jobs(status) every 10–20s; then barmesh_results(get).
|
|
11
|
+
Call **barmesh_guide_workflow** / **barmesh_prepare_mesh_data** for method details, mesh_order rules, and prep recipe (API, CFD entitlement).
|
|
12
|
+
COMMON MISTAKES: omitting feature_columns; bad reference_mesh label; missing volume column — see prepare_mesh_data.`, {
|
|
14
13
|
dataset_id: z.string().describe("Dataset ID from barmesh_datasets(upload)"),
|
|
15
|
-
feature_columns: z.array(z.string()).describe("Per-cell numeric feature columns
|
|
16
|
-
preset: z.enum(["generic", "cavity", "pitz", "datacenter"]).optional().describe("Hyperparameter preset
|
|
17
|
-
mesh_column: z.string().optional().describe("
|
|
18
|
-
volume_column: z.string().optional().describe("Cell-volume column
|
|
19
|
-
reference_mesh: z.string().optional().describe("
|
|
20
|
-
mesh_order: z.array(z.string()).optional().describe("Explicit coarse
|
|
21
|
-
grid: z.array(z.number().int()).optional().describe("SOM grid [rows, cols]
|
|
22
|
-
epochs: z.array(z.number().int()).optional().describe("[ordering, convergence] epochs
|
|
23
|
-
batch_size: z.number().int().optional().describe("SOM batch size
|
|
24
|
-
sigma_f: z.number().optional().describe("Final ordering neighborhood width
|
|
25
|
-
backend: z.enum(["auto", "cpu", "gpu", "gpu_graphs"]).optional().describe("Compute backend
|
|
26
|
-
stratify_scale: z.number().optional().describe("[0,1] per-mesh training-row cap
|
|
27
|
-
emd_method: z.enum(["exact", "sinkhorn"]).optional().describe("EMD solver
|
|
28
|
-
te_panel_size: z.number().int().optional().describe("
|
|
14
|
+
feature_columns: z.array(z.string()).describe("Per-cell numeric feature columns for the SOM"),
|
|
15
|
+
preset: z.enum(["generic", "cavity", "pitz", "datacenter"]).optional().describe("Hyperparameter preset (default generic) — see barmesh_guide_workflow"),
|
|
16
|
+
mesh_column: z.string().optional().describe("Mesh label column (default mesh_id)"),
|
|
17
|
+
volume_column: z.string().optional().describe("Cell-volume column (default V)"),
|
|
18
|
+
reference_mesh: z.string().optional().describe("Reference mesh label (default: finest / most cells)"),
|
|
19
|
+
mesh_order: z.array(z.string()).optional().describe("Explicit coarse→fine mesh order — see barmesh_prepare_mesh_data"),
|
|
20
|
+
grid: z.array(z.number().int()).optional().describe("SOM grid [rows, cols] override"),
|
|
21
|
+
epochs: z.array(z.number().int()).optional().describe("[ordering, convergence] epochs override"),
|
|
22
|
+
batch_size: z.number().int().optional().describe("SOM batch size override"),
|
|
23
|
+
sigma_f: z.number().optional().describe("Final ordering neighborhood width override"),
|
|
24
|
+
backend: z.enum(["auto", "cpu", "gpu", "gpu_graphs"]).optional().describe("Compute backend"),
|
|
25
|
+
stratify_scale: z.number().optional().describe("[0,1] per-mesh training-row cap"),
|
|
26
|
+
emd_method: z.enum(["exact", "sinkhorn"]).optional().describe("EMD solver — see barmesh_guide_workflow"),
|
|
27
|
+
te_panel_size: z.number().int().optional().describe("Live TE evaluation panel size"),
|
|
29
28
|
te_inner_samples: z.number().int().optional().describe("Deprecated alias for te_panel_size"),
|
|
30
|
-
component_planes_physical: z.boolean().optional().describe("Physical-scale component-plane colorbars
|
|
29
|
+
component_planes_physical: z.boolean().optional().describe("Physical-scale component-plane colorbars"),
|
|
31
30
|
figures: z.boolean().optional().describe("Generate publication figures (default true)"),
|
|
32
|
-
transforms: z.record(z.enum(["log", "log1p", "log10", "sqrt", "square", "abs", "invert", "none"])).optional().describe("Per-feature transform
|
|
33
|
-
normalize: z.union([z.enum(["all", "auto", "mad", "sigmoidal", "sepd"]), z.array(z.string())]).optional().describe("
|
|
34
|
-
normalization_methods: z.record(z.enum(["zscore", "mad", "sigmoidal", "sepd", "none"])).optional().describe("Per-feature normalization override
|
|
35
|
-
row_range: z.tuple([z.number().int().min(1), z.number().int().min(1)]).optional().describe("1-based inclusive [start, end] row slice
|
|
31
|
+
transforms: z.record(z.enum(["log", "log1p", "log10", "sqrt", "square", "abs", "invert", "none"])).optional().describe("Per-feature transform before normalization"),
|
|
32
|
+
normalize: z.union([z.enum(["all", "auto", "mad", "sigmoidal", "sepd"]), z.array(z.string())]).optional().describe("SOM feature normalization mode"),
|
|
33
|
+
normalization_methods: z.record(z.enum(["zscore", "mad", "sigmoidal", "sepd", "none"])).optional().describe("Per-feature normalization override"),
|
|
34
|
+
row_range: z.tuple([z.number().int().min(1), z.number().int().min(1)]).optional().describe("1-based inclusive [start, end] row slice"),
|
|
36
35
|
label: z.string().optional().describe("Optional job label"),
|
|
37
36
|
description: z.string().optional().describe("Optional free-text description for inventory"),
|
|
38
37
|
tags: z.array(z.string()).optional().describe("Optional tags for inventory/navigation"),
|
|
@@ -63,21 +62,20 @@ COMMON MISTAKES: omitting feature_columns (required); choosing a reference_mesh
|
|
|
63
62
|
}
|
|
64
63
|
return textResult(data);
|
|
65
64
|
});
|
|
66
|
-
registerAuditedTool(server, "barmesh_richardson", `Run classical Richardson extrapolation / Grid Convergence Index (GCI) on scalar
|
|
67
|
-
|
|
68
|
-
What it does: from a small CSV with one row per mesh (a mesh label, a representative cell size h or a cell count, and one or more QoI columns), slides the three-level Celik (2008) GCI over consecutive mesh triplets (finest first) and reports the observed order p, the extrapolated value, and the fine-grid GCI per triplet per QoI.
|
|
65
|
+
registerAuditedTool(server, "barmesh_richardson", `Run classical Richardson extrapolation / Grid Convergence Index (GCI) on scalar QoIs.
|
|
69
66
|
|
|
70
|
-
BEST FOR: Scalar benchmarks
|
|
71
|
-
NOT FOR: High-dimensional field comparison — use barmesh_mesh_convergence
|
|
72
|
-
ASYNC: queued job; poll barmesh_jobs(
|
|
73
|
-
|
|
67
|
+
BEST FOR: Scalar benchmarks where classical asymptotic-convergence checks are expected.
|
|
68
|
+
NOT FOR: High-dimensional field comparison — use barmesh_mesh_convergence.
|
|
69
|
+
ASYNC: queued job; poll barmesh_jobs(status), then barmesh_results(get).
|
|
70
|
+
Method details (Fs, one-family-per-CSV, triplets): **barmesh_guide_workflow**.
|
|
71
|
+
COMMON MISTAKES: missing h_column or n_cells_column; mixing mesh families — see guide.`, {
|
|
74
72
|
dataset_id: z.string().describe("Dataset ID (one row per mesh)"),
|
|
75
73
|
qoi_columns: z.array(z.string()).describe("Scalar QoI column names to extrapolate"),
|
|
76
74
|
mesh_column: z.string().optional().describe("Mesh label column (default mesh_id)"),
|
|
77
|
-
h_column: z.string().optional().describe("Representative cell-size column (
|
|
78
|
-
n_cells_column: z.string().optional().describe("Cell-count column
|
|
79
|
-
dimension: z.number().int().optional().describe("Spatial dimension
|
|
80
|
-
safety_factor: z.number().optional().describe("GCI safety factor
|
|
75
|
+
h_column: z.string().optional().describe("Representative cell-size column (or use n_cells_column)"),
|
|
76
|
+
n_cells_column: z.string().optional().describe("Cell-count column (alternative to h_column)"),
|
|
77
|
+
dimension: z.number().int().optional().describe("Spatial dimension for h from cell count"),
|
|
78
|
+
safety_factor: z.number().optional().describe("GCI safety factor — see barmesh_guide_workflow"),
|
|
81
79
|
label: z.string().optional().describe("Optional job label"),
|
|
82
80
|
description: z.string().optional().describe("Optional free-text description for inventory"),
|
|
83
81
|
tags: z.array(z.string()).optional().describe("Optional tags for inventory/navigation"),
|
package/dist/tools/feedback.js
CHANGED
|
@@ -23,7 +23,7 @@ export function registerFeedbackTool(server) {
|
|
|
23
23
|
|
|
24
24
|
For a substantial issue, prefer feedback_items: submit several focused instances (each max 1400 chars) covering, e.g., symptoms, exact reproduction steps, environment, and concrete asks — they are stored together as one batch so developers see the full picture. Use the single feedback field for a short one-off note.
|
|
25
25
|
|
|
26
|
-
If the API or object storage is unavailable (e.g. gateway HTML 502 mid-demo), this tool queues feedback locally (~/.barivia/deferred-feedback, shared with barsom send_feedback) and returns deferred=true with request_id + api_health from GET /health (no R2). Call again later to flush.`, {
|
|
26
|
+
If the API or object storage is unavailable (e.g. gateway HTML 502 mid-demo), this tool queues feedback locally (~/.barivia/deferred-feedback, shared with barsom send_feedback) and returns deferred=true with request_id + api_health from GET /health (no R2). Call again later to flush (idempotent enqueue of the same draft).`, {
|
|
27
27
|
feedback: z.string().max(1400).optional().describe("Single feedback note (max 1400 characters). Use feedback_items instead for a multi-part report."),
|
|
28
28
|
feedback_items: z.array(z.string().max(1400)).max(10).optional().describe("Several feedback instances (each max 1400 characters, up to 10), stored together as one batch. Prefer this for a detailed issue: split it into focused parts (symptoms, reproduction, environment, asks)."),
|
|
29
29
|
}, async ({ feedback, feedback_items }) => {
|
|
@@ -103,5 +103,10 @@ If the API or object storage is unavailable (e.g. gateway HTML 502 mid-demo), th
|
|
|
103
103
|
}
|
|
104
104
|
throw err;
|
|
105
105
|
}
|
|
106
|
+
}, {
|
|
107
|
+
readOnlyHint: false,
|
|
108
|
+
destructiveHint: false,
|
|
109
|
+
idempotentHint: true,
|
|
110
|
+
openWorldHint: true,
|
|
106
111
|
});
|
|
107
112
|
}
|
package/dist/tools/guide.js
CHANGED
|
@@ -3,27 +3,12 @@ import { apiCall, probeApiHealth, structuredTextResult, textResult } from "../sh
|
|
|
3
3
|
import { listDeferredFeedback } from "../deferred_feedback.js";
|
|
4
4
|
const OFFLINE_GUIDE = `Mesh analysis API temporarily unavailable — figures already on disk stay available; new jobs/feedback will wait.
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
Say the API is unavailable plainly; the proxy already burst-retried (~3×2s) — wait ~30s before another round. Keep reviewing the last good distances/figures; do not treat the session as failed.
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
- barmesh_richardson — classical Richardson / GCI on scalar QoIs (one mesh family per CSV: uniform OR graded, not mixed)
|
|
8
|
+
When reconnecting (BARIVIA_API_KEY / BARIVIA_API_URL), call barmesh_guide_workflow again for the plan-scoped workflow.`;
|
|
9
|
+
const OFFLINE_PREP = `API unreachable — cannot load the CFD mesh-prep recipe.
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
1. barmesh_prepare_mesh_data — recipe for the combined per-cell CSV (mesh_id + features + cell volume V)
|
|
14
|
-
2. barmesh_datasets(upload) → barmesh_datasets(preview)
|
|
15
|
-
3. barmesh_mesh_convergence and/or barmesh_richardson — returns job_id
|
|
16
|
-
- Multi-topology cavity studies: pass mesh_order interleaved (U20,G20,U40,G40,…) or preset=cavity
|
|
17
|
-
4. barmesh_training_monitor (visual MCP App) or barmesh_jobs(action=monitor) headless — poll until complete
|
|
18
|
-
5. barmesh_results(action=get) → barmesh_results_explorer for figures (mesh_convergence) or richardson_gci.csv summary (Richardson)
|
|
19
|
-
|
|
20
|
-
(Set BARIVIA_API_KEY / BARIVIA_API_URL when reconnecting.)`;
|
|
21
|
-
const OFFLINE_PREP = `barmesh mesh-data prep (offline summary; API unreachable):
|
|
22
|
-
Build ONE combined per-cell CSV across all meshes of the refinement study:
|
|
23
|
-
- one row per cell; a mesh label column (mesh_id); the physical channels you want compared (e.g. p, U_mag, k, log_epsilon — log-compress turbulence quantities); and a cell-volume column (V) for fingerprint weighting.
|
|
24
|
-
- keep the SAME feature columns across every mesh; pick the finest mesh as the reference.
|
|
25
|
-
- multi-topology (U + G): pass mesh_order coarse→fine interleaved by refinement (U20,G20,U40,G40,U80,G80,U160,G160) or use preset=cavity.
|
|
26
|
-
Then: barmesh_datasets(upload) -> barmesh_mesh_convergence. (Set BARIVIA_API_KEY / BARIVIA_API_URL.)`;
|
|
11
|
+
Retry when BARIVIA_API_KEY / BARIVIA_API_URL reconnect, then call barmesh_prepare_mesh_data again (entitlement-scoped).`;
|
|
27
12
|
export function registerGuideTool(server) {
|
|
28
13
|
registerAuditedTool(server, "barmesh_guide_workflow", `Get the barmesh CFD mesh-convergence workflow and tool map from the API (tier-scoped).
|
|
29
14
|
|
|
@@ -49,7 +34,7 @@ UNAVAILABLE: If the API is down, this tool returns a calm offline summary — sa
|
|
|
49
34
|
|
|
50
35
|
BEST FOR: Before barmesh_datasets(upload) — tells you which physical channels to extract, how to label meshes (mesh_id), the cell-volume column (V), and how to pick the reference mesh.
|
|
51
36
|
NOT FOR: Submitting jobs — after preparing the CSV, use barmesh_datasets(upload) then barmesh_mesh_convergence.
|
|
52
|
-
COMMON MISTAKES:
|
|
37
|
+
COMMON MISTAKES: missing volume column; mismatched channels across meshes; multi-topology order issues — full recipe is in the API response when entitled.`, {}, async () => {
|
|
53
38
|
try {
|
|
54
39
|
const data = (await apiCall("GET", "/v1/cfd/prep"));
|
|
55
40
|
return textResult({ recipe: data.recipe, entitled: data.entitled });
|
package/dist/tools/jobs.js
CHANGED
|
@@ -20,8 +20,9 @@ export function registerJobsTool(server) {
|
|
|
20
20
|
|
|
21
21
|
BEST FOR: action=monitor after submit (one call, throttled snapshots — preferred for agents). action=status for a single one-shot check. action=inventory for a durable org catalog. action=delete for cleanup by known job_id (do not deep-page list to find IDs).
|
|
22
22
|
MONITOR MODES: barmesh_training_monitor — default visual MCP App (live curves, post-hoc review on completed jobs). barmesh_jobs(action=monitor) — headless blocking snapshots for agents (live or post-hoc review; attaches learning_curve.png when already completed).
|
|
23
|
-
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=...) then barmesh_results_explorer(job_id=...).
|
|
24
|
-
LIST: slim by default (label,id,status,job_type,dataset_id,created_at,result_ref,description,tags — no fat params). has_results=true is the results catalog.
|
|
23
|
+
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 — do not abandon after one poll. When status=completed, call barmesh_results(action=get, job_id=...) then barmesh_results_explorer(job_id=...).
|
|
24
|
+
LIST: slim by default (label,id,status,job_type,dataset_id,created_at,result_ref,description,tags — no fat params). Page with limit+cursor; when next_cursor is returned, call again with cursor=. has_results=true is the results catalog.
|
|
25
|
+
INVENTORY: one-shot datasets + latest N jobs (limit); not a full unbounded job history — use list+cursor for more.
|
|
25
26
|
ESCALATION: status=failed returns an error message and (when available) a failure_stage; read it before retrying.
|
|
26
27
|
UNAVAILABLE: If the mesh analysis API is temporarily unavailable, say so plainly, pause (proxy already burst-retried; wait ~retry_after_sec), and keep reviewing the last good distances/figures — do not treat the demo as failed. Suggest retry later.`, {
|
|
27
28
|
action: z
|
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { registerAppTool } from "@modelcontextprotocol/ext-apps/server";
|
|
3
3
|
import { runMcpToolAudit } from "../audit.js";
|
|
4
|
+
import { logWarn } from "../logger.js";
|
|
4
5
|
import { apiCall, BARMESH_VIZ_VIEWS, buildStandaloneVizPageUrl, buildVizRelatedUrls, getVizPort, structuredTextResult, } from "../shared.js";
|
|
5
6
|
import { appendUiDeliveryContent, resolveUiDelivery, } from "../ui-delivery.js";
|
|
6
7
|
import { enrichWithTrainingLog, needsTrainingLogEnrichment } from "../training_review.js";
|
|
7
8
|
import { formatRunConfigTable } from "../run_config.js";
|
|
8
9
|
export const TRAINING_MONITOR_URI = "ui://barmesh/training-monitor";
|
|
9
10
|
export const TRAINING_MONITOR_REFRESH_MS = 5000;
|
|
11
|
+
/** Loose shape check — warn + pass through on failure (never drop a successful monitor payload). */
|
|
12
|
+
const BarmeshTrainingMonitorStructuredSchema = z
|
|
13
|
+
.object({
|
|
14
|
+
type: z.literal("barmesh-training-monitor"),
|
|
15
|
+
jobId: z.string(),
|
|
16
|
+
refresh_interval_ms: z.number(),
|
|
17
|
+
})
|
|
18
|
+
.passthrough();
|
|
10
19
|
function buildStructuredContent(job_id, data, port) {
|
|
11
20
|
const id = String(data.id ?? job_id);
|
|
12
21
|
const standaloneUrl = buildStandaloneVizPageUrl(port, BARMESH_VIZ_VIEWS.trainingMonitor, id);
|
|
@@ -24,13 +33,13 @@ function buildStructuredContent(job_id, data, port) {
|
|
|
24
33
|
export function registerTrainingMonitorTool(server) {
|
|
25
34
|
registerAppTool(server, "barmesh_training_monitor", {
|
|
26
35
|
title: "Mesh Convergence Training Monitor",
|
|
27
|
-
description: "Default
|
|
36
|
+
description: "Default monitor for mesh_convergence jobs after submit. Prefer the standalone localhost URL in the tool result (always surfaced when the viz server is up) — some hosts list MCP Apps without mounting panels. When mounted, the App auto-refreshes every 5s: epoch progress, phase, ETA, QE/TE curves (≤1000 batch samples per phase), panel fingerprint ΔKL, hit-grid heatmap. Completed jobs replay training-log curves. Set BARIVIA_VIZ_PORT for a stable port. Headless: barmesh_jobs(action=monitor) or barmesh_jobs(action=status). After completion, barmesh_results_explorer for figures.",
|
|
28
37
|
inputSchema: {
|
|
29
38
|
job_id: z.string().describe("Job ID from barmesh_mesh_convergence or barmesh_richardson"),
|
|
30
39
|
fetch_training_log: z
|
|
31
40
|
.boolean()
|
|
32
41
|
.optional()
|
|
33
|
-
.describe("
|
|
42
|
+
.describe("[INTERNAL — App/refresh path] Merge completed-job training-log arrays when live progress is empty. Agents should omit this."),
|
|
34
43
|
},
|
|
35
44
|
_meta: { ui: { resourceUri: TRAINING_MONITOR_URI } },
|
|
36
45
|
}, async (args) => runMcpToolAudit("barmesh_training_monitor", "default", args, async () => {
|
|
@@ -58,12 +67,14 @@ export function registerTrainingMonitorTool(server) {
|
|
|
58
67
|
const modeNote = terminal ? " (post-hoc review — training-log curves)" : "";
|
|
59
68
|
const runConfigNote = formatRunConfigTable(data);
|
|
60
69
|
const text = (runConfigNote ? `${runConfigNote}\n` : "") +
|
|
61
|
-
`Training monitor (
|
|
70
|
+
`Training monitor (standalone URL + optional App panel; refreshes every ${TRAINING_MONITOR_REFRESH_MS / 1000}s when mounted): job ${job_id} — ${jobStatus} (${progress.toFixed(1)}%).${modeNote}${timingNote} ` +
|
|
71
|
+
`For headless progress use barmesh_jobs(action=status); this tool is optional.`;
|
|
62
72
|
const content = [{ type: "text", text }];
|
|
63
73
|
const uiDelivery = resolveUiDelivery({
|
|
64
74
|
tool: "training_monitor",
|
|
65
75
|
jobId: job_id,
|
|
66
76
|
jobStatus,
|
|
77
|
+
uiResourceUri: TRAINING_MONITOR_URI,
|
|
67
78
|
});
|
|
68
79
|
appendUiDeliveryContent(content, uiDelivery, {
|
|
69
80
|
linkLabel: "Open training monitor",
|
|
@@ -79,6 +90,13 @@ export function registerTrainingMonitorTool(server) {
|
|
|
79
90
|
}
|
|
80
91
|
}
|
|
81
92
|
structuredContent.ui_delivery = uiDelivery;
|
|
93
|
+
const parsed = BarmeshTrainingMonitorStructuredSchema.safeParse(structuredContent);
|
|
94
|
+
if (!parsed.success) {
|
|
95
|
+
logWarn("barmesh_training_monitor structuredContent validation failed — passing through", {
|
|
96
|
+
job_id,
|
|
97
|
+
issues: parsed.error.issues.slice(0, 5).map((i) => i.message),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
82
100
|
return {
|
|
83
101
|
...structuredTextResult(structuredContent, text, content),
|
|
84
102
|
_meta: { ui: { resourceUri: TRAINING_MONITOR_URI } },
|
package/dist/ui-delivery.js
CHANGED
|
@@ -6,11 +6,20 @@ import { getClientSupportsMcpApps, getVizPort, } from "./shared.js";
|
|
|
6
6
|
import { buildBarmeshVizStandaloneUrl } from "./viz_links.js";
|
|
7
7
|
export function parseUiDeliveryOverride() {
|
|
8
8
|
const raw = (process.env.BARIVIA_UI_DELIVERY ?? "auto").trim().toLowerCase();
|
|
9
|
-
if (raw === "apps" ||
|
|
9
|
+
if (raw === "apps" ||
|
|
10
|
+
raw === "localhost" ||
|
|
11
|
+
raw === "inline" ||
|
|
12
|
+
raw === "text_only" ||
|
|
13
|
+
raw === "auto") {
|
|
10
14
|
return raw;
|
|
11
15
|
}
|
|
12
16
|
return "auto";
|
|
13
17
|
}
|
|
18
|
+
/** Opt-in: force localhost under auto even when the host advertises MCP Apps. */
|
|
19
|
+
export function preferLocalhostUi() {
|
|
20
|
+
const v = (process.env.BARIVIA_UI_PREFER_LOCALHOST ?? "").trim().toLowerCase();
|
|
21
|
+
return v === "1" || v === "true" || v === "yes";
|
|
22
|
+
}
|
|
14
23
|
function vizPortPinned() {
|
|
15
24
|
return Boolean(process.env.BARIVIA_VIZ_PORT?.trim());
|
|
16
25
|
}
|
|
@@ -29,17 +38,26 @@ export function buildHealthUrl(jobId) {
|
|
|
29
38
|
}
|
|
30
39
|
function hintForTier(tier, tool, opts) {
|
|
31
40
|
const label = tool === "training_monitor" ? "training monitor" : "results explorer";
|
|
41
|
+
const linkFirst = opts.standaloneUrl
|
|
42
|
+
? `AGENT: Post a clickable markdown link first (e.g. "[Open ${label}](${opts.standaloneUrl})") — do not bury it after metrics.`
|
|
43
|
+
: undefined;
|
|
32
44
|
switch (tier) {
|
|
33
45
|
case "embedded":
|
|
34
|
-
return
|
|
46
|
+
return linkFirst
|
|
47
|
+
? `${linkFirst} Embedded panel if the host mounts Apps; some hosts list ui:// without mounting.`
|
|
48
|
+
: `The ${label} MCP App panel may embed. Always mention the standalone URL when present.`;
|
|
35
49
|
case "localhost":
|
|
36
|
-
return
|
|
37
|
-
?
|
|
50
|
+
return linkFirst
|
|
51
|
+
? `${linkFirst} Set BARIVIA_VIZ_PORT for a stable port.`
|
|
38
52
|
: `AGENT: Viz server unavailable — use barmesh_jobs(status) and barmesh_results(get).`;
|
|
39
53
|
case "inline_image":
|
|
40
|
-
return
|
|
54
|
+
return linkFirst
|
|
55
|
+
? `${linkFirst} Inline raster figure(s) may also be attached; offer barmesh_results_explorer for more.`
|
|
56
|
+
: `AGENT: Inline raster figure(s) are attached. Summarize metrics; offer barmesh_results_explorer for more figures.`;
|
|
41
57
|
case "text_only":
|
|
42
|
-
return
|
|
58
|
+
return linkFirst
|
|
59
|
+
? `${linkFirst} Otherwise use barmesh_jobs(status) and barmesh_results(get).`
|
|
60
|
+
: `AGENT: No embedded panel or localhost viz — use barmesh_jobs(status) and barmesh_results(get).`;
|
|
43
61
|
}
|
|
44
62
|
}
|
|
45
63
|
export function resolveUiDelivery(options) {
|
|
@@ -57,6 +75,12 @@ export function resolveUiDelivery(options) {
|
|
|
57
75
|
else if (override === "inline") {
|
|
58
76
|
delivery = options.inlineImageAttached ? "inline_image" : "text_only";
|
|
59
77
|
}
|
|
78
|
+
else if (override === "text_only") {
|
|
79
|
+
delivery = "text_only";
|
|
80
|
+
}
|
|
81
|
+
else if (preferLocalhostUi() && port) {
|
|
82
|
+
delivery = "localhost";
|
|
83
|
+
}
|
|
60
84
|
else if (mcpApps) {
|
|
61
85
|
delivery = "embedded";
|
|
62
86
|
}
|
|
@@ -70,11 +94,11 @@ export function resolveUiDelivery(options) {
|
|
|
70
94
|
delivery = "text_only";
|
|
71
95
|
}
|
|
72
96
|
const urls = [];
|
|
73
|
-
if (standaloneUrl
|
|
97
|
+
if (standaloneUrl) {
|
|
74
98
|
urls.push(standaloneUrl);
|
|
75
99
|
}
|
|
76
100
|
const healthUrl = buildHealthUrl(options.jobId);
|
|
77
|
-
if (healthUrl && delivery === "localhost") {
|
|
101
|
+
if (healthUrl && (delivery === "localhost" || preferLocalhostUi())) {
|
|
78
102
|
urls.push(healthUrl);
|
|
79
103
|
}
|
|
80
104
|
const crossLink = options.tool === "training_monitor" && options.jobStatus === "completed"
|
|
@@ -93,6 +117,7 @@ export function resolveUiDelivery(options) {
|
|
|
93
117
|
viz_port: port,
|
|
94
118
|
port_pinned: vizPortPinned(),
|
|
95
119
|
override,
|
|
120
|
+
...(options.uiResourceUri ? { ui_resource_uri: options.uiResourceUri } : {}),
|
|
96
121
|
};
|
|
97
122
|
}
|
|
98
123
|
/** Prepend prominent standalone URL + structured ui_delivery (Apps handshake may lie). */
|
|
@@ -128,6 +153,7 @@ export function getMcpClientDiagnostics() {
|
|
|
128
153
|
const override = parseUiDeliveryOverride();
|
|
129
154
|
const mcpApps = getClientSupportsMcpApps();
|
|
130
155
|
const port = getVizPort() || null;
|
|
156
|
+
const preferLocal = preferLocalhostUi();
|
|
131
157
|
let activeTier = "text_only";
|
|
132
158
|
if (override === "apps") {
|
|
133
159
|
activeTier = mcpApps ? "embedded" : port ? "localhost" : "text_only";
|
|
@@ -138,6 +164,12 @@ export function getMcpClientDiagnostics() {
|
|
|
138
164
|
else if (override === "inline") {
|
|
139
165
|
activeTier = "inline_image";
|
|
140
166
|
}
|
|
167
|
+
else if (override === "text_only") {
|
|
168
|
+
activeTier = "text_only";
|
|
169
|
+
}
|
|
170
|
+
else if (preferLocal && port) {
|
|
171
|
+
activeTier = "localhost";
|
|
172
|
+
}
|
|
141
173
|
else if (mcpApps) {
|
|
142
174
|
activeTier = "embedded";
|
|
143
175
|
}
|
|
@@ -149,6 +181,7 @@ export function getMcpClientDiagnostics() {
|
|
|
149
181
|
viz_port: port,
|
|
150
182
|
viz_port_pinned: vizPortPinned(),
|
|
151
183
|
ui_delivery_override: override,
|
|
184
|
+
ui_prefer_localhost: preferLocal,
|
|
152
185
|
active_delivery_tier: activeTier,
|
|
153
186
|
health_url: buildHealthUrl(),
|
|
154
187
|
};
|
|
@@ -136,7 +136,7 @@ Boolean requesting whether a visible border and background is provided by the ho
|
|
|
136
136
|
- omitted: host decides border`)});p({method:c("ui/request-display-mode"),params:p({mode:Le.describe("The display mode being requested.")})});var Hf=p({mode:Le.describe("The display mode that was actually set. May differ from requested if not supported.")}).passthrough(),Jf=T([c("model"),c("app")]).describe("Tool visibility scope - who can access the tool.");p({resourceUri:l().optional(),visibility:I(Jf).optional().describe(`Who can access this tool. Default: ["model", "app"]
|
|
137
137
|
- "model": Tool visible to and callable by the agent
|
|
138
138
|
- "app": Tool callable by the app from this server only`),csp:_e().optional(),permissions:_e().optional()});p({mimeTypes:I(l()).optional().describe('Array of supported MIME types for UI resources.\nMust include `"text/html;profile=mcp-app"` for MCP Apps support.')});p({method:c("ui/download-file"),params:p({contents:I(T([Wu,Ku])).describe("Resource contents to download — embedded (inline data) or linked (host fetches). Uses standard MCP resource types.")})});p({method:c("ui/message"),params:p({role:c("user").describe('Message role, currently only "user" is supported.'),content:I(ut).describe("Message content blocks (text, image, etc.).")})});p({method:c("ui/notifications/sandbox-resource-ready"),params:p({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:Gi.optional().describe("CSP configuration from resource metadata."),permissions:Qi.optional().describe("Sandbox permissions from resource metadata.")})});var Bf=p({method:c("ui/notifications/tool-result"),params:Un.describe("Standard MCP tool execution result.")}),tl=p({toolInfo:p({id:rt.optional().describe("JSON-RPC id of the tools/call request."),tool:Ki.describe("Tool definition including name, inputSchema, etc.")}).optional().describe("Metadata of the tool call that instantiated this App."),theme:Tf.optional().describe("Current color theme preference."),styles:Mf.optional().describe("Style configuration for theming the app."),displayMode:Le.optional().describe("How the UI is currently displayed."),availableDisplayModes:I(Le).optional().describe("Display modes the host supports."),containerDimensions:T([p({height:x().describe("Fixed container height in pixels.")}),p({maxHeight:T([x(),Ce()]).optional().describe("Maximum container height in pixels.")})]).and(T([p({width:x().describe("Fixed container width in pixels.")}),p({maxWidth:T([x(),Ce()]).optional().describe("Maximum container width in pixels.")})])).optional().describe(`Container dimensions. Represents the dimensions of the iframe or other
|
|
139
|
-
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:p({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:p({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(),Vf=p({method:c("ui/notifications/host-context-changed"),params:tl.describe("Partial context update containing only changed fields.")});p({method:c("ui/update-model-context"),params:p({content:I(ut).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.")})});p({method:c("ui/initialize"),params:p({appInfo:Nn.describe("App identification (name and version)."),appCapabilities:Ff.describe("Features and capabilities this app provides."),protocolVersion:l().describe("Protocol version this app supports.")})});var Wf=p({protocolVersion:l().describe('Negotiated protocol version string (e.g., "2025-11-21").'),hostInfo:Nn.describe("Host application identification and version."),hostCapabilities:qf.describe("Features and capabilities provided by the host."),hostContext:tl.describe("Rich context about the host environment.")}).passthrough(),Kf={target:"draft-2020-12"};async function wo(t,n){let r=t["~standard"];if(r.jsonSchema)return r.jsonSchema[n](Kf);if(r.vendor==="zod"){let{z:o}=await dl(()=>Promise.resolve().then(()=>Rm),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 So(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 Yi extends zf{_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:Rf,toolinputpartial:Zf,toolresult:Bf,toolcancelled:Cf,hostcontextchanged:Vf};static ONE_SHOT_EVENTS=new Set(["toolinput","toolinputpartial","toolresult","toolcancelled"]);_everHadListener=new Set;_assertHandlerTiming(n){if(!Yi.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(Tn,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=If(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(m){Object.assign(this,m),i()},remove(){e._registeredTools[n]===s&&(delete e._registeredTools[n],i())},handler:async(m,g)=>{if(!s.enabled)throw Error(`Tool ${n} is disabled`);let h;if(a){let d=s.inputSchema,b=d?await So(d,m??{},`Invalid input for tool ${n}: `):m??{};h=await o(b,g)}else h=await o(g);return s.outputSchema&&!h.isError&&(h.structuredContent=await So(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 wo(e.inputSchema,"input"):{type:"object",properties:{}}};return e.outputSchema&&(i.outputSchema=await wo(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(Lf,(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(Qu,(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(Gu,(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},Un,{onprogress:()=>{},resetTimeoutOnProgress:!0,...r})}async readServerResource(n,r){return this._assertInitialized("readServerResource"),await this.request({method:"resources/read",params:n},Vu,r)}async listServerResources(n,r){return this._assertInitialized("listServerResources"),await this.request({method:"resources/list",params:n},Bu,r)}async createSamplingMessage(n,r){this._assertInitialized("createSamplingMessage");let o=n.tools?el:Xu;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},Df,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},Ei,r)}openLink(n,r){return this._assertInitialized("openLink"),this.request({method:"ui/open-link",params:n},Pf,r)}sendOpenLink=this.openLink;downloadFile(n,r){return this._assertInitialized("downloadFile"),this.request({method:"ui/download-file",params:n},Ef,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},Hf,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 m=Math.ceil(a.getBoundingClientRect().height);a.style.height=s;let g=Math.ceil(window.innerWidth);(g!==r||m!==o)&&(r=g,o=m,this.sendSizeChanged({width:g,height:m}))}))};e();let i=new ResizeObserver(e);return i.observe(document.documentElement),i.observe(document.body),()=>i.disconnect()}async connect(n=new Nf(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:xf}},Wf,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}}}const Zn=["overview","distances","diagnostics","component"],Gf={overview:"Overview",distances:"Distances",diagnostics:"Diagnostics",component:"Component planes"};function zt(t){return t==="combined"?"overview":t==="overview_distances"||t.startsWith("KL_")||t.startsWith("EMD_")?"distances":t==="learning_curve"||t.startsWith("plot_vol")?"diagnostics":t.startsWith("divergence_")||t.startsWith("plot_")?"component":"diagnostics"}const Io={combined:0,overview_distances:1,KL_ref:10,KL_stepwise:11,KL_asymmetric:12,EMD_ref:13,EMD_stepwise:14,learning_curve:15,plot_vol_all_meshes:19,plot_vol_coarse_fine:20,divergence_kl:28,divergence_w1:29};function zo(t){return t in Io?Io[t]:t.startsWith("plot_vol")?25:t.startsWith("divergence_")?28:t.startsWith("plot_")?30:50}let ae=null,ue=null,re=null,Cn=null;const xo=new Map,vt=document.getElementById("loading"),Qf=document.getElementById("app"),nl=document.getElementById("title"),rl=document.getElementById("subtitle"),ft=document.getElementById("metadata-strip"),Yf=document.getElementById("metrics"),qe=document.getElementById("figure-select"),Xf=document.getElementById("figure-title"),eh=document.getElementById("figure-file"),th=document.getElementById("figure-caption"),ne=document.getElementById("main-image"),nh=document.getElementById("figure-empty"),ht=document.getElementById("open-standalone"),On=document.getElementById("open-monitor-btn"),ce=document.getElementById("copy-url"),jo=document.getElementById("related-monitor"),il=document.getElementById("download-current");function rh(t){return[...t].sort((n,r)=>Zn.indexOf(n.section??zt(n.key))-Zn.indexOf(r.section??zt(r.key))||zo(n.key)-zo(r.key)||n.label.localeCompare(r.label))}function ih(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 oh(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==="KL_asymmetric"?"Asymmetric mesh KL matrix (coarse↔fine)":n==="divergence_kl"?"Feature-plane symmetric KL (not mesh KL)":n==="divergence_w1"?"Feature-plane Wasserstein-1":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 ah(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 rh([...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`,m=/\.(png|svg)$/i.test(a),g=i.filter(h=>h==="pdf"||h==="svg");return{key:o,label:oh(a),filename:a,downloadFilename:s,formats:i,section:zt(o),caption:g.length>0?`High-quality ${g.join("/").toUpperCase()} available to download.`:void 0,url:m?`/api/results/${n}/image/${a}`:void 0,downloadUrl:`/api/results/${n}/image/${s}`}}))}function sh(t,n){const r=t.summary??{},o=t.label!=null?String(t.label):null,e=n.length>8?`${n.slice(0,8)}…`:n,i=r.grid??[],a=r.epochs??[],s=[o?`label ${o}`:null,`job ${e}`,String(r.job_type??"cfd_mesh_convergence")].filter(Boolean).join(" · "),m=[r.n_train_total!=null?`n_train ${r.n_train_total}`:null,r.reference_mesh!=null?`ref ${r.reference_mesh}`:null,i.length>=2?`grid ${i[0]}×${i[1]}`:null,a.length>=2?`epochs [${a[0]},${a[1]}]`:null,r.quantization_error!=null?`QE ${Number(r.quantization_error).toFixed(4)}`:null].filter(Boolean).join(" · ");return[s,m].filter(g=>g.length>0)}function ch(t,n){const r=t.summary??{},o=(r.files??[]).filter(d=>/\.(png|pdf|svg)$/i.test(d)),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",m=r.epochs??[],g=[{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:"Epochs",value:m.length>=2?`${m[0]}+${m[1]}`:"N/A"},{label:"QE",value:a},{label:"TE",value:s}],h=ah(o,n);return{type:"barmesh-results-explorer",jobId:n,title:"Mesh Convergence Results",subtitle:String(t.label??""),metadataStrip:sh(t,n),metrics:g,availableFigures:h,defaultFigureKey:ih(h),trainingMonitorUrl:`/viz/barmesh-training-monitor?mode=standalone&job_id=${encodeURIComponent(n)}`,standaloneUrl:`${window.location.origin}/viz/barmesh-results-explorer?mode=standalone&job_id=${encodeURIComponent(n)}`,relatedUrls:{trainingMonitor:`${window.location.origin}/viz/barmesh-training-monitor?mode=standalone&job_id=${encodeURIComponent(n)}`,resultsExplorer:`${window.location.origin}/viz/barmesh-results-explorer?mode=standalone&job_id=${encodeURIComponent(n)}`}}}function uh(t){if(!t||t.length===0){ft.style.display="none",ft.innerHTML="";return}ft.style.display="block",ft.innerHTML=t.map(n=>`<div class="metadata-line">${n}</div>`).join("")}function lh(t){Yf.innerHTML=t.map(n=>`<div class="metric"><span class="metric-label">${n.label}</span><span class="metric-value">${n.value}</span></div>`).join("")}function ol(t){const n=new Map;for(const r of t){const o=r.section??zt(r.key),e=n.get(o)??[];e.push(r),n.set(o,e)}qe.innerHTML="";for(const r of Zn){const o=n.get(r);if(!o||o.length===0)continue;const e=document.createElement("optgroup");e.label=Gf[r];for(const i of o){const a=document.createElement("option");a.value=i.key,a.textContent=`${i.label} (${i.filename})`,i.key===ue?.key&&(a.selected=!0),e.appendChild(a)}qe.appendChild(e)}}qe.addEventListener("change",()=>{const t=ae?.availableFigures.find(n=>n.key===qe.value);t&&al(t)});async function dh(t,n){const r=`${t}/${n}`,o=xo.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 xo.set(r,a),a}}catch{}return null}function mh(t){!re||!ae||re.updateModelContext({content:[{type:"text",text:`User is viewing the "${t.label}" figure (${t.filename}) of mesh convergence job ${ae.jobId}.`}]}).catch(()=>{})}function ph(t){return t.formats.length>1?` · formats: ${t.formats.join(", ")}`:""}function fh(t){const n=/\.(pdf|svg)$/i.test(t.downloadFilename);il.textContent=n?re?"Download PNG (preview)":`Download ${t.downloadFilename.split(".").pop()?.toUpperCase()} (high quality)`:"Download figure"}function hh(t){ne.style.display=t?"block":"none",nh.style.display=t?"none":"block"}async function al(t){ue=t,Xf.textContent=t.label,eh.textContent=`${t.filename}${ph(t)}`,th.textContent=t.caption??"",fh(t),mh(t),qe.value=t.key;let n=!1;if(t.url)ne.src=t.url,n=!0;else if(re&&ae){if(t.key===ae.defaultFigureKey&&Cn)ne.src=Cn,n=!0;else if(/\.(png|svg|jpe?g|webp)$/i.test(t.filename)){ne.removeAttribute("src");const r=await dh(ae.jobId,t.filename);r&&ue?.key===t.key&&(ne.src=r,n=!0)}}else/\.(png|svg|jpe?g|webp)$/i.test(t.filename)&&ne.removeAttribute("src");hh(n),ae&&ol(ae.availableFigures)}function gh(t){return t.standaloneUrl??window.location.href}function vh(t){const n=gh(t);t.standaloneUrl?(ht.href=t.standaloneUrl,ht.style.display="inline-flex"):cl&&(ht.href=window.location.href,ht.style.display="inline-flex");const r=t.relatedUrls?.trainingMonitor??t.trainingMonitorUrl??`/viz/barmesh-training-monitor?mode=standalone&job_id=${encodeURIComponent(t.jobId)}`;On.href=r,On.style.display="inline-flex",On.title="View live or completed training curves for this job",ce.style.display="inline-flex",ce.dataset.url=n;const o=t.relatedUrls?.trainingMonitor??t.trainingMonitorUrl;o&&(jo.href=o,jo.style.display="inline-flex")}function No(t){ae=t,nl.textContent=t.title,rl.textContent=t.subtitle?t.subtitle:`Job ${t.jobId}`,uh(t.metadataStrip),lh(t.metrics),vh(t);const n=t.availableFigures.find(r=>r.key===t.defaultFigureKey)??t.availableFigures[0]??null;ue=n,ol(t.availableFigures),n&&al(n),vt.style.display="none",Qf.style.display="block"}il.addEventListener("click",()=>{if(!ue)return;const t=document.createElement("a");if(ue.downloadUrl&&!re)t.href=ue.downloadUrl,t.download=ue.downloadFilename;else{const n=ne.src;if(!n)return;t.href=n,t.download=ue.filename}t.click()});ce.addEventListener("click",async()=>{const t=ce.dataset.url||ae?.standaloneUrl||window.location.href;try{await navigator.clipboard.writeText(t);const n=ce.textContent;ce.textContent="Copied!",setTimeout(()=>{ce.textContent=n??"Copy explorer URL"},1500)}catch{ce.textContent="Copy failed",setTimeout(()=>{ce.textContent="Copy explorer URL"},1500)}});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 sl=new URLSearchParams(window.location.search),cl=sl.get("mode")==="standalone",gt=sl.get("job_id");if(cl&>){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/${gt}`);if(o.status===404){vt.textContent=`Results for job ${gt} 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();No(ch(e,gt));return}catch(o){if(r<3){await new Promise(e=>setTimeout(e,1e3*r));continue}vt.textContent=`Could not load results from the local viz server (${o instanceof Error?o.message:"error"}). ${t}`}})()}else{const t=new Yi({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&&(nl.textContent="Mesh Convergence Results",rl.textContent=`Loading job ${o}...`)},t.ontoolresult=n=>{const r=n.content?.find(e=>e.type==="image");r&&(Cn=`data:${r.mimeType};base64,${r.data}`);const o=n.structuredContent;if(!o||typeof o!="object"){vt.textContent="The results explorer did not receive structured data.";return}No(o)},t.connect()}</script>
|
|
139
|
+
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:p({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:p({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(),Vf=p({method:c("ui/notifications/host-context-changed"),params:tl.describe("Partial context update containing only changed fields.")});p({method:c("ui/update-model-context"),params:p({content:I(ut).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.")})});p({method:c("ui/initialize"),params:p({appInfo:Nn.describe("App identification (name and version)."),appCapabilities:Ff.describe("Features and capabilities this app provides."),protocolVersion:l().describe("Protocol version this app supports.")})});var Wf=p({protocolVersion:l().describe('Negotiated protocol version string (e.g., "2025-11-21").'),hostInfo:Nn.describe("Host application identification and version."),hostCapabilities:qf.describe("Features and capabilities provided by the host."),hostContext:tl.describe("Rich context about the host environment.")}).passthrough(),Kf={target:"draft-2020-12"};async function wo(t,n){let r=t["~standard"];if(r.jsonSchema)return r.jsonSchema[n](Kf);if(r.vendor==="zod"){let{z:o}=await dl(()=>Promise.resolve().then(()=>Rm),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 So(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 Yi extends zf{_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:Rf,toolinputpartial:Zf,toolresult:Bf,toolcancelled:Cf,hostcontextchanged:Vf};static ONE_SHOT_EVENTS=new Set(["toolinput","toolinputpartial","toolresult","toolcancelled"]);_everHadListener=new Set;_assertHandlerTiming(n){if(!Yi.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(Tn,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=If(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(m){Object.assign(this,m),i()},remove(){e._registeredTools[n]===s&&(delete e._registeredTools[n],i())},handler:async(m,g)=>{if(!s.enabled)throw Error(`Tool ${n} is disabled`);let h;if(a){let d=s.inputSchema,b=d?await So(d,m??{},`Invalid input for tool ${n}: `):m??{};h=await o(b,g)}else h=await o(g);return s.outputSchema&&!h.isError&&(h.structuredContent=await So(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 wo(e.inputSchema,"input"):{type:"object",properties:{}}};return e.outputSchema&&(i.outputSchema=await wo(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(Lf,(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(Qu,(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(Gu,(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},Un,{onprogress:()=>{},resetTimeoutOnProgress:!0,...r})}async readServerResource(n,r){return this._assertInitialized("readServerResource"),await this.request({method:"resources/read",params:n},Vu,r)}async listServerResources(n,r){return this._assertInitialized("listServerResources"),await this.request({method:"resources/list",params:n},Bu,r)}async createSamplingMessage(n,r){this._assertInitialized("createSamplingMessage");let o=n.tools?el:Xu;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},Df,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},Ei,r)}openLink(n,r){return this._assertInitialized("openLink"),this.request({method:"ui/open-link",params:n},Pf,r)}sendOpenLink=this.openLink;downloadFile(n,r){return this._assertInitialized("downloadFile"),this.request({method:"ui/download-file",params:n},Ef,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},Hf,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 m=Math.ceil(a.getBoundingClientRect().height);a.style.height=s;let g=Math.ceil(window.innerWidth);(g!==r||m!==o)&&(r=g,o=m,this.sendSizeChanged({width:g,height:m}))}))};e();let i=new ResizeObserver(e);return i.observe(document.documentElement),i.observe(document.body),()=>i.disconnect()}async connect(n=new Nf(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:xf}},Wf,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}}}const Zn=["overview","distances","diagnostics","component"],Gf={overview:"Overview",distances:"Distances",diagnostics:"Diagnostics",component:"Component planes"};function zt(t){return t==="combined"?"overview":t==="overview_distances"||t.startsWith("KL_")||t.startsWith("EMD_")?"distances":t==="learning_curve"||t.startsWith("plot_vol")?"diagnostics":t.startsWith("divergence_")||t.startsWith("plot_")?"component":"diagnostics"}const Io={combined:0,overview_distances:1,KL_ref:10,KL_stepwise:11,KL_asymmetric:12,EMD_ref:13,EMD_stepwise:14,learning_curve:15,plot_vol_all_meshes:19,plot_vol_coarse_fine:20,divergence_kl:28,divergence_w1:29};function zo(t){return t in Io?Io[t]:t.startsWith("plot_vol")?25:t.startsWith("divergence_")?28:t.startsWith("plot_")?30:50}let ae=null,ue=null,re=null,Cn=null;const xo=new Map,vt=document.getElementById("loading"),Qf=document.getElementById("app"),nl=document.getElementById("title"),rl=document.getElementById("subtitle"),ft=document.getElementById("metadata-strip"),Yf=document.getElementById("metrics"),qe=document.getElementById("figure-select"),Xf=document.getElementById("figure-title"),eh=document.getElementById("figure-file"),th=document.getElementById("figure-caption"),ne=document.getElementById("main-image"),nh=document.getElementById("figure-empty"),ht=document.getElementById("open-standalone"),On=document.getElementById("open-monitor-btn"),ce=document.getElementById("copy-url"),jo=document.getElementById("related-monitor"),il=document.getElementById("download-current");function rh(t){return[...t].sort((n,r)=>Zn.indexOf(n.section??zt(n.key))-Zn.indexOf(r.section??zt(r.key))||zo(n.key)-zo(r.key)||n.label.localeCompare(r.label))}function ih(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 oh(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==="KL_asymmetric"?"Asymmetric mesh KL matrix (coarse↔fine)":n==="divergence_kl"?"Feature-plane symmetric KL (not mesh KL)":n==="divergence_w1"?"Feature-plane Wasserstein-1":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 ah(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 rh([...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`,m=/\.(png|svg)$/i.test(a),g=i.filter(h=>h==="pdf"||h==="svg");return{key:o,label:oh(a),filename:a,downloadFilename:s,formats:i,section:zt(o),caption:g.length>0?`High-quality ${g.join("/").toUpperCase()} available to download.`:void 0,url:m?`/api/results/${n}/image/${a}`:void 0,downloadUrl:`/api/results/${n}/image/${s}`}}))}function sh(t,n){const r=t.summary??{},o=t.label!=null?String(t.label):null,e=n.length>8?`${n.slice(0,8)}…`:n,i=r.grid??[],a=r.epochs??[],s=[o?`label ${o}`:null,`job ${e}`,String(r.job_type??"cfd_mesh_convergence")].filter(Boolean).join(" · "),m=[r.n_train_total!=null?`n_train ${r.n_train_total}`:null,r.reference_mesh!=null?`ref ${r.reference_mesh}`:null,i.length>=2?`grid ${i[0]}×${i[1]}`:null,a.length>=2?`epochs [${a[0]},${a[1]}]`:null,r.quantization_error!=null?`QE ${Number(r.quantization_error).toFixed(4)}`:null].filter(Boolean).join(" · ");return[s,m].filter(g=>g.length>0)}function ch(t,n){const r=t.summary??{},o=(r.files??[]).filter(d=>/\.(png|pdf|svg)$/i.test(d)),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",m=r.epochs??[],g=[{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:"Epochs",value:m.length>=2?`${m[0]}+${m[1]}`:"N/A"},{label:"QE",value:a},{label:"TE",value:s}],h=ah(o,n);return{type:"barmesh-results-explorer",jobId:n,title:"Mesh Convergence Results",subtitle:String(t.label??""),metadataStrip:sh(t,n),metrics:g,availableFigures:h,defaultFigureKey:ih(h),trainingMonitorUrl:`/viz/barmesh-training-monitor?mode=standalone&job_id=${encodeURIComponent(n)}`,standaloneUrl:`${window.location.origin}/viz/barmesh-results-explorer?mode=standalone&job_id=${encodeURIComponent(n)}`,relatedUrls:{trainingMonitor:`${window.location.origin}/viz/barmesh-training-monitor?mode=standalone&job_id=${encodeURIComponent(n)}`,resultsExplorer:`${window.location.origin}/viz/barmesh-results-explorer?mode=standalone&job_id=${encodeURIComponent(n)}`}}}function uh(t){if(!t||t.length===0){ft.style.display="none",ft.innerHTML="";return}ft.style.display="block",ft.innerHTML=t.map(n=>`<div class="metadata-line">${n}</div>`).join("")}function lh(t){Yf.innerHTML=t.map(n=>`<div class="metric"><span class="metric-label">${n.label}</span><span class="metric-value">${n.value}</span></div>`).join("")}function ol(t){const n=new Map;for(const r of t){const o=r.section??zt(r.key),e=n.get(o)??[];e.push(r),n.set(o,e)}qe.innerHTML="";for(const r of Zn){const o=n.get(r);if(!o||o.length===0)continue;const e=document.createElement("optgroup");e.label=Gf[r];for(const i of o){const a=document.createElement("option");a.value=i.key,a.textContent=`${i.label} (${i.filename})`,i.key===ue?.key&&(a.selected=!0),e.appendChild(a)}qe.appendChild(e)}}qe.addEventListener("change",()=>{const t=ae?.availableFigures.find(n=>n.key===qe.value);t&&al(t)});async function dh(t,n){const r=`${t}/${n}`,o=xo.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 xo.set(r,a),a}}catch{}return null}function mh(t){!re||!ae||re.updateModelContext({content:[{type:"text",text:`User is viewing the "${t.label}" figure (${t.filename}) of mesh convergence job ${ae.jobId}.`}]}).catch(()=>{})}function ph(t){return t.formats.length>1?` · formats: ${t.formats.join(", ")}`:""}function fh(t){const n=/\.(pdf|svg)$/i.test(t.downloadFilename);il.textContent=n?re?"Download PNG (preview)":`Download ${t.downloadFilename.split(".").pop()?.toUpperCase()} (high quality)`:"Download figure"}function hh(t){ne.style.display=t?"block":"none",nh.style.display=t?"none":"block"}async function al(t){ue=t,Xf.textContent=t.label,eh.textContent=`${t.filename}${ph(t)}`,th.textContent=t.caption??"",fh(t),mh(t),qe.value=t.key;let n=!1;if(t.url)ne.src=t.url,n=!0;else if(re&&ae){if(t.key===ae.defaultFigureKey&&Cn)ne.src=Cn,n=!0;else if(/\.(png|svg|jpe?g|webp)$/i.test(t.filename)){ne.removeAttribute("src");const r=await dh(ae.jobId,t.filename);r&&ue?.key===t.key&&(ne.src=r,n=!0)}}else/\.(png|svg|jpe?g|webp)$/i.test(t.filename)&&ne.removeAttribute("src");hh(n),ae&&ol(ae.availableFigures)}function gh(t){return t.standaloneUrl??window.location.href}function vh(t){const n=gh(t);t.standaloneUrl?(ht.href=t.standaloneUrl,ht.style.display="inline-flex"):cl&&(ht.href=window.location.href,ht.style.display="inline-flex");const r=t.relatedUrls?.trainingMonitor??t.trainingMonitorUrl??`/viz/barmesh-training-monitor?mode=standalone&job_id=${encodeURIComponent(t.jobId)}`;On.href=r,On.style.display="inline-flex",On.title="View live or completed training curves for this job",ce.style.display="inline-flex",ce.dataset.url=n;const o=t.relatedUrls?.trainingMonitor??t.trainingMonitorUrl;o&&(jo.href=o,jo.style.display="inline-flex")}function No(t){ae=t,nl.textContent=t.title,rl.textContent=t.subtitle?t.subtitle:`Job ${t.jobId}`,uh(t.metadataStrip),lh(t.metrics),vh(t);const n=t.availableFigures.find(r=>r.key===t.defaultFigureKey)??t.availableFigures[0]??null;ue=n,ol(t.availableFigures),n&&al(n),vt.style.display="none",Qf.style.display="block"}il.addEventListener("click",()=>{if(!ue)return;const t=document.createElement("a");if(ue.downloadUrl&&!re)t.href=ue.downloadUrl,t.download=ue.downloadFilename;else{const n=ne.src;if(!n)return;t.href=n,t.download=ue.filename}t.click()});ce.addEventListener("click",async()=>{const t=ce.dataset.url||ae?.standaloneUrl||window.location.href;try{await navigator.clipboard.writeText(t);const n=ce.textContent;ce.textContent="Copied!",setTimeout(()=>{ce.textContent=n??"Copy explorer URL"},1500)}catch{ce.textContent="Copy failed",setTimeout(()=>{ce.textContent="Copy explorer URL"},1500)}});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 sl=new URLSearchParams(window.location.search),cl=sl.get("mode")==="standalone",gt=sl.get("job_id");if(cl&>){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/${gt}`);if(o.status===404){vt.innerHTML=`Results for job ${gt} were not found (HTTP 404). Compute may have finished but cfd_finalize is still rendering figures — poll barmesh_jobs(status) until finalize completes. ${t}<br><button type="button" id="viz-retry-load" style="margin-top:0.75rem">Retry</button>`,document.getElementById("viz-retry-load")?.addEventListener("click",()=>location.reload());return}if(!o.ok)throw new Error(`HTTP ${o.status}`);const e=await o.json();No(ch(e,gt));return}catch(o){if(r<3){await new Promise(e=>setTimeout(e,1e3*r));continue}vt.innerHTML=`Could not load results from the local viz server (${o instanceof Error?o.message:"error"}). ${t}<br><button type="button" id="viz-retry-load" style="margin-top:0.75rem">Retry</button>`,document.getElementById("viz-retry-load")?.addEventListener("click",()=>location.reload())}})()}else{const t=new Yi({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&&(nl.textContent="Mesh Convergence Results",rl.textContent=`Loading job ${o}...`)},t.ontoolresult=n=>{const r=n.content?.find(e=>e.type==="image");r&&(Cn=`data:${r.mimeType};base64,${r.data}`);const o=n.structuredContent;if(!o||typeof o!="object"){vt.textContent="The results explorer did not receive structured data.";return}No(o)},t.connect()}</script>
|
|
140
140
|
</head>
|
|
141
141
|
<body>
|
|
142
142
|
<div id="loading">Loading mesh convergence results...</div>
|