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
2 changes: 1 addition & 1 deletion docs-site/src/content/docs/guides/codex-app-models.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ actually advertised, and assigns low catalog priorities in the selected order. W
are active, bare native selections expand into selector-qualified groups. Other models remain callable
by exact id.

The active roster is separate from the Dashboard's **Sub-agent delegation** selection. It
The configured roster is separate from the Dashboard's **Sub-agent delegation** selection. It
controls which overrides Codex offers first; it does not select a model or trigger delegation by
itself.

Expand Down
2 changes: 1 addition & 1 deletion docs-site/src/content/docs/guides/model-ordering.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ have expanded into selector-qualified groups.
## Changing the order

The supported way to customize leading model order is to reorder `subagentModels`. In the dashboard,
open **Subagents** → **Agent Command Center** and reorder the **Active Roster** by dragging, with the
open **Subagents** → **Agent Command Center** and reorder the **Configured Roster** by dragging, with the
arrow buttons, or with <kbd>Alt</kbd> + <kbd>↑</kbd>/<kbd>↓</kbd>. The searchable **Agent Library** may
contain far more than five catalog models; entries remain addressable by exact id when their route is
available, while the five-slot limit applies only to the overrides advertised first to `spawn_agent`.
Expand Down
7 changes: 5 additions & 2 deletions docs-site/src/content/docs/guides/sub-agent-surface.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,11 @@ switching an active conversation in place.
- **Dashboard** → first stat cell: choose **v1**, **base**, or **v2**.
- **Models** → **Current behavior** → **Collaboration**: choose **Reliable v1**, **Codex native** (base/default semantics), or **Concurrent v2**.
- **Subagents** → **Agent Command Center**:
- **Active Roster** chooses and orders the five model overrides advertised first to `spawn_agent`.
Drag rows, use the arrow buttons, or press <kbd>Alt</kbd> + <kbd>↑</kbd>/<kbd>↓</kbd>.
- **Configured Roster** chooses and orders the five model overrides advertised first to `spawn_agent`.
Drag rows, use the arrow buttons, or press <kbd>Alt</kbd> + <kbd>↑</kbd>/<kbd>↓</kbd>. The card
shows your configured quick picks: a row's status line (for example **V1 under Codex defaults** or
**Routed · V2 compatible**) indicates which collaboration surfaces can select it, while rows
without a status line are available on both.
- **Agent Library** searches the current model catalog and filters factual capabilities such as
reasoning, long context, vision, and tool support. Entries remain addressable by exact id when
their route is available, including models outside the five-slot roster.
Expand Down
109 changes: 105 additions & 4 deletions gui/src/components/subagents-workspace/SubagentsWorkspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@ import {
IconSearch,
IconX,
} from "../../icons";
import { useI18n, useT, type Locale } from "../../i18n/shared";
import { useI18n, useT, type Locale, type TFn } from "../../i18n/shared";
import { modelLabel } from "../../model-display";
import { formatNamespacedModelId, providerIconSrc } from "../../provider-icons";
import SubagentDelegationSection from "./SubagentDelegationSection";
import type { DelegationPatch, DelegationModelOption } from "../../pages/use-subagent-delegation";
import type { RosterReachability } from "../../pages/subagent-roster-reachability";

export const FEATURED_MAX = 5;
export const LONG_CONTEXT_MIN = 200_000;
Expand Down Expand Up @@ -46,6 +47,9 @@ export interface SubagentsWorkspaceProps {
available: string[];
models: AgentModelRow[];
chosen: string[];
protocol?: "v1" | "default" | "v2" | null;
rosterReachability?: ReadonlyMap<string, RosterReachability>;
onUseConcurrentV2?: () => void;
busy?: boolean;
rosterDirty?: boolean;
onToggle: (model: string) => void;
Expand Down Expand Up @@ -135,10 +139,22 @@ function ModelChips({ model }: { model: AgentModelRow }) {
);
}

function surfaceLabel(surface: RosterReachability, t: TFn): string {
switch (surface) {
case "both": return t("sub.roster.surface.both");
case "v1": return t("sub.roster.surface.v1");
case "v2": return t("sub.roster.surface.v2");
case "neither": return t("sub.roster.surface.none");
}
}

export default function SubagentsWorkspace({
available,
models,
chosen,
protocol,
rosterReachability,
onUseConcurrentV2,
busy = false,
rosterDirty = false,
onToggle,
Expand Down Expand Up @@ -213,14 +229,37 @@ export default function SubagentsWorkspace({
{ id: "tools", label: t("sub.filter.tools") },
];

// Per-selector collaboration-surface exceptions among the chosen rows.
const rosterStatus = useMemo(() => {
const v1: string[] = [];
const v2: string[] = [];
if (rosterReachability) {
for (const selector of chosen) {
const state = rosterReachability.get(selector);
if (state === "v1") v1.push(selector);
else if (state === "v2") v2.push(selector);
}
}
return { v1, v2 };
}, [chosen, rosterReachability]);

// One notice, only when the legacy "default" protocol splits the roster
// across surfaces. The gate is inherently map-driven: rosterStatus only
// collects entries from a non-empty rosterReachability map, so an empty or
// missing map can never produce a notice. "neither" rows are deliberately
// not listed by name. Names stay in roster order.
const showSplitNotice = protocol === "default" && (rosterStatus.v1.length > 0 || rosterStatus.v2.length > 0);
const v1Names = rosterStatus.v1.map(selector => formatNamespacedModelId(selector, t)).join(", ");
const v2Names = rosterStatus.v2.map(selector => formatNamespacedModelId(selector, t)).join(", ");

return (
<div className="subagents-workspace-shell">
<span className="sr-only" aria-live="polite">{announcement}</span>
<div className="subagents-command-grid">
<section className="subagents-command-card swi-roster" aria-labelledby="active-roster-title">
<section className="subagents-command-card swi-roster" aria-labelledby="configured-roster-title">
<div className="swi-card-head">
<div>
<h2 id="active-roster-title" className="swi-card-title">{t("sub.featured")}</h2>
<h2 id="configured-roster-title" className="swi-card-title">{t("sub.featured")}</h2>
<p className="swi-card-subtitle">{t("sub.rosterCount", { n: chosen.length, max: FEATURED_MAX })}</p>
</div>
<button type="button" className="btn btn-ghost btn-sm" onClick={() => searchRef.current?.focus()}>
Expand All @@ -238,6 +277,11 @@ export default function SubagentsWorkspace({
<ol className="swi-roster-list">
{chosen.map((selector, index) => {
const model = modelForSelector(selector, modelBySelector);
const state = rosterReachability?.get(selector);
// Routed = namespaced row. modelForSelector always returns a
// row with native = (no slash), so this stays computable even
// when catalog metadata failed to load.
const routed = model.native !== true;
const dragging = dragIndex === index;
const dropTarget = overIndex === index && dragIndex !== null && dragIndex !== index;
return (
Expand Down Expand Up @@ -294,7 +338,20 @@ export default function SubagentsWorkspace({
<ModelMark model={model} />
<span className="swi-roster-identity">
<span className="swi-roster-name">{formatNamespacedModelId(selector, t)}</span>
<ModelChips model={model} />
{/* No map entry (e.g. unsaved draft row) => no claim at
all; the sr-only "both" line requires an explicit
"both" entry from the server. */}
{state !== undefined && (state === "both" ? (
<span className="sr-only">{t("sub.roster.status.bothSr")}</span>
) : (
<span className={`swi-roster-status${state === "neither" ? " swi-roster-status--neither" : ""}`}>
{state === "v1"
? t("sub.roster.status.v1Only")
: state === "v2"
? routed ? t("sub.roster.status.routedV2") : t("sub.roster.status.v2Only")
: t("sub.roster.status.notAdvertised")}
</span>
))}
</span>
<span className="swi-roster-actions">
<button
Expand Down Expand Up @@ -325,6 +382,50 @@ export default function SubagentsWorkspace({
</ol>
)}

{showSplitNotice && (
<div className="swi-roster-note">
<IconInfo width={15} height={15} aria-hidden="true" />
<span className="swi-roster-note-text">
{rosterStatus.v1.length > 0 && rosterStatus.v2.length > 0
? t("sub.roster.splitNoticeBoth", { v1Models: v1Names, v2Models: v2Names })
: rosterStatus.v1.length > 0
? t("sub.roster.splitNoticeV2", { models: v1Names })
: t("sub.roster.splitNoticeV1", { models: v2Names })}
</span>
{/* Focus-only navigation: the button never mutates drafts, so it
stays enabled while the roster is busy. The hint travels via
aria-describedby on a dedicated sr-only span — a reliable
accessible description without re-announcing the full note
text that sits directly adjacent. */}
<button
type="button"
className="btn btn-ghost btn-sm swi-roster-note-action"
onClick={() => onUseConcurrentV2?.()}
aria-describedby="swi-roster-note-action-hint"
>{t("sub.roster.useConcurrentV2")}</button>
<span id="swi-roster-note-action-hint" className="sr-only">{t("sub.roster.useConcurrentV2Hint")}</span>
</div>
)}

{rosterReachability && rosterReachability.size > 0 && (
<details className="swi-roster-diagnostics">
<summary>{t("sub.roster.diagnostics")}</summary>
<p>{t("sub.roster.diagnosticsHint")}</p>
<ul className="swi-roster-matrix">
{chosen.map(selector => {
const surface = rosterReachability.get(selector);
if (!surface) return null;
return (
<li className="swi-roster-matrix-row" key={selector}>
<span className="swi-roster-matrix-name">{formatNamespacedModelId(selector, t)}</span>
<span className="swi-roster-matrix-surface">{surfaceLabel(surface, t)}</span>
</li>
);
})}
</ul>
</details>
)}

<div className="swi-card-footer">
<span><IconInfo width={15} height={15} aria-hidden="true" />{t("sub.dragHint")}</span>
<button type="button" className="btn btn-primary" onClick={onSave} disabled={busy || !rosterDirty}>
Expand Down
24 changes: 20 additions & 4 deletions gui/src/i18n/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -570,7 +570,7 @@ export const de: Record<TKey, string> = {
"sub.pageTitle": "Agenten-Kommandozentrale",
"sub.pageSubtitle": "Wähle beworbene Worker-Modelle, Anleitung und Fallbacks für Kindläufe.",
"sub.desktopPickerLimit": "Diese Liste steuert spawn_agent, nicht den Modell-Picker von Codex Desktop. Desktop kann geroutete Provider-IDs per Remote-Allowlist ausblenden; verwende Combos → Natives OpenAI-Alias für eine explizite Kompatibilitätszuordnung.",
"sub.featured": "Aktiver Kader",
"sub.featured": "Konfigurierter Kader",
"sub.noneSelected": "Noch keine Schnellauswahl.",
"sub.models": "Agenten-Bibliothek",
"sub.search": "Modelle suchen",
Expand Down Expand Up @@ -696,10 +696,26 @@ export const de: Record<TKey, string> = {
"sub.catalog.newTaskNote": "Eine neue Aufgabe allein lädt den Codex-Hintergrundworker nicht neu und ändert sein Routing nicht. Anwenden gleicht beides ab und ersetzt nur verifizierte veraltete Worker.",
"sub.catalog.legacyStaleNotice": "Der gespeicherte Katalog weicht von {n} laufenden Codex-Sitzung(en) ab. Dieser Proxy bietet noch kein gesichertes Anwenden; nutze {cmd} als erweiterten Fallback, wenn du sie neu starten möchtest.",
"sub.catalog.notRunningNotice": "Kein Codex-Hintergrundworker läuft. Der nächste Workerstart lädt den gespeicherten Katalog.",
"sub.roster.diagnostics": "Protokolldiagnose",
"sub.roster.diagnosticsHint": "Wo jedes konfigurierte Modell auf den Collab-Oberflächen angeboten wird.",
"sub.roster.excludedNotice": "Gespeicherte Roster-Modelle werden auf der gewählten spawn_agent-Oberfläche derzeit nicht angeboten ({n}): {models}. Die Katalogdatei ist geladen, aber dieses Roster ist nicht vollständig wirksam.",
"sub.workspace.addToFeatured": "{m} zum aktiven Kader hinzufügen",
"sub.workspace.featuredFull": "Aktiver Kader ist voll (max. 5)",
"sub.workspace.removeFromFeatured": "{m} aus dem aktiven Kader entfernen",
"sub.roster.splitNoticeBoth": "Der Codex-Standard teilt dieses Roster auf V1 und V2 auf. Ein V2-Elternagent kann {v1Models} nicht auswählen. Ein V1-Elternagent kann {v2Models} nicht auswählen.",
"sub.roster.splitNoticeV1": "Der Codex-Standard teilt dieses Roster auf V1 und V2 auf. Ein V1-Elternagent kann {models} nicht auswählen.",
"sub.roster.splitNoticeV2": "Der Codex-Standard teilt dieses Roster auf V1 und V2 auf. Ein V2-Elternagent kann {models} nicht auswählen.",
"sub.roster.status.bothSr": "Auf den V1- und V2-Collab-Oberflächen verfügbar.",
"sub.roster.status.notAdvertised": "Nicht angeboten",
"sub.roster.status.routedV2": "Geroutet · V2-kompatibel",
"sub.roster.status.v1Only": "V1 unter Codex-Standard",
"sub.roster.status.v2Only": "Nur V2",
"sub.roster.surface.both": "V1 + V2",
"sub.roster.surface.none": "Nicht angeboten",
"sub.roster.surface.v1": "V1",
"sub.roster.surface.v2": "V2",
"sub.roster.useConcurrentV2": "Parallel v2 für das gesamte Roster verwenden",
"sub.roster.useConcurrentV2Hint": "Scrollt zum Agentenprotokoll-Auswahlfeld und fokussiert es. Es ändert oder speichert nichts.",
"sub.workspace.addToFeatured": "{m} zum konfigurierten Kader hinzufügen",
"sub.workspace.featuredFull": "Konfigurierter Kader ist voll (max. 5)",
"sub.workspace.removeFromFeatured": "{m} aus dem konfigurierten Kader entfernen",
"logs.title": "Anfrage-Protokolle",
"logs.tabLogs": "Protokolle",
"logs.tabDebug": "Diagnose",
Expand Down
24 changes: 20 additions & 4 deletions gui/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -593,7 +593,7 @@ export const en = {
"sub.pageTitle": "Agent Command Center",
"sub.pageSubtitle": "Choose advertised worker models, guidance, and child fallback behavior.",
"sub.desktopPickerLimit": "This roster controls spawn_agent, not the top-level Codex Desktop picker. Desktop may hide routed provider ids with its remote allowlist; use Combos → Native OpenAI alias for an explicit compatibility mapping.",
"sub.featured": "Active Roster",
"sub.featured": "Configured Roster",
"sub.noneSelected": "No quick picks yet.",
"sub.models": "Agent Library",
"sub.search": "Search models",
Expand Down Expand Up @@ -719,10 +719,26 @@ export const en = {
"sub.catalog.newTaskNote": "A new task alone does not reload the Codex background worker or change its routing. Apply reconciles both and replaces only verified stale workers.",
"sub.catalog.legacyStaleNotice": "Saved catalog differs from {n} running Codex session(s). This proxy does not yet expose guarded Apply; use {cmd} as an advanced fallback when ready to restart them.",
"sub.catalog.notRunningNotice": "No Codex background worker is running. The next worker start loads the saved catalog.",
"sub.roster.diagnostics": "Protocol diagnostics",
"sub.roster.diagnosticsHint": "Where each configured model is advertised across collaboration surfaces.",
"sub.roster.excludedNotice": "Saved roster models are not currently advertised on the selected spawn_agent surface ({n}): {models}. The catalog file is loaded, but this roster is not fully effective.",
"sub.workspace.addToFeatured": "Add {m} to active roster",
"sub.workspace.featuredFull": "Active roster is full (max 5)",
"sub.workspace.removeFromFeatured": "Remove {m} from active roster",
"sub.roster.splitNoticeBoth": "Codex defaults split this roster across V1 and V2. A V2 parent cannot select {v1Models}. A V1 parent cannot select {v2Models}.",
"sub.roster.splitNoticeV1": "Codex defaults split this roster across V1 and V2. A V1 parent cannot select {models}.",
"sub.roster.splitNoticeV2": "Codex defaults split this roster across V1 and V2. A V2 parent cannot select {models}.",
"sub.roster.status.bothSr": "Available on V1 and V2 collaboration surfaces.",
"sub.roster.status.notAdvertised": "Not advertised",
"sub.roster.status.routedV2": "Routed · V2 compatible",
"sub.roster.status.v1Only": "V1 under Codex defaults",
"sub.roster.status.v2Only": "V2 only",
"sub.roster.surface.both": "V1 + V2",
"sub.roster.surface.none": "Not advertised",
"sub.roster.surface.v1": "V1",
"sub.roster.surface.v2": "V2",
"sub.roster.useConcurrentV2": "Use Concurrent V2 for the entire roster",
"sub.roster.useConcurrentV2Hint": "Scrolls to the agent protocol selector and focuses it. It does not change or save anything.",
"sub.workspace.addToFeatured": "Add {m} to configured roster",
"sub.workspace.featuredFull": "Configured roster is full (max 5)",
"sub.workspace.removeFromFeatured": "Remove {m} from configured roster",

// logs
"logs.title": "Request Logs",
Expand Down
Loading
Loading