Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions apps/web/src/features/runtimes/runtime-dashboard.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { describe, expect, it } from "vitest";

import { renderRuntimeDashboardHtml } from "./runtime-dashboard.js";
import type { RuntimeDashboardResult } from "./runtime-status-row.js";

const runtimeRows: RuntimeDashboardResult[] = [
runtime("codex", "OpenAI Codex CLI", "ready", {
path: "/usr/local/bin/codex",
version: "0.128.0",
}),
runtime("claude", "Claude Code", "configured", {
path: "/usr/local/bin/claude",
version: "1.2.3",
}),
runtime("ollama", "Ollama", "localProviderReady", {
path: "/usr/local/bin/ollama",
version: "0.9.0",
}),
runtime("lmstudio", "LM Studio", "ready", { path: "/usr/local/bin/lms", version: "0.3.0" }),
runtime("node", "Node.js", "ready", { path: "/usr/local/bin/node", version: "v24.1.0" }),
runtime("git", "Git", "ready", { path: "/usr/bin/git", version: "git version 2.50.0" }),
runtime("code", "Visual Studio Code CLI", "missing", {
warnings: ["missing with token=abc123"],
}),
];

describe("renderRuntimeDashboardHtml", () => {
it("renders Codex, Claude, Ollama, LM Studio, Node, Git, and VS Code in one table", () => {
const html = renderRuntimeDashboardHtml(runtimeRows);

for (const label of [
"OpenAI Codex CLI",
"Claude Code",
"Ollama",
"LM Studio",
"Node.js",
"Git",
"Visual Studio Code CLI",
]) {
expect(html).toContain(label);
}
expect(html).toContain("<table");
});

it("renders status, path, version, capabilities, and warnings", () => {
const html = renderRuntimeDashboardHtml(runtimeRows);

expect(html).toContain("localProviderReady");
expect(html).toContain("/usr/local/bin/codex");
expect(html).toContain("0.128.0");
expect(html).toContain("exec");
expect(html).toContain("chat");
expect(html).toContain("missing with token=[REDACTED]");
});

it("shows missing runtime guidance and avoids secret-like values", () => {
const html = renderRuntimeDashboardHtml(runtimeRows);

expect(html).toContain("Install or configure Visual Studio Code CLI");
expect(html).not.toContain("abc123");
});
});

function runtime(
type: RuntimeDashboardResult["type"],
name: string,
status: RuntimeDashboardResult["status"],
overrides: Partial<RuntimeDashboardResult> = {},
): RuntimeDashboardResult {
return {
id: type,
name,
type,
status,
detected: status !== "missing",
scope:
status === "localProviderReady"
? ["global", "localProvider"]
: status === "missing"
? []
: ["global"],
capabilities: {
exec: type === "codex" || type === "claude" || type === "node",
chat: type === "ollama" || type === "lmstudio",
versionCommand: status !== "missing",
},
warnings: [],
lastDetectedAt: "2026-05-29T00:00:00.000Z",
...overrides,
};
}
29 changes: 29 additions & 0 deletions apps/web/src/features/runtimes/runtime-dashboard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { renderRuntimeStatusRowHtml, type RuntimeDashboardResult } from "./runtime-status-row.js";

export type { RuntimeDashboardResult } from "./runtime-status-row.js";

export function renderRuntimeDashboardHtml(runtimes: readonly RuntimeDashboardResult[]): string {
return `<section class="runtime-dashboard" aria-label="Runtime dashboard">
<header class="runtime-dashboard-header">
<div>
<h2>Runtimes</h2>
<p>Local runtime detection across CLIs and local model providers.</p>
</div>
</header>
<table class="runtime-table">
<thead>
<tr>
<th scope="col">Runtime</th>
<th scope="col">Status</th>
<th scope="col">Path</th>
<th scope="col">Version</th>
<th scope="col">Capabilities</th>
<th scope="col">Warnings</th>
</tr>
</thead>
<tbody>
${runtimes.map(renderRuntimeStatusRowHtml).join("")}
</tbody>
</table>
</section>`;
}
73 changes: 73 additions & 0 deletions apps/web/src/features/runtimes/runtime-status-row.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
export interface RuntimeDashboardCapabilities {
readonly exec?: boolean;
readonly mcp?: boolean;
readonly chat?: boolean;
readonly versionCommand?: boolean;
readonly localProviders?: readonly string[];
}

export interface RuntimeDashboardResult {
readonly id: string;
readonly name: string;
readonly type: "codex" | "claude" | "ollama" | "lmstudio" | "node" | "git" | "code";
readonly status: "missing" | "ready" | "configured" | "projectActive" | "localProviderReady";
readonly detected: boolean;
readonly path?: string;
readonly version?: string;
readonly scope: readonly string[];
readonly capabilities: RuntimeDashboardCapabilities;
readonly warnings: readonly string[];
readonly lastDetectedAt: string;
}

export function renderRuntimeStatusRowHtml(runtime: RuntimeDashboardResult): string {
const capabilities = formatCapabilities(runtime.capabilities);
const warnings = runtime.warnings.map(redactSensitiveValues);
const guidance = renderMissingGuidance(runtime);
const warningContent = [...warnings, guidance].filter(Boolean);

return `<tr class="runtime-row runtime-${runtime.status}">
<th scope="row">${escapeHtml(runtime.name)}</th>
<td><span class="runtime-status">${runtime.status}</span></td>
<td>${escapeHtml(runtime.path ?? "Not detected")}</td>
<td>${escapeHtml(runtime.version ?? "Unknown")}</td>
<td>${escapeHtml(capabilities || "None")}</td>
<td>${warningContent.map(escapeHtml).join("<br>")}</td>
</tr>`;
}

function formatCapabilities(capabilities: RuntimeDashboardCapabilities): string {
return [
capabilities.exec ? "exec" : undefined,
capabilities.mcp ? "mcp" : undefined,
capabilities.chat ? "chat" : undefined,
capabilities.versionCommand ? "version" : undefined,
...(capabilities.localProviders ?? []),
]
.filter(Boolean)
.join(", ");
}

function renderMissingGuidance(runtime: RuntimeDashboardResult): string {
if (runtime.status !== "missing") {
return "";
}

return `Install or configure ${runtime.name}`;
}

function redactSensitiveValues(value: string): string {
return value
.replace(/\b(api[_-]?key\s*[=:]\s*)([^\s]+)/gi, "$1[REDACTED]")
.replace(/\b(token\s*[=:]\s*)([^\s]+)/gi, "$1[REDACTED]")
.replace(/\b(password\s*[=:]\s*)([^\s]+)/gi, "$1[REDACTED]")
.replace(/\b(secret\s*[=:]\s*)([^\s]+)/gi, "$1[REDACTED]");
}

function escapeHtml(value: string): string {
return value
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;");
}
6 changes: 6 additions & 0 deletions apps/web/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,9 @@ export {
export { renderInspectorHtml } from "./components/inspector.js";
export { renderSidebarHtml } from "./components/sidebar.js";
export type { NavItem } from "./components/sidebar.js";
export { renderRuntimeDashboardHtml } from "./features/runtimes/runtime-dashboard.js";
export { renderRuntimeStatusRowHtml } from "./features/runtimes/runtime-status-row.js";
export type {
RuntimeDashboardCapabilities,
RuntimeDashboardResult,
} from "./features/runtimes/runtime-status-row.js";
8 changes: 4 additions & 4 deletions docs/development/task-backlog.md
Original file line number Diff line number Diff line change
Expand Up @@ -445,10 +445,10 @@ Status markers:

**Tasks:**

- [ ] Render runtime name, status, path, version, capabilities, and warnings.
- [ ] Show install/configuration guidance for missing runtimes.
- [ ] Avoid rendering secret-like values.
- [ ] Commit with message `feat: add runtime dashboard ui`.
- [x] Render runtime name, status, path, version, capabilities, and warnings.
- [x] Show install/configuration guidance for missing runtimes.
- [x] Avoid rendering secret-like values.
- [x] Commit with message `feat: add runtime dashboard ui`.

**Acceptance Criteria:**

Expand Down