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.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 4 additions & 4 deletions gui/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import Subagents from "./pages/Subagents";
import Logs from "./pages/Logs";
import Usage from "./pages/Usage";
import Storage from "./pages/Storage";
import CodexAuth from "./pages/CodexAuth";
import CodexSet from "./pages/CodexSet";
import Integrations from "./pages/Integrations";
import Startup from "./pages/Startup";
import ErrorBoundary from "./components/ErrorBoundary";
Expand Down Expand Up @@ -35,7 +35,7 @@ const PAGE_TKEY: Record<Page, TKey> = {
logs: "nav.logs",
usage: "nav.usage",
storage: "nav.storage",
"codex-auth": "nav.codexAuth",
"codex-set": "nav.codexSet",
integrations: "nav.integrations",
};

Expand All @@ -58,7 +58,7 @@ type NavEntry = {

const NAV: NavEntry[] = [
{ id: "dashboard", tkey: "nav.dashboard", Icon: IconGrid },
{ id: "codex-auth", tkey: "nav.codexAuth", Icon: IconKey },
{ id: "codex-set", tkey: "nav.codexSet", Icon: IconKey },

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Synchronize the dashboard documentation with Codex Set

This changes the shipped page name and adds the Prompt workflow, but the directly affected documentation still tells users to navigate to Codex Auth—for example, docs-site/src/content/docs/guides/web-dashboard.md lines 50 and 104–106—and does not document the new prompt controls; the translated guides remain stale as well. Update the English dashboard guide and its localized counterparts so the documented navigation and capabilities match the UI.

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

Useful? React with 👍 / 👎.

{ id: "providers", tkey: "nav.providers", Icon: IconServer },
{ id: "models", tkey: "nav.models", Icon: IconBoxes },
{ id: "subagents", tkey: "nav.subagents", Icon: IconBot },
Expand Down Expand Up @@ -336,7 +336,7 @@ export default function App() {
{page === "logs" && <Logs apiBase={API_BASE} />}
{page === "usage" && <Usage apiBase={API_BASE} />}
{page === "storage" && <Storage apiBase={API_BASE} />}
{page === "codex-auth" && <CodexAuth apiBase={API_BASE} />}
{page === "codex-set" && <CodexSet apiBase={API_BASE} />}
{page === "integrations" && <Integrations apiBase={API_BASE} />}
</ErrorBoundary>
</div>
Expand Down
15 changes: 13 additions & 2 deletions gui/src/app-routing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export type Page =
| "logs"
| "usage"
| "storage"
| "codex-auth"
| "codex-set"
| "integrations";

export const VALID_PAGES = new Set<Page>([
Expand All @@ -23,7 +23,7 @@ export const VALID_PAGES = new Set<Page>([
"logs",
"usage",
"storage",
"codex-auth",
"codex-set",
"integrations",
]);

Expand All @@ -35,6 +35,11 @@ export function readPageFromHash(hash?: string): Page {
const pageId = raw.split("/")[0] as Page;
// Legacy: Debug used to be a standalone page; it now lives as a tab on Logs.
if (pageId === ("debug" as Page)) return "logs";
// Legacy: Codex Auth became the Multi-auth tab of Codex Set. #codex-auth is a
// bookmarkable URL that has shipped, and use-app-route-state reads the initial
// page straight from the hash, so without this an old bookmark lands on an
// unknown page.
if (pageId === ("codex-auth" as Page)) return "codex-set";
// Legacy: Combos, Routing, and Lab are Models tabs now. Resolve the destination page
// immediately so a cold legacy hash never flashes Dashboard before replacement.
if (pageId === ("combos" as Page)
Expand Down Expand Up @@ -98,6 +103,7 @@ export const INTEGRATION_TAB_HASHES = [
export function hashBelongsToPage(rawHash: string, page: Page): boolean {
return rawHash === page
|| (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)))
Expand Down Expand Up @@ -125,6 +131,11 @@ export function resolveAppHashChange(rawHash: string): AppHashChangeAction {
return { page: "logs", replaceTo: "logs/debug" };
}

/* Legacy: Codex Auth is now the Multi-auth tab of Codex Set. */
if (rawHash === "codex-auth" || rawHash.startsWith("codex-auth/")) {
return { page: "codex-set", replaceTo: "codex-set" };
}

/* Legacy Models pages. Delimiter-aware prefix arms preserve nested legacy bookmarks. */
if (rawHash === "combos" || rawHash.startsWith("combos/")) {
return { page: "models", replaceTo: "models/combos" };
Expand Down
117 changes: 117 additions & 0 deletions gui/src/components/codex-set/PromptLayerDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { useCallback, useEffect, useRef } from "react";
import { useT } from "../../i18n/shared";
import type { LayerDescriptorDto, ToggleStateDto } from "../../pages/codex-set-prompt";
import { CLASS_LABEL_KEYS, LAYER_ABOUT_KEYS, LAYER_CONDITION_KEYS, LAYER_LABEL_KEYS } from "./prompt-layer-copy";

/**
* Read-only detail for a built-in layer (ask item 8).
*
* There is no rendered prompt text here, and that is deliberate. Codex exposes
* no API for a layer's assembled body, and reconstructing one would mean
* reimplementing world_state.rs against a moving target. The dialog explains the
* layer, names its key, and shows this file's value - the honest scope. Saying so
* plainly beats an empty panel the reader has to interpret.
*
* No textarea, no Save. Escape closes and returns focus to the trigger.
*/
export default function PromptLayerDialog({
descriptor,
toggle,
onClose,
}: {
descriptor: LayerDescriptorDto;
toggle: ToggleStateDto | undefined;
onClose: () => void;
}) {
const t = useT();
const dialogRef = useRef<HTMLDialogElement>(null);
const titleId = "codex-set-layer-dialog-" + descriptor.id;

useEffect(() => {
const dialog = dialogRef.current;
// Remember who opened this. A modal that closes to the document root strands a
// keyboard user, and the row they were reading is the only sensible return.
const opener = document.activeElement as HTMLElement | null;
if (dialog && !dialog.open) dialog.showModal();
// A parent re-render that drops this node while the dialog is open would
// otherwise leave the top layer occupied.
return () => {
if (dialog?.open) dialog.close();
if (opener && typeof opener.focus === "function") opener.focus();
};
}, []);

const handleCancel = useCallback((event: React.SyntheticEvent) => {
// Escape arrives as `cancel`. Prevent the default close so React owns the
// unmount and the caller can restore focus to the row that opened this.
event.preventDefault();
onClose();
}, [onClose]);

const classKey = CLASS_LABEL_KEYS[descriptor.class];
// Same fallback the row uses. The wire response is cast, not validated, so a
// newer runtime CAN send an id this build has no copy for - and a dialog whose
// title and body silently render blank is worse than one that admits the gap.
const labelKey = LAYER_LABEL_KEYS[descriptor.id];
const aboutKey = LAYER_ABOUT_KEYS[descriptor.id];
const conditionKey = LAYER_CONDITION_KEYS[descriptor.id];

return (
<dialog ref={dialogRef} className="modal-overlay" aria-labelledby={titleId} onCancel={handleCancel}>
<button type="button" className="modal-backdrop-dismiss" aria-label={t("common.close")} tabIndex={-1} onClick={onClose} />
<div className="modal-card codex-set-layer-dialog" onClick={event => event.stopPropagation()} role="document">
<div className="modal-head">
<h3 id={titleId}>{labelKey ? t(labelKey) : descriptor.id}</h3>
<button type="button" className="btn btn-ghost btn-sm" onClick={onClose}>{t("common.close")}</button>
</div>

<p className="muted small">{aboutKey ? t(aboutKey) : t("codexSet.dialog.unknownLayer")}</p>

<div className="codex-set-layer-dialog__line">
<span className="muted text-label">{t("codexSet.dialog.class")}</span>
<span>{t(classKey)}</span>
</div>

{descriptor.key && (
<div className="codex-set-layer-dialog__line">
<span className="muted text-label">{t("codexSet.dialog.key")}</span>
<code className="api-code">{descriptor.key}</code>
<button
type="button"
className="btn btn-ghost btn-sm"
onClick={() => { void navigator.clipboard?.writeText(descriptor.key ?? ""); }}
>
{t("codexSet.dialog.copyKey")}
</button>
</div>
)}

{descriptor.class === "config-toggle" && toggle && (
<div className="codex-set-layer-dialog__line">
<span className="muted text-label">{t("codexSet.dialog.fileValue")}</span>
<span>
{toggle.userFileValue === null
? t("codexSet.dialog.absentDefault", { value: String(toggle.default) })
// The default travels with an explicit value too: knowing the file says
// false is only half the answer without knowing what it would be otherwise.
: t("codexSet.dialog.setValue", {
value: String(toggle.userFileValue),
fallback: String(toggle.default),
})}
</span>
</div>
)}

{descriptor.class === "runtime-conditional" && conditionKey && (
<p className="muted small">{t(conditionKey)}</p>
)}

{/*
Stated, not implied. A dialog that simply omitted the body would read as
a loading failure to anyone who expected one.
*/}
<p className="muted small codex-set-layer-dialog__no-text">{t("codexSet.dialog.noRenderedText")}</p>
</div>
</dialog>
);
}
98 changes: 98 additions & 0 deletions gui/src/components/codex-set/PromptLayerRow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { useT } from "../../i18n/shared";
import { navigateHash } from "../../hash-routing";
import type { LayerDescriptorDto, ToggleStateDto } from "../../pages/codex-set-prompt";
import { LAYER_LABEL_KEYS } from "./prompt-layer-copy";

/**
* One row of the prompt-layer list.
*
* Row kind comes from `descriptor.class`, which is LAYER_INVENTORY, which is the
* taxonomy in devlog 001 section 4. That is the whole mapping - there is no
* heuristic and no second table, so a row can never be classified two ways.
*
* A layer with no upstream off-switch renders with NO switch element at all:
* not a disabled checkbox, not a greyed toggle. A disabled control claims the
* capability exists and is temporarily unavailable, which is false for these
* layers - Codex has no way to suppress them anywhere. This is ask item 9 at the
* rendering layer; the API refuses the same ids independently.
*
* The wording distinction matters too. `base` and `runtime-conditional` rows have
* no off-switch anywhere. `feature-gated` rows ARE disableable - through
* [features], not from this page. Applying the stronger sentence to both would
* tell a user a setting does not exist when it does.
*/
export default function PromptLayerRow({
descriptor,
toggle,
busy,
writesRefused,
onToggle,
onOpen,
}: {
descriptor: LayerDescriptorDto;
toggle: ToggleStateDto | undefined;
busy: boolean;
writesRefused: boolean;
onToggle: (id: string, enabled: boolean) => void;
onOpen: (id: string) => void;
}) {
const t = useT();
const labelKey = LAYER_LABEL_KEYS[descriptor.id];
// An id this build has no copy for is shown verbatim rather than blank: a newer
// runtime listing a layer we do not know about is information, not an error.
const label = labelKey ? t(labelKey) : descriptor.id;
const checked = toggle?.defaultedUserValue ?? descriptor.default ?? true;

return (
<li className="codex-set-prompt__row" data-layer-id={descriptor.id} data-layer-class={descriptor.class}>
<button
type="button"
className="link-btn codex-set-prompt__name"
onClick={() => onOpen(descriptor.id)}
>
{label}
</button>

{descriptor.key && <code className="codex-set-prompt__key">{descriptor.key}</code>}

{descriptor.class === "config-toggle" ? (
<label className="switch">
<input
type="checkbox"
role="switch"
aria-label={label}
checked={checked}
disabled={busy || writesRefused}
onChange={e => { onToggle(descriptor.id, e.target.checked); }}
/>
<span className="switch-track" aria-hidden="true" />
</label>
) : descriptor.class === "feature-gated" ? (
// Configurable, just not here. Naming the governing key is the whole point -
// "always on" would be a lie about a setting the user can actually change -
// and the link is what turns that from a dead end into a destination.
<span className="codex-set-prompt__note">
{t("codexSet.row.featureGated")}{" "}
{/*
A button through `navigateHash`, not a bare fragment href. Nothing in
the document carries `id="integrations/codex"` - it is a route, so the
anchor pointed at a target that does not exist and the app router had
to rescue the click.
*/}
<button
type="button"
className="link-btn"
onClick={() => navigateHash("integrations/codex")}
>
{t("codexSet.row.openFeatures")}
</button>
</span>
) : (
// base and runtime-conditional: no off-switch exists anywhere in Codex.
<span className="codex-set-prompt__note codex-set-prompt__note--locked">
{t("codexSet.row.alwaysOn")}
</span>
)}
</li>
);
}
91 changes: 91 additions & 0 deletions gui/src/components/codex-set/prompt-layer-copy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import type { TKey } from "../../i18n/en";
import type { LayerClass } from "../../pages/codex-set-prompt";

/**
* Every layer id in LAYER_INVENTORY, as a finite union.
*
* This is what makes the maps below exhaustive. `Record<string, TKey>` does NOT:
* `string` has no required members, so deleting an entry typechecks cleanly and
* the non-null assertion at the call site hides the missing lookup until a user
* sees a blank row. A union has required members, so a gap is a build failure.
*/
export type LayerId =
| "base-instructions"
| "model-switch"
| "personality"
| "context-window-guidance"
| "realtime"
| "agents-md"
| "permissions"
| "collaboration"
| "environment"
| "environments-instructions"
| "apps"
| "plugins"
| "tools"
| "skills"
| "multi-agent-mode";

/**
* Layer id -> i18n key, written out rather than built by string concatenation.
*
* A template like `("codexSet.layer." + id) as never` typechecks whether or not
* the key exists, so a missing translation reaches the user as a raw key instead
* of failing the build.
*/
export const LAYER_LABEL_KEYS: Record<LayerId, TKey> = {
"base-instructions": "codexSet.layer.base-instructions",
"model-switch": "codexSet.layer.model-switch",
personality: "codexSet.layer.personality",
"context-window-guidance": "codexSet.layer.context-window-guidance",
realtime: "codexSet.layer.realtime",
"agents-md": "codexSet.layer.agents-md",
permissions: "codexSet.layer.permissions",
collaboration: "codexSet.layer.collaboration",
environment: "codexSet.layer.environment",
"environments-instructions": "codexSet.layer.environments-instructions",
apps: "codexSet.layer.apps",
plugins: "codexSet.layer.plugins",
tools: "codexSet.layer.tools",
skills: "codexSet.layer.skills",
"multi-agent-mode": "codexSet.layer.multi-agent-mode",
};

export const LAYER_ABOUT_KEYS: Record<LayerId, TKey> = {
"base-instructions": "codexSet.about.base-instructions",
"model-switch": "codexSet.about.model-switch",
personality: "codexSet.about.personality",
"context-window-guidance": "codexSet.about.context-window-guidance",
realtime: "codexSet.about.realtime",
"agents-md": "codexSet.about.agents-md",
permissions: "codexSet.about.permissions",
collaboration: "codexSet.about.collaboration",
environment: "codexSet.about.environment",
"environments-instructions": "codexSet.about.environments-instructions",
apps: "codexSet.about.apps",
plugins: "codexSet.about.plugins",
tools: "codexSet.about.tools",
skills: "codexSet.about.skills",
"multi-agent-mode": "codexSet.about.multi-agent-mode",
};

/**
* Only runtime-conditional rows state a condition, so this map is deliberately
* PARTIAL - a Partial<Record<LayerId, TKey>> rather than a total one, because
* requiring an entry for every layer would mean inventing conditions that do not
* exist.
*/
export const LAYER_CONDITION_KEYS: Partial<Record<LayerId, TKey>> = {
"model-switch": "codexSet.condition.model-switch",
realtime: "codexSet.condition.realtime",
"agents-md": "codexSet.condition.agents-md",
plugins: "codexSet.condition.plugins",
};

export const CLASS_LABEL_KEYS: Record<LayerClass, TKey> = {
base: "codexSet.class.base",
"config-toggle": "codexSet.class.config-toggle",
"feature-gated": "codexSet.class.feature-gated",
"runtime-conditional": "codexSet.class.runtime-conditional",
"extension-unknown": "codexSet.class.extension-unknown",
};
Loading
Loading