diff --git a/apps/web/src/features/runtimes/runtime-dashboard.test.ts b/apps/web/src/features/runtimes/runtime-dashboard.test.ts
new file mode 100644
index 0000000..1a8b481
--- /dev/null
+++ b/apps/web/src/features/runtimes/runtime-dashboard.test.ts
@@ -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("
{
+ 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 {
+ 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,
+ };
+}
diff --git a/apps/web/src/features/runtimes/runtime-dashboard.tsx b/apps/web/src/features/runtimes/runtime-dashboard.tsx
new file mode 100644
index 0000000..eb9ce85
--- /dev/null
+++ b/apps/web/src/features/runtimes/runtime-dashboard.tsx
@@ -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 `
+
+
+
+
+ | Runtime |
+ Status |
+ Path |
+ Version |
+ Capabilities |
+ Warnings |
+
+
+
+ ${runtimes.map(renderRuntimeStatusRowHtml).join("")}
+
+
+`;
+}
diff --git a/apps/web/src/features/runtimes/runtime-status-row.tsx b/apps/web/src/features/runtimes/runtime-status-row.tsx
new file mode 100644
index 0000000..79bb17a
--- /dev/null
+++ b/apps/web/src/features/runtimes/runtime-status-row.tsx
@@ -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 `
+ | ${escapeHtml(runtime.name)} |
+ ${runtime.status} |
+ ${escapeHtml(runtime.path ?? "Not detected")} |
+ ${escapeHtml(runtime.version ?? "Unknown")} |
+ ${escapeHtml(capabilities || "None")} |
+ ${warningContent.map(escapeHtml).join(" ")} |
+
`;
+}
+
+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("&", "&")
+ .replaceAll("<", "<")
+ .replaceAll(">", ">")
+ .replaceAll('"', """);
+}
diff --git a/apps/web/src/index.ts b/apps/web/src/index.ts
index 8af70a2..4852018 100644
--- a/apps/web/src/index.ts
+++ b/apps/web/src/index.ts
@@ -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";
diff --git a/docs/development/task-backlog.md b/docs/development/task-backlog.md
index e879e56..204d98d 100644
--- a/docs/development/task-backlog.md
+++ b/docs/development/task-backlog.md
@@ -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:**