diff --git a/studio/frontend/src/features/api-monitor/api-monitor-overlay.tsx b/studio/frontend/src/features/api-monitor/api-monitor-overlay.tsx index a9384792d4..93d774cf7e 100644 --- a/studio/frontend/src/features/api-monitor/api-monitor-overlay.tsx +++ b/studio/frontend/src/features/api-monitor/api-monitor-overlay.tsx @@ -24,7 +24,7 @@ import { useRef, useState, } from "react"; -import { isLifecycleEntry, lifecycleLabel } from "./api-monitor-page"; +import { isLifecycleEntry, lifecycleLabel } from "./lifecycle"; import { useApiMonitorOverlayStore } from "./overlay-store"; import { computeStats } from "./use-api-monitor"; diff --git a/studio/frontend/src/features/api-monitor/api-monitor-page.tsx b/studio/frontend/src/features/api-monitor/api-monitor-page.tsx index 502eda0063..4d998c6f88 100644 --- a/studio/frontend/src/features/api-monitor/api-monitor-page.tsx +++ b/studio/frontend/src/features/api-monitor/api-monitor-page.tsx @@ -41,6 +41,7 @@ import { import { HugeiconsIcon } from "@hugeicons/react"; import { type ReactElement, useEffect, useMemo, useRef, useState } from "react"; import { SavedModelSettingsPanel } from "./components/saved-model-settings"; +import { isLifecycleEntry, lifecycleLabel } from "./lifecycle"; import { type MonitorStatusFilter, filterEntries, @@ -86,40 +87,6 @@ function compactEndpoint(endpoint: string): string { .replace(V1_PREFIX_RE, "/"); } -// A lifecycle row is a model load/unload/download, not an HTTP call: it carries -// an event and reason instead of a prompt, so there is no payload to expand. -export function isLifecycleEntry(entry: ApiMonitorEntry): boolean { - return entry.kind === "lifecycle"; -} - -export function lifecycleLabel(entry: ApiMonitorEntry): string { - if (entry.event === "unload") { - return entry.reason === "idle" ? "Model unloaded (idle)" : "Model unloaded"; - } - if (entry.event === "download") { - if (entry.status === "running") { - const pct = entry.progress; - return typeof pct === "number" - ? `Downloading model (${Math.round(pct)}%)` - : "Downloading model"; - } - if (entry.status === "completed") { - return "Model downloaded"; - } - // A cancel is deliberate, so saying it failed misreads the user's own action. - return entry.status === "cancelled" - ? "Model download cancelled" - : "Model download failed"; - } - if (entry.status === "running") { - return "Loading model"; - } - if (entry.status === "completed") { - return "Model loaded"; - } - return "Model load failed"; -} - function statusDotClass(status: ApiMonitorEntry["status"]): string { switch (status) { case "running": diff --git a/studio/frontend/src/features/api-monitor/lifecycle.ts b/studio/frontend/src/features/api-monitor/lifecycle.ts new file mode 100644 index 0000000000..70e31df6fa --- /dev/null +++ b/studio/frontend/src/features/api-monitor/lifecycle.ts @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: AGPL-3.0-only +// Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0 + +// Labels for model load/unload/download rows, shared by the overlay and the +// full page. +// +// They live here rather than on the page because the overlay is mounted from +// __root.tsx, so importing them from the page would pull the whole page and its +// dependency graph into the eagerly loaded bundle and undo the route's +// lazyRouteComponent: every route would pay for the monitor page even when it +// is never opened. + +import type { ApiMonitorEntry } from "@/features/chat/types/api"; + +// A lifecycle row is a model load/unload/download, not an HTTP call: it carries +// an event and reason instead of a prompt, so there is no payload to expand. +export function isLifecycleEntry(entry: ApiMonitorEntry): boolean { + return entry.kind === "lifecycle"; +} + +export function lifecycleLabel(entry: ApiMonitorEntry): string { + if (entry.event === "unload") { + return entry.reason === "idle" ? "Model unloaded (idle)" : "Model unloaded"; + } + if (entry.event === "download") { + if (entry.status === "running") { + const pct = entry.progress; + return typeof pct === "number" + ? `Downloading model (${Math.round(pct)}%)` + : "Downloading model"; + } + if (entry.status === "completed") { + return "Model downloaded"; + } + // A cancel is deliberate, so saying it failed misreads the user's own action. + return entry.status === "cancelled" + ? "Model download cancelled" + : "Model download failed"; + } + if (entry.status === "running") { + return "Loading model"; + } + if (entry.status === "completed") { + return "Model loaded"; + } + return "Model load failed"; +} diff --git a/tests/studio/test_model_picker_contracts.py b/tests/studio/test_model_picker_contracts.py index 79efdbf61e..f146de5c0f 100644 --- a/tests/studio/test_model_picker_contracts.py +++ b/tests/studio/test_model_picker_contracts.py @@ -746,3 +746,20 @@ def test_backfill_includes_a_standalone_gguf_with_no_variant(): assert 'entry.modelId.toLowerCase().endsWith(".gguf")' in src # Still excluded for safetensors, which auto-switch does not resolve. assert "entry.ggufVariant != null ||" in src + + +def test_monitor_overlay_does_not_pull_in_the_lazy_page(): + """The overlay is mounted from __root.tsx, so a static import of the page + for two label helpers drags the whole 900-line page and its dependency + graph into the eagerly loaded bundle and undoes the route's + lazyRouteComponent. Measured: the async api-monitor chunk was 0.20 kB with + the page in the main bundle, and 18.83 kB after the helpers moved, with the + main bundle 18 kB smaller. + """ + overlay = _read("features/api-monitor/api-monitor-overlay.tsx") + assert 'from "./lifecycle"' in overlay + assert "api-monitor-page" not in overlay, "the overlay must not reach the page" + # The helpers live in their own module, not re-exported through the page. + shared = _read("features/api-monitor/lifecycle.ts") + assert "export function isLifecycleEntry(" in shared + assert "export function lifecycleLabel(" in shared