diff --git a/devlog/_plan/260904_dashboard_minimal/assets/021_dashboard_after.png b/devlog/_plan/260904_dashboard_minimal/assets/021_dashboard_after.png new file mode 100644 index 00000000000..1a4d8f5b3bc Binary files /dev/null and b/devlog/_plan/260904_dashboard_minimal/assets/021_dashboard_after.png differ diff --git a/devlog/_plan/260904_dashboard_minimal/assets/021_startup_autostart_row.png b/devlog/_plan/260904_dashboard_minimal/assets/021_startup_autostart_row.png new file mode 100644 index 00000000000..001fc819b8c Binary files /dev/null and b/devlog/_plan/260904_dashboard_minimal/assets/021_startup_autostart_row.png differ diff --git a/gui/src/app-routing.ts b/gui/src/app-routing.ts index bab41b1eeec..05ceaf44183 100644 --- a/gui/src/app-routing.ts +++ b/gui/src/app-routing.ts @@ -55,11 +55,13 @@ export function readPageFromHash(hash?: string): Page { } /** - * Dashboard section tabs live in the hash so refresh/bookmark/back-forward keep the - * choice, mirroring Logs (`#logs` / `#logs/debug`). Overview is the bare `#dashboard`, - * so it has no suffix entry here. + * 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 DASHBOARD_TAB_HASHES = ["dashboard/providers", "dashboard/models"] as const; +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 +109,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 +133,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..5d718d31da3 --- /dev/null +++ b/gui/src/components/subagents-workspace/EffortCapSection.tsx @@ -0,0 +1,78 @@ +/** + * 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. The read goes through the shared client-resource layer; a save is an + * optimistic override until the server answers. + */ +import { useCallback, useState } from "react"; +import { useKeyedClientResource } from "../../client-resource"; +import { useT } from "../../i18n/shared"; +import { Select } from "../../ui"; +import { EFFORT_CAP_LEVELS, requireJson } from "../../pages/dashboard-shared"; + +type Caps = { effortCap: string; subagentEffortCap: string }; + +async function readCaps(apiBase: string, signal: AbortSignal): Promise { + const res = await fetch(`${apiBase}/api/effort-caps`, { signal }); + const data = await requireJson<{ effortCap?: string | null; subagentEffortCap?: string | null }>(res); + return { effortCap: data.effortCap ?? "", subagentEffortCap: data.subagentEffortCap ?? "" }; +} + +export function EffortCapSection({ apiBase }: { apiBase: string }) { + const t = useT(); + const read = useKeyedClientResource(`effort-caps:${apiBase}`, [apiBase], (signal) => readCaps(apiBase, signal)); + const [override, setOverride] = useState(null); + const [saving, setSaving] = useState(false); + const caps: Caps = override ?? read.data ?? { effortCap: "", subagentEffortCap: "" }; + + 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); + setOverride({ 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")}} ; -}; export type InjectionSelectionResponse = { multiAgentGuidanceEnabled?: boolean; @@ -42,10 +33,6 @@ export function normalizeInjectionSelection(data: InjectionSelectionResponse) { }; } -export type EffortCapPoll = { - effortCap: string; - subagentEffortCap: string; -}; export type DashboardOverviewPoll = { health: HealthData | null; @@ -53,21 +40,10 @@ export type DashboardOverviewPoll = { error: boolean; }; -/** Multi-agent extras — slower peers must not gate status/uptime/provider counts. */ -export type DashboardMultiAgentPoll = { - /** Absent when the optional endpoint failed — callers must keep prior UI state. */ - injection: InjectionPoll | undefined; - effortCaps: EffortCapPoll | undefined; -}; -/** Sidecar + shadow only — must not wait on /api/settings (startup-health). */ +/** Sidecar only — must not wait on /api/settings (startup-health). */ export type DashboardSidecarPoll = { sidecar: SidecarData; - /** - * `null` = authoritative endpoint failure (clear UI). - * `undefined` = lost poll authority (epoch gate) — do not commit. - */ - shadowCall: ShadowCallData | null | undefined; }; export type DashboardSettingsPoll = { @@ -76,17 +52,11 @@ export type DashboardSettingsPoll = { startupHealthSeed: SettingsData["startupHealth"] | null | undefined; }; -export type DashboardMaModePoll = { - maMode: "v1" | "default" | "v2"; -}; export type DashboardEpochRefs = { settingsRequestEpochRef: { current: number }; settingsMutationEpochRef: { current: number }; settingsMutationInFlightRef: { current: boolean }; - shadowCallRequestEpochRef: { current: number }; - shadowCallMutationEpochRef: { current: number }; - shadowCallMutationInFlightRef: { current: boolean }; }; function isAbortError(error: unknown, signal: AbortSignal): boolean { @@ -141,62 +111,14 @@ export async function fetchDashboardUsage(apiBase: string, signal: AbortSignal): return requireJson(response); } -/** Web-search / vision sidecar + shadow-call — config reads, typically sub-10ms. */ +/** Web-search / vision sidecar — a config read, typically sub-10ms. */ export async function fetchDashboardSidecars( apiBase: string, signal: AbortSignal, - epochs: DashboardEpochRefs, ): Promise { - // Only bump shadow epochs — settings has its own poll and must not be invalidated here. - const { request: shadowRequestEpoch, mutation: shadowMutationEpoch } = beginPollEpoch( - epochs.shadowCallRequestEpochRef, - epochs.shadowCallMutationEpochRef, - ); - - const [scRes, shRes] = await Promise.all([ - fetch(`${apiBase}/api/sidecar-settings`, { signal }), - fetch(`${apiBase}/api/shadow-call-settings`, { signal }), - ]); - + const scRes = await fetch(`${apiBase}/api/sidecar-settings`, { signal }); const sidecar = await requireJson(scRes); - let shadowCall: ShadowCallData | null | undefined = undefined; - try { - if (shRes.ok) { - const nextShadow = await shRes.json() as ShadowCallData; - if (settingsPollMayCommit( - { request: shadowRequestEpoch, mutation: shadowMutationEpoch }, - { - request: epochs.shadowCallRequestEpochRef.current, - mutation: epochs.shadowCallMutationEpochRef.current, - mutationInFlight: epochs.shadowCallMutationInFlightRef.current, - }, - )) { - shadowCall = nextShadow; - } - } else if (settingsPollMayCommit( - { request: shadowRequestEpoch, mutation: shadowMutationEpoch }, - { - request: epochs.shadowCallRequestEpochRef.current, - mutation: epochs.shadowCallMutationEpochRef.current, - mutationInFlight: epochs.shadowCallMutationInFlightRef.current, - }, - )) { - shadowCall = null; - } - } catch { - if (settingsPollMayCommit( - { request: shadowRequestEpoch, mutation: shadowMutationEpoch }, - { - request: epochs.shadowCallRequestEpochRef.current, - mutation: epochs.shadowCallMutationEpochRef.current, - mutationInFlight: epochs.shadowCallMutationInFlightRef.current, - }, - )) { - shadowCall = null; - } - } - - return { sidecar, shadowCall }; + return { sidecar }; } /** Codex auto-start + startup-health seed — can be slower because settings embeds startup probe. */ @@ -229,25 +151,6 @@ export async function fetchDashboardSettings( return { settings, startupHealthSeed }; } -/** Multi-agent mode toggle only — must not wait on injection-model / effort-caps. */ -export async function fetchDashboardMaMode( - apiBase: string, - signal: AbortSignal, -): Promise { - try { - const v2Res = await fetch(`${apiBase}/api/v2`, { signal }); - if (!v2Res.ok) return { maMode: "default" }; - const v2Data = await v2Res.json() as { multiAgentMode?: unknown }; - if (v2Data.multiAgentMode === "v1" || v2Data.multiAgentMode === "v2") { - return { maMode: v2Data.multiAgentMode }; - } - return { maMode: "default" }; - } catch (error) { - if (isAbortError(error, signal)) throw error; - return { maMode: "default" }; - } -} - export async function fetchDashboardOverview( apiBase: string, signal: AbortSignal, @@ -265,40 +168,3 @@ export async function fetchDashboardOverview( } } -export async function fetchDashboardMultiAgent( - apiBase: string, - signal: AbortSignal, -): Promise { - const [imRes, ecRes] = await Promise.all([ - fetch(`${apiBase}/api/injection-model`, { signal }).catch(() => null), - fetch(`${apiBase}/api/effort-caps`, { signal }).catch(() => null), - ]); - - let injection: InjectionPoll | undefined; - try { - if (imRes?.ok) { - const imData = await imRes.json() as InjectionSelectionResponse & { - efforts?: string[]; - available?: InjectionPoll["injectionAvailable"]; - }; - injection = { - ...normalizeInjectionSelection(imData), - injectionEfforts: imData.efforts ?? [], - injectionAvailable: imData.available ?? [], - }; - } - } catch { /* old server / malformed — keep prior UI state */ } - - let effortCaps: EffortCapPoll | undefined; - try { - if (ecRes?.ok) { - const ecData = await ecRes.json() as { effortCap?: string | null; subagentEffortCap?: string | null }; - effortCaps = { - effortCap: ecData.effortCap ?? "", - subagentEffortCap: ecData.subagentEffortCap ?? "", - }; - } - } catch { /* old server */ } - - return { injection, effortCaps }; -} diff --git a/gui/src/pages/dashboard-dialogs.tsx b/gui/src/pages/dashboard-dialogs.tsx index 39aed2c1d86..35957bd8b19 100644 --- a/gui/src/pages/dashboard-dialogs.tsx +++ b/gui/src/pages/dashboard-dialogs.tsx @@ -6,7 +6,6 @@ import { type UpdateChannel, } from "./dashboard-shared"; import type { useDashboardData } from "./use-dashboard-data"; -import { shadowSourceModelLabel } from "./shadow-call-source"; type Dash = ReturnType; @@ -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..d1a86b3f651 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,6 @@ export function DashboardSidecarPanels({ d }: { d: Dash }) { )}
- -
-
-
- {t("dash.shadowCallIntercept")} - - {`⚠ ${shadowSourceModelBadge(shadowCall?.sourceModels)}`} -
-
- -