From f2232bd6f5ec80936320e45ec7293785d7987415 Mon Sep 17 00:00:00 2001 From: jun Date: Fri, 4 Sep 2026 02:52:41 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat(gui):=20dashboard=20shows=20health,=20?= =?UTF-8?q?sync,=20sidecars=20and=20memory=20=E2=80=94=20settings=20go=20h?= =?UTF-8?q?ome?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dashboard carried two read-only copies of the Providers and Models pages as tabs, and four settings that each had a real owner elsewhere: the subagent v1/base/v2 switch and the delegation-model card (Subagents), the shadow-call intercept (Models), and the Codex-autostart toggle (Startup). Every one was a second editor for one server value. Now: three stat cards (status with version and uptime as a visible sub-line, providers, 30-day tokens), the reboot-protection bar, model sync, the two sidecar cards behind one closed disclosure, and the memory card with its four byte counts folded into its own details. and bookmarks redirect to the real pages with a passive replace. Nothing lost capability. The autostart switch is rehomed in this same change to the Startup page's protection panel (useCodexAutostart, /api/settings), and the effort caps to the Subagents page (EffortCapSection, /api/effort-caps, shown outside v1); UltraModeState now carries multiAgentMode so that gate has a source. The dashboard hook drops its MA-mode, injection, effort-cap and shadow-call polls and the three help dialogs that went with them. Tests: dashboard-legacy-hashes (redirects), dashboard-rehome (autostart PUT, effort-cap PUT, v1 hides the caps); contracts/hash/shadow-call/startup-cache tests updated to the new shape. Plan: devlog/_plan/260904_dashboard_minimal/020_dashboard_home.md. --- gui/src/app-routing.ts | 15 +- .../components/MemoryObservabilityCard.tsx | 12 +- .../subagents-workspace/EffortCapSection.tsx | 83 +++++++ .../SubagentsWorkspace.tsx | 11 + gui/src/i18n/de.ts | 1 + gui/src/i18n/en.ts | 1 + gui/src/i18n/fr.ts | 1 + gui/src/i18n/ja.ts | 1 + gui/src/i18n/ko.ts | 1 + gui/src/i18n/ru.ts | 1 + gui/src/i18n/tr.ts | 1 + gui/src/i18n/zh-TW.ts | 1 + gui/src/i18n/zh.ts | 1 + gui/src/pages/Dashboard.tsx | 96 ++------ gui/src/pages/Startup.tsx | 3 + gui/src/pages/Subagents.tsx | 4 +- gui/src/pages/dashboard-dialogs.tsx | 79 ------- gui/src/pages/dashboard-models-section.tsx | 83 ------- gui/src/pages/dashboard-overview-head.tsx | 69 ++---- gui/src/pages/dashboard-overview-panels.tsx | 20 +- gui/src/pages/dashboard-overview-sections.tsx | 198 +---------------- gui/src/pages/dashboard-providers-section.tsx | 38 ---- gui/src/pages/dashboard-shared.ts | 14 -- gui/src/pages/startup-sections.tsx | 20 ++ gui/src/pages/use-codex-autostart.ts | 48 ++++ gui/src/pages/use-dashboard-data.ts | 210 +----------------- gui/src/pages/use-subagent-delegation.ts | 2 + gui/src/styles.css | 14 +- gui/tests/dashboard-contracts.test.ts | 17 +- gui/tests/dashboard-legacy-hashes.test.ts | 31 +++ gui/tests/dashboard-rehome.test.tsx | 133 +++++++++++ gui/tests/dashboard-tabs.test.ts | 68 ------ gui/tests/fr-localization.test.ts | 2 + gui/tests/multi-agent-guidance.test.tsx | 2 +- gui/tests/providers-hash-history.test.tsx | 3 +- gui/tests/shadow-call-model-options.test.ts | 5 +- gui/tests/startup-revisit-cache.test.tsx | 5 +- 37 files changed, 449 insertions(+), 845 deletions(-) create mode 100644 gui/src/components/subagents-workspace/EffortCapSection.tsx delete mode 100644 gui/src/pages/dashboard-models-section.tsx delete mode 100644 gui/src/pages/dashboard-providers-section.tsx create mode 100644 gui/src/pages/use-codex-autostart.ts create mode 100644 gui/tests/dashboard-legacy-hashes.test.ts create mode 100644 gui/tests/dashboard-rehome.test.tsx delete mode 100644 gui/tests/dashboard-tabs.test.ts diff --git a/gui/src/app-routing.ts b/gui/src/app-routing.ts index bab41b1eeec..6114ecd9cdf 100644 --- a/gui/src/app-routing.ts +++ b/gui/src/app-routing.ts @@ -59,7 +59,14 @@ export function readPageFromHash(hash?: string): Page { * choice, mirroring Logs (`#logs` / `#logs/debug`). Overview is the bare `#dashboard`, * so it has no suffix entry here. */ -export const DASHBOARD_TAB_HASHES = ["dashboard/providers", "dashboard/models"] as const; +/** + * The dashboard used to have Providers and Models tabs at these hashes. They were read-only + * copies of the real pages, so the bookmarks now redirect there (see resolveAppHashChange). + */ +export const LEGACY_DASHBOARD_TAB_REDIRECTS: Readonly> = { + "dashboard/providers": "providers", + "dashboard/models": "models", +}; /** * Models owns four tabs: the catalog, Combos, Routing, and Compatibility. The catalog @@ -107,8 +114,7 @@ export function hashBelongsToPage(rawHash: string, page: Page): boolean { || (page === "logs" && rawHash === "logs/debug") || (page === "codex-set" && rawHash === "codex-set/prompt") || (page === "models" && (MODELS_TAB_HASHES as readonly string[]).includes(rawHash)) - || (page === "dashboard" - && (rawHash === DASHBOARD_UPDATE_HASH || (DASHBOARD_TAB_HASHES as readonly string[]).includes(rawHash))) + || (page === "dashboard" && rawHash === DASHBOARD_UPDATE_HASH) || (page === "integrations" && (INTEGRATION_TAB_HASHES as readonly string[]).includes(rawHash)); } @@ -132,6 +138,9 @@ export function resolveAppHashChange(rawHash: string): AppHashChangeAction { if (rawHash === "debug" || rawHash.startsWith("debug/")) { return { page: "logs", replaceTo: "logs/debug" }; } + // Legacy: the dashboard's Providers / Models tabs. Passive replace, like the others. + const legacyDashboardTarget = LEGACY_DASHBOARD_TAB_REDIRECTS[rawHash]; + if (legacyDashboardTarget) return { page: legacyDashboardTarget, replaceTo: legacyDashboardTarget }; /* Legacy: Codex Auth is now the Multi-auth tab of Codex Set. */ if (rawHash === "codex-auth" || rawHash.startsWith("codex-auth/")) { diff --git a/gui/src/components/MemoryObservabilityCard.tsx b/gui/src/components/MemoryObservabilityCard.tsx index ebf12a4ad70..3e052a76815 100644 --- a/gui/src/components/MemoryObservabilityCard.tsx +++ b/gui/src/components/MemoryObservabilityCard.tsx @@ -448,7 +448,13 @@ export default function MemoryObservabilityCard({ apiBase }: { apiBase: string } t={t} /> -
+ {/* Everything numeric is collapsed by default: the pressure bar above is the headline; four byte + counts refreshing every few seconds are diagnosis, not a decision. */} +
+ {t("dash.mem.details")} +
{t("dash.mem.hint")}
+ +
{/* heapUsed can exceed heapTotal on Bun (external buffers are counted in used but not in the JS-object arena), so a "used / total" label reads @@ -466,10 +472,6 @@ export default function MemoryObservabilityCard({ apiBase }: { apiBase: string } />
- {/* Secondary diagnostics collapsed by default: only the headline stats stay visible. */} -
- {t("dash.mem.details")} -
{t("dash.mem.hint")}
{t("dash.mem.runtime")}
diff --git a/gui/src/components/subagents-workspace/EffortCapSection.tsx b/gui/src/components/subagents-workspace/EffortCapSection.tsx new file mode 100644 index 00000000000..702e6a78ed0 --- /dev/null +++ b/gui/src/components/subagents-workspace/EffortCapSection.tsx @@ -0,0 +1,83 @@ +/** + * Effort caps for the main model and for subagents (`/api/effort-caps`). + * + * This was a dashboard card. It only means something once multi-agent mode is on, and + * it is a delegation setting, so it lives with the other delegation settings on the + * Subagents page. Self-contained: it owns its fetch and save. + */ +import { useCallback, useEffect, useState } from "react"; +import { useT } from "../../i18n/shared"; +import { Select } from "../../ui"; +import { EFFORT_CAP_LEVELS, requireJson } from "../../pages/dashboard-shared"; + +type Caps = { effortCap: string; subagentEffortCap: string }; + +export function EffortCapSection({ apiBase }: { apiBase: string }) { + const t = useT(); + const [caps, setCaps] = useState({ effortCap: "", subagentEffortCap: "" }); + const [saving, setSaving] = useState(false); + const [loaded, setLoaded] = useState(false); + + useEffect(() => { + const controller = new AbortController(); + (async () => { + try { + const res = await fetch(`${apiBase}/api/effort-caps`, { signal: controller.signal }); + const data = await requireJson<{ effortCap?: string | null; subagentEffortCap?: string | null }>(res); + if (controller.signal.aborted) return; + setCaps({ effortCap: data.effortCap ?? "", subagentEffortCap: data.subagentEffortCap ?? "" }); + setLoaded(true); + } catch { /* leave the selects empty; a save still round-trips */ } + })(); + return () => controller.abort(); + }, [apiBase]); + + const save = useCallback(async (patch: { effortCap?: string | null; subagentEffortCap?: string | null }) => { + if (saving) return; + setSaving(true); + try { + const res = await fetch(`${apiBase}/api/effort-caps`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(patch), + }); + const data = await requireJson<{ ok: boolean; effortCap?: string | null; subagentEffortCap?: string | null }>(res); + setCaps({ effortCap: data.effortCap ?? "", subagentEffortCap: data.subagentEffortCap ?? "" }); + } catch { /* ignore */ } + finally { setSaving(false); } + }, [apiBase, saving]); + + const options = [ + { value: "", label: t("dash.effortCapNone") }, + ...EFFORT_CAP_LEVELS.map(e => ({ value: e, label: e })), + ]; + + return ( +
+
+
+ {t("dash.effortCapLabel")} + {t("dash.effortCapHelp")} +
+
+ { void save({ subagentEffortCap: v || null }); }} + disabled={saving} + label={t("dash.subagentEffortCapLabel")} + align="right" + /> +
+
+
+ ); +} diff --git a/gui/src/components/subagents-workspace/SubagentsWorkspace.tsx b/gui/src/components/subagents-workspace/SubagentsWorkspace.tsx index 862467da09f..ba2b2bbdb53 100644 --- a/gui/src/components/subagents-workspace/SubagentsWorkspace.tsx +++ b/gui/src/components/subagents-workspace/SubagentsWorkspace.tsx @@ -27,9 +27,12 @@ import { modelLabel } from "../../model-display"; import { SectionTabs } from "../section-tabs"; import { sectionAnchorId } from "../../section-anchors"; import SubagentDelegationSection from "./SubagentDelegationSection"; +import { EffortCapSection } from "./EffortCapSection"; import type { DelegationPatch, DelegationModelOption, UltraModePatch, UltraModeState } from "../../pages/use-subagent-delegation"; export interface SubagentsWorkspaceProps { + /** Needed by sections that own their own fetch (effort caps). */ + apiBase?: string; available: string[]; chosen: string[]; busy?: boolean; @@ -56,6 +59,7 @@ export interface SubagentsWorkspaceProps { export const FEATURED_MAX = 5; export default function SubagentsWorkspace({ + apiBase, available, chosen, busy = false, @@ -235,6 +239,13 @@ export default function SubagentsWorkspace({ ultraLoadFailed={delegation.ultraLoadFailed} onUltraModeRetry={delegation.onUltraModeRetry} /> + {/* + Effort caps only apply outside v1 (there are no ultra-mode turns to cap), which + is the same condition the dashboard used before this card moved here. + */} + {apiBase && delegation.ultraMode.multiAgentMode !== "v1" && ( + + )}
diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts index 6e2649d563f..23f9b9c253c 100644 --- a/gui/src/i18n/de.ts +++ b/gui/src/i18n/de.ts @@ -284,6 +284,7 @@ export const de: Record = { "dash.searchReasoning": "Such-Reasoning-Aufwand", "dash.visionModel": "Vision-Sidecar-Modell", "dash.visionModelHint": "Modell zur Beschreibung von Bildern für nur-Text-Routen. Erfordert ChatGPT-Login.", + "dash.sidecars": "Sidecars", "dash.webSearchSidecar": "Websuche-Sidecar", "dash.webSearchSidecarHint": "Backend und Modell für die Websuche gerouteter Modelle auswählen.", "dash.webSearchStream": "Antworten live streamen", diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts index b5adb6617ce..7ee5b852126 100644 --- a/gui/src/i18n/en.ts +++ b/gui/src/i18n/en.ts @@ -296,6 +296,7 @@ export const en = { "dash.searchReasoning": "Search reasoning effort", "dash.visionModel": "Vision sidecar model", "dash.visionModelHint": "Model used to describe images for text-only routed models. Requires ChatGPT login.", + "dash.sidecars": "Sidecars", "dash.webSearchSidecar": "Web search sidecar", "dash.webSearchSidecarHint": "Choose the backend and model used for web search on routed models.", "dash.webSearchStream": "Stream answers live", diff --git a/gui/src/i18n/fr.ts b/gui/src/i18n/fr.ts index d94a8f299b8..a9e3eb4c179 100644 --- a/gui/src/i18n/fr.ts +++ b/gui/src/i18n/fr.ts @@ -286,6 +286,7 @@ export const fr: Record = { "dash.searchReasoning": "Effort de raisonnement pour la recherche", "dash.visionModel": "Modèle auxiliaire de vision", "dash.visionModelHint": "Modèle utilisé pour décrire les images aux modèles routés en mode texte uniquement. Nécessite une connexion à ChatGPT.", + "dash.sidecars": "Sidecars", "dash.webSearchSidecar": "Service auxiliaire de recherche Web", "dash.webSearchSidecarHint": "Choisissez le moteur et le modèle utilisés pour la recherche Web sur les modèles routés.", "dash.webSearchStream": "Diffuser les réponses en direct", diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts index a5768971e18..cdc2def42c7 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -293,6 +293,7 @@ export const ja: Record = { "dash.searchReasoning": "検索の推論負荷", "dash.visionModel": "ビジョンサイドカーモデル", "dash.visionModelHint": "テキスト専用ルーティングモデルで画像を説明するために使うモデル。ChatGPT ログインが必要です。", + "dash.sidecars": "サイドカー", "dash.webSearchSidecar": "ウェブ検索サイドカー", "dash.webSearchSidecarHint": "ルーティングモデルでウェブ検索に使うバックエンドとモデルを選択します。", "dash.webSearchStream": "回答をライブ配信", diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts index 4f3d5bd7153..f4b1bf65ef2 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -288,6 +288,7 @@ export const ko: Record = { "dash.searchReasoning": "서치 추론 강도", "dash.visionModel": "비전 사이드카 모델", "dash.visionModelHint": "텍스트 전용 라우팅 모델에 이미지를 설명하는 데 사용되는 모델입니다. ChatGPT 로그인 필요.", + "dash.sidecars": "사이드카", "dash.webSearchSidecar": "웹 검색 사이드카", "dash.webSearchSidecarHint": "라우팅 모델의 웹 검색에 쓸 백엔드와 모델을 고릅니다.", "dash.webSearchStream": "응답 실시간 스트리밍", diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts index 10446d06da0..5c6a793c602 100644 --- a/gui/src/i18n/ru.ts +++ b/gui/src/i18n/ru.ts @@ -293,6 +293,7 @@ export const ru: Record = { "dash.searchReasoning": "Уровень рассуждений для поиска", "dash.visionModel": "Модель сайдкара для изображений", "dash.visionModelHint": "Модель, которая описывает изображения для маршрутизируемых моделей, работающих только с текстом. Требуется вход в аккаунт ChatGPT.", + "dash.sidecars": "Сайдкары", "dash.webSearchSidecar": "Сайдкар веб-поиска", "dash.webSearchSidecarHint": "Выберите бэкенд и модель, используемые для веб-поиска на маршрутизируемых моделях.", "dash.webSearchStream": "Стримить ответы вживую", diff --git a/gui/src/i18n/tr.ts b/gui/src/i18n/tr.ts index 31bc74cd9a1..1f20a725010 100644 --- a/gui/src/i18n/tr.ts +++ b/gui/src/i18n/tr.ts @@ -294,6 +294,7 @@ export const tr: Record = { "dash.searchReasoning": "Arama akıl yürütme çabası", "dash.visionModel": "Görsel yan araç modeli", "dash.visionModelHint": "Salt metin yönlendirilen modeller için görselleri tanımlamakta kullanılan model. ChatGPT girişi gerektirir.", + "dash.sidecars": "Yan araçlar", "dash.webSearchSidecar": "Web arama yan aracı (sidecar)", "dash.webSearchSidecarHint": "Yönlendirilen modellerde web araması için kullanılan arka ucu ve modeli seçin.", "dash.webSearchStream": "Yanıtları canlı akıt", diff --git a/gui/src/i18n/zh-TW.ts b/gui/src/i18n/zh-TW.ts index 1cae3e20928..8b5f78640a4 100644 --- a/gui/src/i18n/zh-TW.ts +++ b/gui/src/i18n/zh-TW.ts @@ -186,6 +186,7 @@ export const zhTW: Record = { "dash.searchReasoning": "搜尋推理強度", "dash.visionModel": "視覺附屬模型", "dash.visionModelHint": "為純文字路由模型描述圖像的模型。需要 ChatGPT 登入。", + "dash.sidecars": "側車", "dash.webSearchSidecar": "網頁搜尋附屬服務", "dash.webSearchSidecarHint": "選擇路由模型進行網頁搜尋時使用的後端和模型。", "dash.webSearchStream": "即時串流輸出回答", diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts index f133031c3f8..8b429555601 100644 --- a/gui/src/i18n/zh.ts +++ b/gui/src/i18n/zh.ts @@ -288,6 +288,7 @@ export const zh: Record = { "dash.searchReasoning": "搜索推理强度", "dash.visionModel": "视觉附属模型", "dash.visionModelHint": "为纯文本路由模型描述图像的模型。需要 ChatGPT 登录。", + "dash.sidecars": "边车", "dash.webSearchSidecar": "网页搜索附属服务", "dash.webSearchSidecarHint": "选择路由模型进行网页搜索时使用的后端和模型。", "dash.webSearchStream": "实时流式输出回答", diff --git a/gui/src/pages/Dashboard.tsx b/gui/src/pages/Dashboard.tsx index e32a671ec84..3d6d567e350 100644 --- a/gui/src/pages/Dashboard.tsx +++ b/gui/src/pages/Dashboard.tsx @@ -1,30 +1,22 @@ -import { type ReactNode } from "react"; import { IconAlert } from "../icons"; import { Trans } from "../i18n/provider"; -import { navigateHash } from "../hash-routing"; import { EmptyState } from "../ui"; import { DashboardDialogs } from "./dashboard-dialogs"; -import { DashboardModelsSection } from "./dashboard-models-section"; import { DashboardOverviewSection } from "./dashboard-overview-section"; -import { DashboardProvidersSection } from "./dashboard-providers-section"; -import { - dashboardHashForSection, - type DashboardSection, -} from "./dashboard-shared"; import { useDashboardData } from "./use-dashboard-data"; -function selectDashboardTab(next: DashboardSection) { - // Deliberate navigation: push a history entry so Back/Forward restore the tab. - navigateHash(dashboardHashForSection(next)); -} - +/** + * The dashboard is one surface: health, the reboot-protection bar, model sync, the sidecar + * disclosure and memory pressure. It used to carry two more tabs — read-only copies of the + * Providers and Models pages with no way to act on them — and the settings that have a home + * elsewhere (subagent mode, delegation model, shadow-call intercept, Codex autostart). Each + * of those was a second editor for one server value, which is a state-divergence bug + * waiting to be filed. The old `#dashboard/providers` and `#dashboard/models` bookmarks + * redirect to the real pages (app-routing.ts). + */ export default function Dashboard({ apiBase }: { apiBase: string }) { const d = useDashboardData(apiBase); - const { - t, error, selectedSection, - providers, models, modelsLoading, modelQuery, setModelQuery, - filteredGroups, expandedProviders, setExpandedProviders, - } = d; + const { t, error } = d; if (error) { return ( @@ -35,77 +27,15 @@ export default function Dashboard({ apiBase }: { apiBase: string }) { ); } - const overviewSection = ; - const providersSection = ; - const modelsSection = ( - - ); - const updateDialog = ; - - const sections: { id: DashboardSection; label: string; body: ReactNode }[] = [ - { id: "overview", label: t("dash.workspace.overview"), body: overviewSection }, - { id: "providers", label: t("dash.activeProviders"), body: providersSection }, - { id: "models", label: t("dash.availableModels"), body: modelsSection }, - ]; - const selected = sections.find(s => s.id === selectedSection) ?? sections[0]; - const selectTab = selectDashboardTab; - const onTabKeyDown = (e: React.KeyboardEvent) => { - const index = sections.findIndex(s => s.id === selectedSection); - let next = -1; - if (e.key === "ArrowRight") next = (index + 1) % sections.length; - else if (e.key === "ArrowLeft") next = (index - 1 + sections.length) % sections.length; - else if (e.key === "Home") next = 0; - else if (e.key === "End") next = sections.length - 1; - if (next < 0) return; - e.preventDefault(); - const target = sections[next]!; - selectTab(target.id); - document.getElementById(`dashboard-tab-${target.id}`)?.focus(); - }; - return (

{t("nav.dashboard")}

-

{t("dash.subtitle")}

-
- {sections.map(s => ( - - ))} -
-
- {selected.body} +
+
- {updateDialog} +
); } diff --git a/gui/src/pages/Startup.tsx b/gui/src/pages/Startup.tsx index c9e5ef2aaf1..55c35c1e659 100644 --- a/gui/src/pages/Startup.tsx +++ b/gui/src/pages/Startup.tsx @@ -6,6 +6,7 @@ import { readSessionListCache, writeSessionListCache } from "../session-list-cac import { Notice } from "../ui"; import { useDataSurface } from "../data-surface"; import { DataSurfaceSkeleton } from "../components/data-surface"; +import { useCodexAutostart } from "./use-codex-autostart"; import { StartupDetailsSection, StartupHeroSection, @@ -72,6 +73,7 @@ function deriveCodexRuntimeNotice( export default function Startup({ apiBase, machineApiBase = apiBase, connected = false }: { apiBase: string; machineApiBase?: string; connected?: boolean }) { const { t } = useI18n(); + const autostart = useCodexAutostart(apiBase); const cacheKey = `${STARTUP_PAGE_CACHE_PREFIX}${apiBase}`; const cached = useMemo(() => readSessionListCache(cacheKey), [cacheKey]); const startupResourceKey = `startup-page:${apiBase}`; @@ -385,6 +387,7 @@ export default function Startup({ apiBase, machineApiBase = apiBase, connected = installBusy={installBusy} installResult={installResult} onInstall={(action, opts) => { void runInstallAction(action, opts); }} + autostart={autostart} /> {data.platform === "win32" && ( ({ enabled: false, hintText: null, multiAgentV2Enabled: false }); + const [ultraMode, setUltraMode] = useState({ enabled: false, hintText: null, multiAgentV2Enabled: false, multiAgentMode: "default" }); const [ultraSaving, setUltraSaving] = useState(false); const [ultraLoadFailed, setUltraLoadFailed] = useState(false); const ultraLoadGeneration = useRef(0); @@ -57,6 +57,7 @@ export default function Subagents({ apiBase }: { apiBase: string }) { // `default` surface still preserves upstream V1 pins (for example luna), // so only an explicitly forced V2 catalog is an effective surface here. multiAgentV2Enabled: data.enabled === true && data.multiAgentMode === "v2", + multiAgentMode: data.multiAgentMode ?? "default", }); return true; }, [apiBase, t]); @@ -208,6 +209,7 @@ export default function Subagents({ apiBase }: { apiBase: string }) { {status && {status}} {state.showError && {t("sub.loadFail")}} ; @@ -16,10 +15,6 @@ export function DashboardDialogs(d: Dash) { updateOpen, closeUpdateDialog, updateDialogRef, updateChannel, changeUpdateChannel, updateLoading, updateError, updateCheck, fetchUpdateCheck, updateRestart, setUpdateRestart, runUpdate, - maHelpOpen, setMaHelpOpen, maHelpDialogRef, - effortCapHelpOpen, setEffortCapHelpOpen, effortCapHelpDialogRef, - shadowCallHelpOpen, setShadowCallHelpOpen, shadowCallHelpDialogRef, - shadowCall, } = d; return ( @@ -141,80 +136,6 @@ export function DashboardDialogs(d: Dash) { - - { event.preventDefault(); setMaHelpOpen(false); }} - > - - -
- {t("models.v2Help")} -
- -
- -
- -
- - { event.preventDefault(); setEffortCapHelpOpen(false); }} - > - - -
- {t("dash.effortCapHelp")} -
-
- -
- -
- - { event.preventDefault(); setShadowCallHelpOpen(false); }} - > - - -
- {t("dash.shadowCallTooltip", { models: shadowSourceModelLabel(shadowCall?.sourceModels) })} -
-
- -
- -
); } diff --git a/gui/src/pages/dashboard-models-section.tsx b/gui/src/pages/dashboard-models-section.tsx deleted file mode 100644 index b839828e590..00000000000 --- a/gui/src/pages/dashboard-models-section.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { type Dispatch, type SetStateAction } from "react"; -import { IconChevron, IconSearch } from "../icons"; -import type { TFn } from "../i18n/shared"; -import { EmptyState } from "../ui"; -import { formatProviderDisplayName } from "../provider-icons"; -import type { ModelInfo } from "./dashboard-shared"; - -export function DashboardModelsSection({ - t, - models, - modelsLoading, - modelQuery, - setModelQuery, - filteredGroups, - expandedProviders, - setExpandedProviders, -}: { - t: TFn; - models: ModelInfo[]; - modelsLoading: boolean; - modelQuery: string; - setModelQuery: (v: string) => void; - filteredGroups: Array<[string, ModelInfo[]]>; - expandedProviders: Set; - setExpandedProviders: Dispatch>>; -}) { - return ( - <> -
- {t("dash.availableModels")} {models.length} - {modelsLoading && } -
- {models.length === 0 && !modelsLoading ? ( - - ) : ( - <> -
-
- {filteredGroups.length === 0 ? ( -

{t("dash.modelsNoResults")}

- ) : ( -
- {filteredGroups.map(([provider, rows]) => { - const q = modelQuery.trim().toLowerCase(); - const open = q !== "" || expandedProviders.has(provider); - return ( -
- - {open && ( -
- {rows.map(m => ( - {m.id} - ))} -
- )} -
- ); - })} -
- )} - - )} - - ); -} diff --git a/gui/src/pages/dashboard-overview-head.tsx b/gui/src/pages/dashboard-overview-head.tsx index 9cccc0e26c4..a768dee5613 100644 --- a/gui/src/pages/dashboard-overview-head.tsx +++ b/gui/src/pages/dashboard-overview-head.tsx @@ -1,5 +1,5 @@ -import { IconAlert, IconInfo } from "../icons"; -import { type TKey, useT } from "../i18n/shared"; +import { IconAlert } from "../icons"; +import { useT } from "../i18n/shared"; import { formatTokens } from "../format-tokens"; import { formatUptime } from "../formatUptime"; import { navigateHash } from "../hash-routing"; @@ -16,68 +16,29 @@ export function DashboardOverviewHead({ healthLoading, startupHealth, projectConfigWarnings, - maMode, - maBusy, - maHelpTriggerRef, - maHelpOpen, - setMaHelpOpen, - switchMaMode, - maError, -}: Pick) { - const t = useT(); +}: Pick) { + const t = useT(); const online = health?.status === "ok"; return ( <>
+ {/* + Three cards: reachability, capacity, volume. Version and uptime used to be cards + of their own; they are diagnosis facts, not decisions, so they ride as the status + card's sub-line (visible, not a title attribute). The subagent v1/base/v2 switch + that opened this row is a Subagents setting and lives there now. + */}
-
-
- {t("dash.multiAgent")} - -
-
-
- {(["v1", "default", "v2"] as const).map(mode => ( - - ))} -
-
- {maError && ( -
- {maError} -
- )} -
-
-
{t("dash.status")}
+
+
{t("dash.status")}
{online ? t("dash.online") : t("dash.offline")}
+
+ {health ? `v${health.version} · ${formatUptime(health.uptime, locale)}` : "\u00a0"} +
-
{t("dash.version")}
{health?.version ?? "—"}
-
{t("dash.uptime")}
{health ? formatUptime(health.uptime, locale) : "—"}
{t("dash.providers")}
{providers.length}
{t("dash.tokens30d")}
diff --git a/gui/src/pages/dashboard-overview-panels.tsx b/gui/src/pages/dashboard-overview-panels.tsx index 8009d044232..adfae2326aa 100644 --- a/gui/src/pages/dashboard-overview-panels.tsx +++ b/gui/src/pages/dashboard-overview-panels.tsx @@ -1,8 +1,7 @@ import MemoryObservabilityCard from "../components/MemoryObservabilityCard"; +import { useT } from "../i18n/shared"; import type { useDashboardData } from "./use-dashboard-data"; import { - DashboardEffortCapPanel, - DashboardInjectionPanel, DashboardMaintenancePanel, DashboardSidecarPanels, } from "./dashboard-overview-sections"; @@ -10,14 +9,19 @@ import { type Dash = ReturnType; export function DashboardOverviewPanels(props: Dash) { + const t = useT(); return ( <> - -
- - -
- + + {/* + Web-search and vision sidecars are routing configuration, not health. They stay on + the dashboard (there is no better home yet) but closed by default, so the first + viewport is the state of the proxy rather than two model pickers. + */} +
+ {t("dash.sidecars")} + +
); diff --git a/gui/src/pages/dashboard-overview-sections.tsx b/gui/src/pages/dashboard-overview-sections.tsx index 7bdf2829315..8ec3e1a4d71 100644 --- a/gui/src/pages/dashboard-overview-sections.tsx +++ b/gui/src/pages/dashboard-overview-sections.tsx @@ -1,20 +1,15 @@ import { useCallback, useEffect, useLayoutEffect, useRef, useState, type CSSProperties } from "react"; import { createPortal } from "react-dom"; -import { IconAlert, IconCheck, IconChevron, IconInfo, IconRefresh, IconX } from "../icons"; +import { IconAlert, IconCheck, IconChevron, IconRefresh, IconX } from "../icons"; import { Trans } from "../i18n/provider"; import type { TFn } from "../i18n/shared"; import { Select } from "../ui"; import { computeSelectMenuStyle } from "../select-position"; -import { formatNamespacedModelId } from "../provider-icons"; -import { navigateHash } from "../hash-routing"; import { clampVisionReasoningToLadder, - EFFORT_CAP_LEVELS, parsePositiveInteger, parseVisionTimeoutMs, - requireJson, type SidecarPatch, - shadowCallModelOptions, webSearchSidecarSelectionForModel, updateJobLabel, visionEnabledPatch, @@ -29,136 +24,10 @@ import { VISION_TIMEOUT_MS_MAX, VISION_TIMEOUT_MS_MIN, } from "./dashboard-shared"; -import { shadowSourceModelBadge } from "./shadow-call-source"; import type { useDashboardData } from "./use-dashboard-data"; type Dash = ReturnType; -export function DashboardEffortCapPanel({ apiBase, d }: { apiBase: string; d: Dash }) { - const { - t, maMode, maModeResolved, - effortCapHelpTriggerRef, effortCapHelpOpen, setEffortCapHelpOpen, - effortCap, subagentEffortCap, effortCapSaving, setEffortCap, setSubagentEffortCap, setEffortCapSaving, - } = d; - - if (!maModeResolved || maMode === "v1") return null; - - return ( -
-
- - {t("dash.effortCapLabel")} - - - ({ value: e, label: e })), - ]} - onChange={async (v) => { - if (effortCapSaving) return; - setEffortCapSaving(true); - try { - const res = await fetch(`${apiBase}/api/effort-caps`, { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ subagentEffortCap: v || null }), - }); - const data = await requireJson<{ ok: boolean; effortCap?: string | null; subagentEffortCap?: string | null }>(res); - setEffortCap(data.effortCap ?? ""); - setSubagentEffortCap(data.subagentEffortCap ?? ""); - } catch { /* ignore */ } - finally { setEffortCapSaving(false); } - }} - disabled={effortCapSaving} - label={t("dash.subagentEffortCapLabel")} - align="right" - /> -
-
- ); -} - -export function DashboardInjectionPanel({ d }: { apiBase: string; d: Dash }) { - const { - t, injectionModel, injectionEffort, injectionEfforts, injectionAvailable, injectionSaving, - saveInjection, - } = d; - - return ( -
-
{t("dash.injectionLabel")}
-
- ({ value: e, label: e })), - ]} - onChange={(v) => { void saveInjection({ model: injectionModel || null, effort: v || null }); }} - disabled={injectionSaving} - label={t("dash.injectionEffortLabel")} - align="right" - /> - )} - -
-
- ); -} - export function DashboardMaintenancePanel({ d }: { d: Dash }) { const { t, runSync, syncing, updateTriggerRef, openUpdateDialog, updateLoading, updateOpen, @@ -436,9 +305,8 @@ function VisionAdvancedPopover({ t, open, triggerRef, onClose, maxValue, maxInva export function DashboardSidecarPanels({ d }: { d: Dash }) { const { - t, settings, settingsSaving, toggleCodexAutoStart, + t, sidecar, sidecarSaving, sidecarModels, visionModels, models, saveSidecar, - shadowCall, shadowCallSaving, shadowCallHelpTriggerRef, shadowCallHelpOpen, setShadowCallHelpOpen, saveShadowCall, } = d; const visionEnabled = sidecar?.vision.enabled !== false; const visionModel = visionEnabled ? (sidecar?.vision.model ?? "gpt-5.4-mini") : ""; @@ -484,25 +352,6 @@ export function DashboardSidecarPanels({ d }: { d: Dash }) { return ( <> -
-
-
-
{t("dash.codexAutoStart")}
-
{t("dash.codexAutoStartHint")}
-
- -
-
-
{/* Both sidecar cards wear the DashboardInjectionPanel shell: the PANEL is the flex row, copy left, controls right. */} @@ -622,48 +471,7 @@ export function DashboardSidecarPanels({ d }: { d: Dash }) { )}
- -
-
-
- {t("dash.shadowCallIntercept")} - - {`⚠ ${shadowSourceModelBadge(shadowCall?.sourceModels)}`} -
-
- -