Skip to content

Commit bc4b5a3

Browse files
authored
fix: ux improvements (#37)
* feat(ui): inference --no-verify, OpenShell rebrand, NVIDIA green vars, fixed save bar, upgrade URL short name * Hide recommendation in policy tab page when there is no recommendation * feat(welcome-ui): add inference provider picker (NVIDIA + partners) in install flow * Fixed inference providers UI * fixed NCP welcome selection
1 parent d547453 commit bc4b5a3

29 files changed

+1234
-157
lines changed

brev/launch.sh

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -566,19 +566,23 @@
566566
}
567567

568568
set_inference_route() {
569+
# Set the default inference route to one model (nvidia-endpoints + kimi-k2.5).
570+
# Canonical CLI per brev/welcome-ui/SERVER_ARCHITECTURE.md: cluster inference set
571+
# (CLI_BIN is either "openshell" or "nemoclaw"; both accept the same subcommands.)
572+
# Try canonical first, then "inference set" as fallback for other CLI versions.
569573
log "Configuring inference route..."
570574

571-
if "$CLI_BIN" inference set --provider nvidia-endpoints --model minimaxai/minimax-m2.5 >/dev/null 2>&1; then
572-
log "Configured inference via '$CLI_BIN inference set'."
575+
if "$CLI_BIN" cluster inference set --provider nvidia-endpoints --model moonshotai/kimi-k2.5 --no-verify >/dev/null 2>&1; then
576+
log "Configured inference via '$CLI_BIN cluster inference set'."
573577
return
574578
fi
575579

576-
if "$CLI_BIN" cluster inference set --provider nvidia-endpoints --model minimaxai/minimax-m2.5 >/dev/null 2>&1; then
577-
log "Configured inference via legacy '$CLI_BIN cluster inference set'."
580+
if "$CLI_BIN" inference set --provider nvidia-endpoints --model moonshotai/kimi-k2.5 --no-verify >/dev/null 2>&1; then
581+
log "Configured inference via '$CLI_BIN inference set'."
578582
return
579583
fi
580584

581-
log "Unable to configure inference route with either current or legacy CLI commands."
585+
log "Unable to configure inference route with either 'cluster inference set' or 'inference set'."
582586
exit 1
583587
}
584588

brev/welcome-ui/SERVER_ARCHITECTURE.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,8 @@ The server operates in **two distinct modes** depending on sandbox readiness:
126126
| `LOG_FILE` | `/tmp/nemoclaw-sandbox-create.log` | Sandbox creation log (written by subprocess) |
127127
| `PROVIDER_CONFIG_CACHE` | `/tmp/nemoclaw-provider-config-cache.json` | Provider config values cache |
128128
| `OTHER_AGENTS_YAML` | `ROOT/other-agents.yaml` | YAML modal definition file |
129+
| `INFERENCE_PROVIDERS_YAML` | `ROOT/inference-providers.yaml` | Inference provider picker and per-partner instructions |
130+
| `NCP_LOGOS_DIR` | `SANDBOX_DIR/ncp-logos` | Partner and NVIDIA logos served at `/ncp-logos/*` |
129131
| `NEMOCLAW_IMAGE` | `ghcr.io/nvidia/openshell-community/sandboxes/openclaw-nvidia:local` | Optional image override |
130132
| `SANDBOX_PORT` | `18789` | Port the sandbox listens on (localhost) |
131133

@@ -845,6 +847,21 @@ steps: # Array of instruction sections
845847

846848
4. **Fallback:** If YAML fails to parse or PyYAML is not installed, the placeholder is replaced with an HTML comment: `<!-- other-agents.yaml not available -->`
847849

850+
### Inference Provider Picker (`inference-providers.yaml`)
851+
852+
The server also renders `inference-providers.yaml` into HTML and injects it into `index.html`, replacing the `{{INFERENCE_PROVIDER_PICKER}}` placeholder. This provides:
853+
854+
- **Picker screen:** "Choose your inference provider" with NVIDIA (free) in its own row and paid partners in a 5×2 grid. Each tile has `data-provider-id` for JS.
855+
- **Partner instruction blocks:** For each partner, a `div#provider-instructions-{id}` with title, intro, and steps (same schema as other-agents steps). Shown when the user clicks a partner.
856+
857+
**YAML schema:** Top-level `nvidia: { displayName, logoFile }` and `partners: [ { id, name, logoFile, instructions: { title, intro?, steps[] } } ]`. Logo filenames refer to files under `NCP_LOGOS_DIR`, served at `GET /ncp-logos/<filename>`.
858+
859+
**Fallback:** If the file is missing or invalid, the placeholder is replaced with `<!-- inference-providers.yaml not available -->` and the Install OpenClaw modal shows only the NVIDIA API key view (no picker).
860+
861+
### NCP Logos Route (`GET /ncp-logos/*`)
862+
863+
In welcome-ui mode (before sandbox is ready), `GET /ncp-logos/<file>` serves static files from `SANDBOX_DIR/ncp-logos/`. Path traversal is rejected; only files under that directory are served. Used for NVIDIA and partner logos in the provider picker. MIME type for `.webp` is `image/webp`.
864+
848865
---
849866

850867
## 9. Policy Management Pipeline
@@ -1146,6 +1163,8 @@ All CLI commands are executed via `subprocess.run()` or `subprocess.Popen()`. Ev
11461163
|------|------|----------|
11471164
| `ROOT/index.html` | First request to `/` | Yes |
11481165
| `ROOT/other-agents.yaml` | First request to `/` | No (graceful fallback) |
1166+
| `ROOT/inference-providers.yaml` | First request to `/` | No (graceful fallback) |
1167+
| `SANDBOX_DIR/ncp-logos/*` | `GET /ncp-logos/<file>` | No (optional logos) |
11491168
| `ROOT/styles.css` | Static file serving | Yes (for UI) |
11501169
| `ROOT/app.js` | Static file serving | Yes (for UI) |
11511170
| `SANDBOX_DIR/policy.yaml` | Sandbox creation | No (graceful fallback) |

brev/welcome-ui/__tests__/cluster-inference.test.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ describe("POST /api/cluster-inference", () => {
151151
expect(res.status).toBe(400);
152152
});
153153

154-
it("TC-CI10: calls nemoclaw cluster inference set with --provider and --model", async () => {
154+
it("TC-CI10: calls CLI (openshell or nemoclaw) with cluster inference set or inference set, --provider, --model, --no-verify", async () => {
155155
execFile.mockImplementation((cmd, args, opts, cb) => {
156156
if (typeof opts === "function") { cb = opts; opts = {}; }
157157
cb(null, "", "");
@@ -162,13 +162,14 @@ describe("POST /api/cluster-inference", () => {
162162
.send({ providerName: "test-prov", modelId: "test-model" });
163163

164164
const setCall = execFile.mock.calls.find(
165-
(c) => c[0] === "nemoclaw" && c[1]?.includes("inference") && c[1]?.includes("set")
165+
(c) => (c[0] === "openshell" || c[0] === "nemoclaw") && c[1]?.includes("inference") && c[1]?.includes("set")
166166
);
167167
expect(setCall).toBeDefined();
168168
const args = setCall[1];
169169
expect(args).toContain("--provider");
170170
expect(args).toContain("test-prov");
171171
expect(args).toContain("--model");
172172
expect(args).toContain("test-model");
173+
expect(args).toContain("--no-verify");
173174
});
174175
});

brev/welcome-ui/__tests__/template-render.test.js

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,13 @@
33

44
import { describe, it, expect, beforeEach } from 'vitest';
55
import serverModule from '../server.js';
6-
const { renderOtherAgentsModal, getRenderedIndex, escapeHtml, _resetForTesting } = serverModule;
6+
const {
7+
renderOtherAgentsModal,
8+
renderInferenceProviderPickerAndInstructions,
9+
getRenderedIndex,
10+
escapeHtml,
11+
_resetForTesting,
12+
} = serverModule;
713

814
// === TC-T01 through TC-T14: YAML-to-HTML template rendering ===
915

@@ -88,6 +94,26 @@ describe("renderOtherAgentsModal", () => {
8894
});
8995
});
9096

97+
describe("renderInferenceProviderPickerAndInstructions", () => {
98+
it("TC-T15: picker contains NVIDIA row and heading when YAML present", () => {
99+
const html = renderInferenceProviderPickerAndInstructions();
100+
if (!html) return;
101+
expect(html).toContain("Choose your inference provider");
102+
expect(html).toContain('data-provider-id="nvidia"');
103+
expect(html).toContain("Free endpoint provider");
104+
});
105+
106+
it("TC-T16: picker contains partner grid and provider-instructions blocks", () => {
107+
const html = renderInferenceProviderPickerAndInstructions();
108+
if (!html) return;
109+
expect(html).toContain("provider-picker__grid");
110+
expect(html).toContain("install-partner-instructions");
111+
expect(html).toContain("partner-instructions-content");
112+
expect(html).toContain("provider-instructions-");
113+
expect(html).toContain("Back to providers");
114+
});
115+
});
116+
91117
describe("getRenderedIndex", () => {
92118
beforeEach(() => {
93119
_resetForTesting();
@@ -112,4 +138,16 @@ describe("getRenderedIndex", () => {
112138
const hasComment = html.includes("<!-- other-agents.yaml not available -->");
113139
expect(hasModal || hasComment).toBe(true);
114140
});
141+
142+
it("TC-T17: {{INFERENCE_PROVIDER_PICKER}} is replaced in index.html", () => {
143+
const html = getRenderedIndex();
144+
expect(html).not.toContain("{{INFERENCE_PROVIDER_PICKER}}");
145+
});
146+
147+
it("TC-T18: inference picker replaced with content or fallback comment", () => {
148+
const html = getRenderedIndex();
149+
const hasPicker = html.includes("install-provider-picker");
150+
const hasComment = html.includes("<!-- inference-providers.yaml not available -->");
151+
expect(hasPicker || hasComment).toBe(true);
152+
});
115153
});

brev/welcome-ui/app.js

Lines changed: 83 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414

1515
// Install modal elements
1616
const installMain = $("#install-main");
17+
const installNvidiaKey = $("#install-nvidia-key");
18+
const installProviderPicker = $("#install-provider-picker");
19+
const installPartnerInstructions = $("#install-partner-instructions");
1720
const stepError = $("#install-step-error");
1821
const apiKeyInput = $("#api-key-input");
1922
const toggleKeyVis = $("#toggle-key-vis");
@@ -233,6 +236,24 @@
233236
stepError.hidden = true;
234237
}
235238

239+
// -- Install modal screens (picker / NVIDIA / partner) -----------------
240+
241+
function showInstallScreen(screen, providerId) {
242+
if (installProviderPicker) installProviderPicker.hidden = screen !== "picker";
243+
if (installNvidiaKey) installNvidiaKey.hidden = screen !== "nvidia";
244+
if (installPartnerInstructions) {
245+
installPartnerInstructions.hidden = screen !== "partner";
246+
if (screen === "partner" && providerId) {
247+
const content = $("#partner-instructions-content");
248+
if (content) {
249+
content.querySelectorAll("[id^=\"provider-instructions-\"]").forEach((el) => {
250+
el.hidden = el.id !== "provider-instructions-" + providerId;
251+
});
252+
}
253+
}
254+
}
255+
}
256+
236257
function showError(msg) {
237258
stopPolling();
238259
installMain.hidden = true;
@@ -387,6 +408,7 @@
387408
updateButtonState();
388409

389410
showOverlay(overlayInstall);
411+
showInstallScreen("nvidia");
390412
if (!keyInjected) {
391413
startPolling();
392414
}
@@ -398,6 +420,7 @@
398420
updateButtonState();
399421

400422
showOverlay(overlayInstall);
423+
showInstallScreen("nvidia");
401424
startPolling();
402425
}
403426
} catch {
@@ -425,13 +448,19 @@
425448
showOverlay(overlayInstall);
426449
if (installFailed) {
427450
stepError.hidden = false;
428-
installMain.hidden = true;
451+
if (installProviderPicker) installProviderPicker.hidden = true;
452+
if (installNvidiaKey) installNvidiaKey.hidden = true;
453+
if (installPartnerInstructions) installPartnerInstructions.hidden = true;
429454
} else {
430-
showMainView();
455+
if (installProviderPicker) {
456+
showInstallScreen("picker");
457+
} else {
458+
showInstallScreen("nvidia");
459+
apiKeyInput.focus();
460+
if (!installTriggered) triggerInstall();
461+
}
431462
}
432-
apiKeyInput.focus();
433463
updateButtonState();
434-
if (!installTriggered && !installFailed) triggerInstall();
435464
});
436465

437466
cardOther.addEventListener("click", () => {
@@ -442,6 +471,56 @@
442471
closeInstall.addEventListener("click", () => hideOverlay(overlayInstall));
443472
closeInstr.addEventListener("click", () => hideOverlay(overlayInstr));
444473

474+
const backFromNvidia = $("#back-from-nvidia");
475+
if (backFromNvidia) {
476+
backFromNvidia.addEventListener("click", () => showInstallScreen("picker"));
477+
}
478+
document.addEventListener("click", (e) => {
479+
const backPartner = e.target.closest("#back-from-partner");
480+
if (backPartner) showInstallScreen("picker");
481+
});
482+
483+
function handleProviderChoice(id) {
484+
if (id === "nvidia") {
485+
showInstallScreen("nvidia");
486+
apiKeyInput.focus();
487+
if (!installTriggered && !installFailed) triggerInstall();
488+
} else {
489+
showInstallScreen("partner", id);
490+
}
491+
}
492+
493+
function onProviderPickerClick(e) {
494+
const tile = e.target.closest("[data-provider-id]");
495+
if (!tile) return;
496+
const id = tile.getAttribute("data-provider-id");
497+
if (!id) return;
498+
e.preventDefault();
499+
e.stopPropagation();
500+
handleProviderChoice(id);
501+
}
502+
503+
function onProviderPickerKeydown(e) {
504+
if (e.key !== "Enter" && e.key !== " ") return;
505+
const tile = e.target.closest("[data-provider-id]");
506+
if (!tile) return;
507+
const id = tile.getAttribute("data-provider-id");
508+
if (!id) return;
509+
e.preventDefault();
510+
handleProviderChoice(id);
511+
}
512+
513+
if (installProviderPicker) {
514+
installProviderPicker.addEventListener("click", onProviderPickerClick);
515+
installProviderPicker.addEventListener("keydown", onProviderPickerKeydown);
516+
} else {
517+
const installBody = $("#install-body");
518+
if (installBody) {
519+
installBody.addEventListener("click", onProviderPickerClick);
520+
installBody.addEventListener("keydown", onProviderPickerKeydown);
521+
}
522+
}
523+
445524
closeOnBackdrop(overlayInstall);
446525
closeOnBackdrop(overlayInstr);
447526

brev/welcome-ui/index.html

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
<title>OpenShell — Agent Sandbox</title>
77
<link rel="icon" type="image/svg+xml" href="/OpenShell-Icon.svg">
88
<link rel="alternate icon" href="/favicon.ico" sizes="any">
9-
<link rel="stylesheet" href="styles.css?v=7">
9+
<link rel="stylesheet" href="styles.css?v=8">
1010
<link rel="preconnect" href="https://fonts.googleapis.com">
1111
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
1212
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
@@ -82,7 +82,11 @@ <h3 class="modal__title">Install OpenClaw</h3>
8282
</div>
8383
<div class="modal__body" id="install-body">
8484

85-
<div id="install-main">
85+
{{INFERENCE_PROVIDER_PICKER}}
86+
87+
<div id="install-nvidia-key" class="install-nvidia-key" hidden>
88+
<button type="button" class="provider-picker__back" id="back-from-nvidia">Back to providers</button>
89+
<div id="install-main">
8690
<!-- Zone A: Active user input — interactive from second zero -->
8791
<div class="zone-input">
8892
<p class="zone-input__desc">
@@ -139,6 +143,7 @@ <h3 class="modal__title">Install OpenClaw</h3>
139143
<span id="btn-launch-label">Waiting for API key...</span>
140144
<svg class="btn__external-icon" viewBox="0 0 24 24" fill="none"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/><polyline points="15 3 21 3 21 9"/><line x1="10" x2="21" y1="14" y2="3"/></svg>
141145
</button>
146+
</div>
142147
</div>
143148

144149
<!-- Error state -->
@@ -164,6 +169,6 @@ <h4 class="error-card__title">Something went wrong</h4>
164169
OpenShell Sandbox &middot; NVIDIA
165170
</footer>
166171

167-
<script src="app.js?v=12"></script>
172+
<script src="app.js?v=13"></script>
168173
</body>
169174
</html>

0 commit comments

Comments
 (0)