Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16 changes: 10 additions & 6 deletions gui/src/app-routing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Record<string, Page>> = {
"dashboard/providers": "providers",
"dashboard/models": "models",
};

/**
* Models owns four tabs: the catalog, Combos, Routing, and Compatibility. The catalog
Expand Down Expand Up @@ -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));
}
Expand All @@ -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 };
Comment on lines +142 to +143

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Update the dashboard guide when redirecting its old tabs

This changes #dashboard/providers and #dashboard/models from persistent dashboard sections into redirects, but the English and translated docs-site/**/guides/web-dashboard.md pages still state that these hashes open dashboard sections and that the delegation picker and multi-agent summary remain on Dashboard. Users following the documented links now land on different pages and are directed to the wrong location for moved controls, so update the dashboard guide and its locales in the same change.

AGENTS.md reference: gui/AGENTS.md:L36-L36

Useful? React with 👍 / 👎.


/* Legacy: Codex Auth is now the Multi-auth tab of Codex Set. */
if (rawHash === "codex-auth" || rawHash.startsWith("codex-auth/")) {
Expand Down
12 changes: 7 additions & 5 deletions gui/src/components/MemoryObservabilityCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,13 @@ export default function MemoryObservabilityCard({ apiBase }: { apiBase: string }
t={t}
/>

<div className="stat-row mem-stats">
{/* 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. */}
<details style={{ marginTop: 10 }}>
<summary className="muted text-label" style={{ cursor: "pointer", padding: "2px 2px" }}>{t("dash.mem.details")}</summary>
<div className="muted text-control" style={{ margin: "8px 0 0" }}>{t("dash.mem.hint")}</div>

<div className="stat-row mem-stats" style={{ marginTop: 10 }}>
<Stat label={t("dash.mem.rss")} value={data ? formatBytes(data.rss, locale) : "—"} />
{/* 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
Expand All @@ -466,10 +472,6 @@ export default function MemoryObservabilityCard({ apiBase }: { apiBase: string }
/>
</div>

{/* Secondary diagnostics collapsed by default: only the headline stats stay visible. */}
<details style={{ marginTop: 10 }}>
<summary className="muted text-label" style={{ cursor: "pointer", padding: "2px 2px" }}>{t("dash.mem.details")}</summary>
<div className="muted text-control" style={{ margin: "8px 0 0" }}>{t("dash.mem.hint")}</div>

<div className="muted text-label" style={{ margin: "14px 0 6px" }}>{t("dash.mem.runtime")}</div>
<div className="stat-row">
Expand Down
78 changes: 78 additions & 0 deletions gui/src/components/subagents-workspace/EffortCapSection.tsx
Original file line number Diff line number Diff line change
@@ -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<Caps> {
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<Caps | null>(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 (
<section className="panel swi-effort-caps" aria-busy={read.data === undefined || undefined}>
<div className="setting-row">
<div className="setting-label">
<span className="title">{t("dash.effortCapLabel")}</span>
<span className="desc">{t("dash.effortCapHelp")}</span>
</div>
<div className="setting-controls" style={{ display: "flex", gap: 8 }}>
<Select
value={caps.effortCap}
options={options}
onChange={v => { void save({ effortCap: v || null }); }}
disabled={saving}
label={t("dash.effortCapLabel")}
align="right"
/>
<Select
value={caps.subagentEffortCap}
options={options}
onChange={v => { void save({ subagentEffortCap: v || null }); }}
disabled={saving}
label={t("dash.subagentEffortCapLabel")}
align="right"
/>
</div>
</div>
</section>
);
}
11 changes: 11 additions & 0 deletions gui/src/components/subagents-workspace/SubagentsWorkspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -56,6 +59,7 @@ export interface SubagentsWorkspaceProps {
export const FEATURED_MAX = 5;

export default function SubagentsWorkspace({
apiBase,
available,
chosen,
busy = false,
Expand Down Expand Up @@ -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" && (
<EffortCapSection apiBase={apiBase} />
Comment on lines +246 to +247

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Hide effort caps until multi-agent mode resolves

When the Subagents workspace paints from session cache, or when /api/v2 is slow or fails, ultraMode.multiAgentMode still contains its initial placeholder value of "default", so this condition mounts the effort-cap editor and fetches /api/effort-caps even if the server is actually in v1. On a failed mode read, the invalid editor remains writable indefinitely. Preserve a resolved flag, as the former dashboard implementation did, and require it before rendering this section.

AGENTS.md reference: gui/AGENTS.md:L9-L10

Useful? React with 👍 / 👎.

)}
</section>
</div>
</div>
Expand Down
1 change: 1 addition & 0 deletions gui/src/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,7 @@ export const de: Record<TKey, string> = {
"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",
Expand Down
1 change: 1 addition & 0 deletions gui/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions gui/src/i18n/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,7 @@ export const fr: Record<TKey, string> = {
"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",
Expand Down
1 change: 1 addition & 0 deletions gui/src/i18n/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,7 @@ export const ja: Record<TKey, string> = {
"dash.searchReasoning": "検索の推論負荷",
"dash.visionModel": "ビジョンサイドカーモデル",
"dash.visionModelHint": "テキスト専用ルーティングモデルで画像を説明するために使うモデル。ChatGPT ログインが必要です。",
"dash.sidecars": "サイドカー",
"dash.webSearchSidecar": "ウェブ検索サイドカー",
"dash.webSearchSidecarHint": "ルーティングモデルでウェブ検索に使うバックエンドとモデルを選択します。",
"dash.webSearchStream": "回答をライブ配信",
Expand Down
1 change: 1 addition & 0 deletions gui/src/i18n/ko.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@ export const ko: Record<TKey, string> = {
"dash.searchReasoning": "서치 추론 강도",
"dash.visionModel": "비전 사이드카 모델",
"dash.visionModelHint": "텍스트 전용 라우팅 모델에 이미지를 설명하는 데 사용되는 모델입니다. ChatGPT 로그인 필요.",
"dash.sidecars": "사이드카",
"dash.webSearchSidecar": "웹 검색 사이드카",
"dash.webSearchSidecarHint": "라우팅 모델의 웹 검색에 쓸 백엔드와 모델을 고릅니다.",
"dash.webSearchStream": "응답 실시간 스트리밍",
Expand Down
1 change: 1 addition & 0 deletions gui/src/i18n/ru.ts
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,7 @@ export const ru: Record<TKey, string> = {
"dash.searchReasoning": "Уровень рассуждений для поиска",
"dash.visionModel": "Модель сайдкара для изображений",
"dash.visionModelHint": "Модель, которая описывает изображения для маршрутизируемых моделей, работающих только с текстом. Требуется вход в аккаунт ChatGPT.",
"dash.sidecars": "Сайдкары",
"dash.webSearchSidecar": "Сайдкар веб-поиска",
"dash.webSearchSidecarHint": "Выберите бэкенд и модель, используемые для веб-поиска на маршрутизируемых моделях.",
"dash.webSearchStream": "Стримить ответы вживую",
Expand Down
1 change: 1 addition & 0 deletions gui/src/i18n/tr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,7 @@ export const tr: Record<TKey, string> = {
"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",
Expand Down
1 change: 1 addition & 0 deletions gui/src/i18n/zh-TW.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ export const zhTW: Record<TKey, string> = {
"dash.searchReasoning": "搜尋推理強度",
"dash.visionModel": "視覺附屬模型",
"dash.visionModelHint": "為純文字路由模型描述圖像的模型。需要 ChatGPT 登入。",
"dash.sidecars": "側車",
"dash.webSearchSidecar": "網頁搜尋附屬服務",
"dash.webSearchSidecarHint": "選擇路由模型進行網頁搜尋時使用的後端和模型。",
"dash.webSearchStream": "即時串流輸出回答",
Expand Down
1 change: 1 addition & 0 deletions gui/src/i18n/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@ export const zh: Record<TKey, string> = {
"dash.searchReasoning": "搜索推理强度",
"dash.visionModel": "视觉附属模型",
"dash.visionModelHint": "为纯文本路由模型描述图像的模型。需要 ChatGPT 登录。",
"dash.sidecars": "边车",
"dash.webSearchSidecar": "网页搜索附属服务",
"dash.webSearchSidecarHint": "选择路由模型进行网页搜索时使用的后端和模型。",
"dash.webSearchStream": "实时流式输出回答",
Expand Down
96 changes: 13 additions & 83 deletions gui/src/pages/Dashboard.tsx
Original file line number Diff line number Diff line change
@@ -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 (
Expand All @@ -35,77 +27,15 @@ export default function Dashboard({ apiBase }: { apiBase: string }) {
);
}

const overviewSection = <DashboardOverviewSection {...d} />;
const providersSection = <DashboardProvidersSection t={t} providers={providers} />;
const modelsSection = (
<DashboardModelsSection
t={t}
models={models}
modelsLoading={modelsLoading}
modelQuery={modelQuery}
setModelQuery={setModelQuery}
filteredGroups={filteredGroups}
expandedProviders={expandedProviders}
setExpandedProviders={setExpandedProviders}
/>
);
const updateDialog = <DashboardDialogs {...d} />;

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 (
<div className="dashboard-workspace-shell">
<div className="page-head">
<h2>{t("nav.dashboard")}</h2>
</div>
<p className="page-sub">{t("dash.subtitle")}</p>
<div className="page-tabs" role="tablist" aria-label={t("dash.workspace.sections")}>
{sections.map(s => (
<button
key={s.id}
type="button"
role="tab"
id={`dashboard-tab-${s.id}`}
aria-selected={selectedSection === s.id}
aria-controls={`dashboard-panel-${s.id}`}
tabIndex={selectedSection === s.id ? 0 : -1}
className={`page-tab${selectedSection === s.id ? " page-tab--active" : ""}`}
onClick={() => selectTab(s.id)}
onKeyDown={onTabKeyDown}
>
{s.label}
</button>
))}
</div>
<section
className="dashboard-workspace-main"
role="tabpanel"
id={`dashboard-panel-${selected.id}`}
aria-labelledby={`dashboard-tab-${selected.id}`}
tabIndex={0}
>
{selected.body}
<section className="dashboard-workspace-main" id="dashboard-panel-overview">
<DashboardOverviewSection {...d} />
</section>
{updateDialog}
<DashboardDialogs {...d} />
</div>
);
}
3 changes: 3 additions & 0 deletions gui/src/pages/Startup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<StartupPageCache>(cacheKey), [cacheKey]);
const startupResourceKey = `startup-page:${apiBase}`;
Expand Down Expand Up @@ -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" && (
<StartupTraySection
Expand Down
Loading
Loading