diff --git a/docs/specs/2026-09-05-logged-in-ui-shell-design.md b/docs/specs/2026-09-05-logged-in-ui-shell-design.md
index ec1f130f..b4a042f6 100644
--- a/docs/specs/2026-09-05-logged-in-ui-shell-design.md
+++ b/docs/specs/2026-09-05-logged-in-ui-shell-design.md
@@ -132,9 +132,9 @@ URLs are unchanged: `/chat`, `/chat/$threadId`, `/dashboards`, `/dashboards/$das
|---|---|---|
| Embedded views render in the system font, eight console errors per view | `ui/host/src/mcp-app-frame.tsx` `mcpAppCSP` | always emit `font-src 'self' data:` |
| "994.6 Ms" | `ui/host/src/dashboard.tsx` `Metric` | drop `tt="capitalize"`, format with `duration()` |
-| 404 on every new chat | `ui/host/src/App.tsx` thread load effect | draft pane, fetch only existing threads |
-| Header clips at 390px | `ui/host/src/App.tsx` header | header reduced to four controls, rail in drawer |
-| Hard-coded "Live" dot | `ui/host/src/App.tsx` header | removed |
+| 404 on every new chat | `ui/host/src/app.tsx` thread load effect | draft pane, fetch only existing threads |
+| Header clips at 390px | `ui/host/src/app.tsx` header | header reduced to four controls, rail in drawer |
+| Hard-coded "Live" dot | `ui/host/src/app.tsx` header | removed |
| Dashboard polls every 3s | `ui/host/src/dashboard.tsx` | 30s |
## 6. Testing and gates
diff --git a/ui/host/build/apps.ts b/ui/host/build/apps.ts
index 257fa7e0..ca8cf16a 100644
--- a/ui/host/build/apps.ts
+++ b/ui/host/build/apps.ts
@@ -10,7 +10,7 @@ export function appBuildBoundary(): Plugin {
const walk = (id: string, chain: string[]) => {
if (seen.has(id)) return; seen.add(id);
const path = id.replaceAll("\\", "/").split("?")[0];
- if (/\/src\/(?:auth[\w-]*(?:\.[jt]sx?$|\/)|router(?:\.[jt]sx?|\/)|routes\/|(?:App|main)\.[jt]sx?$|(?:app-context|provider-context)(?:\.[jt]sx?|\/)|(?:app|providers?)\/|(?:api|observability|mcp-app-frame)\.[jt]sx?$|dashboards\/api\.[jt]sx?$)/.test(path)) this.error(`Forbidden MCP app import: ${[...chain, id].join(" -> ")}`);
+ if (/\/src\/(?:auth[\w-]*(?:\.[jt]sx?$|\/)|router(?:\.[jt]sx?|\/)|routes\/|(?:app|main)\.[jt]sx?$|(?:app-context|provider-context)(?:\.[jt]sx?|\/)|(?:app|providers?)\/|(?:api|observability|mcp-app-frame)\.[jt]sx?$|dashboards\/api\.[jt]sx?$)/.test(path)) this.error(`Forbidden MCP app import: ${[...chain, id].join(" -> ")}`);
const info = this.getModuleInfo(id);
for (const next of [...(info?.importedIds ?? []), ...(info?.dynamicallyImportedIds ?? [])]) walk(next, [...chain, id]);
};
diff --git a/ui/host/src/answer-only.test.tsx b/ui/host/src/answer-only.test.tsx
index 640bbc30..f3d3238d 100644
--- a/ui/host/src/answer-only.test.tsx
+++ b/ui/host/src/answer-only.test.tsx
@@ -4,7 +4,7 @@ import { createMemoryHistory, createRootRoute, createRoute, createRouter, Router
import { act } from "react";
import { createRoot } from "react-dom/client";
import { afterEach, expect, it, vi } from "vitest";
-import App from "./App";
+import App from "./app";
import { useFanoutApp } from "./app-context";
vi.mock("./auth", () => ({
diff --git a/ui/host/src/app.test.tsx b/ui/host/src/app.test.tsx
index 554e7efd..457c9be7 100644
--- a/ui/host/src/app.test.tsx
+++ b/ui/host/src/app.test.tsx
@@ -5,7 +5,7 @@ import { createMemoryHistory, createRootRoute, createRoute, createRouter, Router
import { act } from "react";
import { createRoot } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
-import App from "./App";
+import App from "./app";
import { ChatPage } from "./chat";
import { createDashboardPrompt, useFanoutApp } from "./app-context";
import { parseSearch, toSearchParams } from "./dashboards/search";
diff --git a/ui/host/src/App.tsx b/ui/host/src/app.tsx
similarity index 100%
rename from ui/host/src/App.tsx
rename to ui/host/src/app.tsx
diff --git a/ui/host/src/chat.test.tsx b/ui/host/src/chat.test.tsx
index 3064f817..6858bb83 100644
--- a/ui/host/src/chat.test.tsx
+++ b/ui/host/src/chat.test.tsx
@@ -5,7 +5,7 @@ import { createRoot } from "react-dom/client";
import { afterEach, describe, expect, it, vi } from "vitest";
import { FanoutAppContext, useFanoutApp, type FanoutAppContextValue } from "./app-context";
import { createRootRoute, createRoute, createRouter, RouterProvider } from "@tanstack/react-router";
-import App from "./App";
+import App from "./app";
import { ChatPage } from "./chat";
import { saved as receiptFixture, build as receiptBuild, user, call, result } from "../tests/dashboard-receipts";
import { fixture } from "../tests/fixtures";
diff --git a/ui/host/src/dashboard-change-chips.tsx b/ui/host/src/dashboard-change-chips.tsx
index 02185522..bc091e52 100644
--- a/ui/host/src/dashboard-change-chips.tsx
+++ b/ui/host/src/dashboard-change-chips.tsx
@@ -1,6 +1,6 @@
import { Badge, Group } from "@mantine/core";
-export function EditChips({ labels }: { labels: string[] }) {
+export function DashboardChangeChips({ labels }: { labels: string[] }) {
if (!labels.length) return null;
return {labels.map((label, index) => {label})};
}
diff --git a/ui/host/src/dashboard-receipt-view.tsx b/ui/host/src/dashboard-receipt-view.tsx
index f6ff0be8..007c66eb 100644
--- a/ui/host/src/dashboard-receipt-view.tsx
+++ b/ui/host/src/dashboard-receipt-view.tsx
@@ -2,7 +2,7 @@ import { Button, Collapse, Group, Loader, Paper, Stack, Text } from "@mantine/co
import { Link } from "@tanstack/react-router";
import { useId, useState } from "react";
import { changeChips, stageNames, type BuildReceipt } from "./dashboard-receipt";
-import { EditChips } from "./dashboard-change-chips";
+import { DashboardChangeChips } from "./dashboard-change-chips";
const seconds = (ms: number) => `${(ms / 1000).toFixed(1)} s`;
export function DashboardReceiptView({ receipt, running = false, activity = "", awaitingAnswer = false }: { receipt: BuildReceipt; running?: boolean; activity?: string; awaitingAnswer?: boolean }) {
@@ -27,7 +27,7 @@ export function DashboardReceiptView({ receipt, running = false, activity = "",
{running && awaitingAnswer && {activity || "Analyzing your system"}}
-
+
{receipt.explanations.map(text => running ? text.split(" · ").filter(part => !part.endsWith(": incomplete")).join(" · ") : text).filter(Boolean).map((text,i) => {text})}
{saved && }
diff --git a/ui/host/src/dashboards/chart-keyboard.test.tsx b/ui/host/src/dashboards/chart-keyboard.test.tsx
index 79922ff9..22bb915f 100644
--- a/ui/host/src/dashboards/chart-keyboard.test.tsx
+++ b/ui/host/src/dashboards/chart-keyboard.test.tsx
@@ -15,7 +15,7 @@ import { FragmentView } from './fragment-view';
import { fixture } from '../../tests/fixtures';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { TableViz } from './viz/table';
-import { Waterfall } from './trace/trace-components';
+import { Waterfall } from './trace/waterfall';
import { traceFixture } from '../../tests/fixtures';
const chart=vi.hoisted(()=>({setOption:vi.fn(),dispatchAction:vi.fn(),dispose:vi.fn(),on:vi.fn(),resize:vi.fn()}));
diff --git a/ui/host/src/dashboards/history.tsx b/ui/host/src/dashboards/history.tsx
index 9560b58c..1abfc2d0 100644
--- a/ui/host/src/dashboards/history.tsx
+++ b/ui/host/src/dashboards/history.tsx
@@ -3,7 +3,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useEffect, useRef, useState } from "react";
import { exactTimestamp } from "../../../format";
import { changeChips } from "../dashboard-receipt";
-import { EditChips } from "../dashboard-change-chips";
+import { DashboardChangeChips } from "../dashboard-change-chips";
import { dashboardDataPredicate } from "./cache";
import { ApiError, dashboardsKey, getVersion, listVersions, restoreVersion, type DashboardRecord, type VersionInfo, type VersionRecord } from "./api";
@@ -136,7 +136,7 @@ function VersionDetails({ record, currentVersion, pending, onRestore }: { record
{dashboard.description && {dashboard.description}}
{record.changes_available ? <>
-
+
{!labels.length && !record.layout_changed && No authored changes.}
> : The preceding version is no longer retained. This saved spec is still available.}
Read-only · {dashboard.spec.panels.length} panels. Historical data is not queried.
diff --git a/ui/host/src/dashboards/trace/detail.tsx b/ui/host/src/dashboards/trace/detail.tsx
index 27124f79..6c953296 100644
--- a/ui/host/src/dashboards/trace/detail.tsx
+++ b/ui/host/src/dashboards/trace/detail.tsx
@@ -7,7 +7,7 @@ import { filledTextOn } from "../../../../theme";
import { duration } from "../../../../format";
import { EmptyState } from "./components";
import { ListBullets } from "@phosphor-icons/react";
-import { Waterfall } from "./trace-components";
+import { Waterfall } from "./waterfall";
import { LogsViz } from "../viz/logs";
import type { PanelResult } from "../../../../panels/types";
diff --git a/ui/host/src/dashboards/trace/trace-components.tsx b/ui/host/src/dashboards/trace/waterfall.tsx
similarity index 100%
rename from ui/host/src/dashboards/trace/trace-components.tsx
rename to ui/host/src/dashboards/trace/waterfall.tsx
diff --git a/ui/host/src/routes/__root.tsx b/ui/host/src/routes/__root.tsx
index 968a73d6..42955a31 100644
--- a/ui/host/src/routes/__root.tsx
+++ b/ui/host/src/routes/__root.tsx
@@ -1,5 +1,5 @@
import { createRootRoute, Navigate } from "@tanstack/react-router";
-import App from "../App";
+import App from "../app";
export const Route = createRootRoute({
component: App,
diff --git a/ui/host/tests/build-graph.test.ts b/ui/host/tests/build-graph.test.ts
index b24be248..0e74e8f8 100644
--- a/ui/host/tests/build-graph.test.ts
+++ b/ui/host/tests/build-graph.test.ts
@@ -6,7 +6,7 @@ import { build } from "vite";
import { expect, it } from "vitest";
import { appBuildBoundary, blobWorkerOnly } from "../build/apps";
-it.each(["auth.ts", "auth-session.ts", "api.ts", "observability.ts", "mcp-app-frame.tsx", "router.ts", "routes/index.ts", "App.tsx", "main.tsx", "app-context.tsx", "provider-context.tsx", "dashboards/api.ts"].flatMap(file => [false, true].map(transitive => ({ file, transitive }))))("rejects forbidden runtime imports: $file transitive=$transitive", async ({ file, transitive }) => {
+it.each(["auth.ts", "auth-session.ts", "api.ts", "observability.ts", "mcp-app-frame.tsx", "router.ts", "routes/index.ts", "app.tsx", "main.tsx", "app-context.tsx", "provider-context.tsx", "dashboards/api.ts"].flatMap(file => [false, true].map(transitive => ({ file, transitive }))))("rejects forbidden runtime imports: $file transitive=$transitive", async ({ file, transitive }) => {
const root = mkdtempSync(resolve(tmpdir(), "fanout-graph-"));
try {
mkdirSync(resolve(root, "src/mcp-apps"), { recursive: true }); mkdirSync(resolve(root, "src/dashboards"));