;
+ kind: ComposerMetaPickerKind;
+ value: string;
+ onMouseDown: () => void;
+}): UiNode {
+ return (
+ {
+ event.preventDefault();
+ onMouseDown();
+ }}
+ >
+ {value}
+
+ );
+}
+
+function ComposerMetaChoicePopover({
+ kind,
+ choices,
+ left,
+ onClose,
+}: {
+ kind: ComposerMetaPickerKind;
+ choices: ToolbarChoice[];
+ left: number;
+ onClose: () => void;
+}): UiNode {
+ const style = {
+ "--codex-panel-composer-meta-popover-left": `${String(Math.round(left))}px`,
+ };
+ return (
+
+ {choices.map((choice) => (
+
+ ))}
+
+ );
+}
+
+function ComposerMetaChoice({ choice, onClose }: { choice: ToolbarChoice; onClose: () => void }): UiNode {
+ const onSelect = () => {
+ if (choice.disabled) return;
+ choice.onClick();
+ onClose();
+ };
+ return (
+ {
+ event.preventDefault();
+ onSelect();
+ }}
+ >
+ {choice.label}
+
+ );
}
interface ComposerSendMode {
diff --git a/src/features/chat/view-model.ts b/src/features/chat/view-model.ts
index bf7fa5d2..8ad811d7 100644
--- a/src/features/chat/view-model.ts
+++ b/src/features/chat/view-model.ts
@@ -27,11 +27,14 @@ export interface RuntimeSnapshotInput {
export interface ComposerMetaViewModel {
fatal: string | null;
context: ComposerContextMeterViewModel;
+ statusSummary: string;
model: string;
effort: string | null;
planActive: boolean;
autoReviewActive: boolean;
fastActive: boolean;
+ modelChoices?: ToolbarChoice[];
+ effortChoices?: ToolbarChoice[];
}
export interface ComposerContextMeterCellViewModel {
@@ -160,6 +163,7 @@ export function composerMetaViewModel(state: ChatState, snapshot: RuntimeSnapsho
return {
fatal: "Codex app-server disconnected",
context: contextComposerMeter(null),
+ statusSummary: "Codex app-server disconnected",
model: "",
effort: null,
planActive: false,
@@ -172,17 +176,53 @@ export function composerMetaViewModel(state: ChatState, snapshot: RuntimeSnapsho
const context = contextSummary(snapshot);
const model = currentModel(snapshot, config);
const effort = currentReasoningEffort(snapshot, config);
+ const composerContext = contextComposerMeter(context?.percent ?? null);
+ const compactEffort = effort ? compactReasoningEffortLabel(effort) : null;
+ const planActive = state.selectedCollaborationMode === "plan";
+ const reviewActive = autoReviewActive(snapshot, config);
+ const fastActive = fastModeActive(snapshot, config);
return {
fatal: null,
- context: contextComposerMeter(context?.percent ?? null),
+ context: composerContext,
+ statusSummary: composerStatusSummary({
+ context: composerContext,
+ model: model ?? "default",
+ effort: compactEffort,
+ planActive,
+ autoReviewActive: reviewActive,
+ fastActive,
+ }),
model: model ?? "default",
- effort: effort ? compactReasoningEffortLabel(effort) : null,
- planActive: state.selectedCollaborationMode === "plan",
- autoReviewActive: autoReviewActive(snapshot, config),
- fastActive: fastModeActive(snapshot, config),
+ effort: compactEffort,
+ planActive,
+ autoReviewActive: reviewActive,
+ fastActive,
};
}
+function composerStatusSummary(input: {
+ context: ComposerContextMeterViewModel;
+ model: string;
+ effort: string | null;
+ planActive: boolean;
+ autoReviewActive: boolean;
+ fastActive: boolean;
+}): string {
+ const context = input.context.percent === "--%" ? "Context unavailable" : `Context ${input.context.percent.trim()}`;
+ return [
+ context,
+ `plan ${onOffLabel(input.planActive)}`,
+ `auto-review ${onOffLabel(input.autoReviewActive)}`,
+ `fast ${onOffLabel(input.fastActive)}`,
+ `model ${input.model}`,
+ `reasoning effort ${input.effort ?? "default"}`,
+ ].join(", ");
+}
+
+function onOffLabel(active: boolean): string {
+ return active ? "on" : "off";
+}
+
export function toolbarViewModel(input: ToolbarViewModelInput): ToolbarViewModel {
const { state, snapshot } = input;
const config = readRuntimeConfig(state.effectiveConfig);
diff --git a/src/features/chat/view-snapshot.ts b/src/features/chat/view-snapshot.ts
index d39ff848..d2d56ab4 100644
--- a/src/features/chat/view-snapshot.ts
+++ b/src/features/chat/view-snapshot.ts
@@ -78,6 +78,12 @@ export function composerSlotSnapshot(state: ChatState, activeComposerThreadName:
activeTurnId(state),
state.activeModel,
state.activeReasoningEffort,
+ state.activeCollaborationMode,
+ state.activeServiceTier,
+ state.activeApprovalsReviewer,
+ state.selectedCollaborationMode,
+ state.requestedServiceTier,
+ state.requestedApprovalsReviewer,
state.requestedModel,
state.requestedReasoningEffort,
state.tokenUsage,
diff --git a/src/features/chat/view.ts b/src/features/chat/view.ts
index 98f24a19..681de71f 100644
--- a/src/features/chat/view.ts
+++ b/src/features/chat/view.ts
@@ -556,7 +556,15 @@ export class CodexChatView extends ItemView {
}
private composerMetaViewModel() {
- return buildComposerMetaViewModel(this.state, this.runtimeSnapshot());
+ return {
+ ...buildComposerMetaViewModel(this.state, this.runtimeSnapshot()),
+ ...runtimeToolbarChoices({
+ state: this.state,
+ snapshot: this.runtimeSnapshot(),
+ setRequestedModel: (model) => void this.setRequestedModelFromUi(model),
+ setRequestedReasoningEffort: (effort) => void this.setRequestedReasoningEffortFromUi(effort),
+ }),
+ };
}
private activeComposerThreadName(): string | null {
diff --git a/src/styles/33-chat-composer.css b/src/styles/33-chat-composer.css
index b68b9bca..55dc0040 100644
--- a/src/styles/33-chat-composer.css
+++ b/src/styles/33-chat-composer.css
@@ -25,6 +25,7 @@
}
.codex-panel__composer-meta {
+ position: relative;
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
min-width: 0;
@@ -37,6 +38,7 @@
}
.codex-panel__composer-meta-status {
+ position: relative;
box-sizing: border-box;
display: flex;
min-width: 0;
@@ -47,6 +49,24 @@
white-space: nowrap;
}
+.codex-panel__composer-meta-summary {
+ position: absolute;
+ width: var(--codex-panel-rail-width);
+ height: var(--codex-panel-rail-width);
+ padding: 0;
+ overflow: hidden;
+ clip-path: inset(50%);
+ white-space: nowrap;
+}
+
+.codex-panel__composer-meta-status-visual {
+ display: flex;
+ flex: 0 0 auto;
+ min-width: 0;
+ gap: var(--size-4-2);
+ align-items: center;
+}
+
.codex-panel__composer-meta-context {
display: inline-flex;
flex: 0 0 auto;
@@ -117,11 +137,54 @@
stroke-width: 2;
}
-.codex-panel__composer-meta-model,
-.codex-panel__composer-meta-effort {
+.codex-panel__composer-meta-value.codex-panel__composer-meta-value {
+ display: inline-flex;
+ min-width: 0;
+ align-items: center;
+ text-align: left;
white-space: nowrap;
}
+.codex-panel__composer-meta-popover {
+ position: absolute;
+ bottom: calc(100% + var(--codex-panel-panel-gap));
+ left: var(--codex-panel-composer-meta-popover-left);
+ z-index: var(--layer-popover, 30);
+ display: flex;
+ flex-direction: column;
+ width: max-content;
+ max-width: calc(100% - var(--codex-panel-composer-meta-popover-left));
+ max-height: min(var(--codex-panel-size-toolbar-panel-max-height), 36vh);
+ overflow-y: auto;
+ border: var(--codex-panel-border);
+ border-radius: var(--codex-panel-card-radius);
+ background: var(--codex-panel-surface);
+ box-shadow: var(--shadow-s);
+ padding: var(--codex-panel-panel-gap);
+}
+
+.codex-panel__composer-meta-option {
+ overflow: hidden;
+ min-width: 0;
+ padding: var(--size-4-1) var(--size-4-2);
+ color: var(--text-muted);
+ font: inherit;
+ line-height: var(--line-height-tight);
+ text-align: left;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.codex-panel__composer-meta-option:where(:hover, :focus, :focus-visible, :active):not(.is-disabled) {
+ color: var(--nav-item-color-hover, var(--text-normal));
+ background: var(--background-modifier-hover);
+ outline: none;
+}
+
+.codex-panel__composer-meta-option.is-disabled {
+ opacity: 0.45;
+}
+
.codex-panel__composer-meta--fatal {
color: var(--text-muted);
}
diff --git a/tests/features/chat/chat-composer-controller.test.ts b/tests/features/chat/chat-composer-controller.test.ts
index 4a6b7e23..fe12ed88 100644
--- a/tests/features/chat/chat-composer-controller.test.ts
+++ b/tests/features/chat/chat-composer-controller.test.ts
@@ -35,6 +35,7 @@ describe("ChatComposerController", () => {
],
percent: "--%",
},
+ statusSummary: "Context unavailable, plan off, auto-review off, fast off, model default, reasoning effort default",
model: "default",
effort: null,
planActive: false,
@@ -42,6 +43,9 @@ describe("ChatComposerController", () => {
fastActive: false,
}),
currentModelForSuggestions: () => null,
+ togglePlan: vi.fn(),
+ toggleAutoReview: vi.fn(),
+ toggleFast: vi.fn(),
renderIfDetached: vi.fn(),
onDraftChange: vi.fn(),
onComposerResize: vi.fn(),
@@ -64,6 +68,56 @@ describe("ChatComposerController", () => {
expect(composer(parent).getAttribute("aria-expanded")).toBe("false");
expect(composer(parent).hasAttribute("aria-activedescendant")).toBe(false);
});
+
+ it("delegates composer runtime toggles without forcing a local rerender", () => {
+ const stateStore = createChatStateStore();
+ const parent = document.createElement("div");
+ const togglePlan = vi.fn();
+ const controller = new ChatComposerController({
+ app: app(),
+ stateStore,
+ viewId: "view",
+ sendShortcut: () => "enter",
+ scrollThreadFromComposerEdges: () => false,
+ canInterrupt: () => false,
+ composerPlaceholder: () => "Ask Codex to work on this task...",
+ composerMeta: () => ({
+ fatal: null,
+ context: {
+ cells: [
+ { text: "⣀", placeholder: true },
+ { text: "⣀", placeholder: true },
+ { text: "⣀", placeholder: true },
+ { text: "⣀", placeholder: true },
+ ],
+ percent: "--%",
+ },
+ statusSummary: "Context unavailable, plan off, auto-review off, fast off, model default, reasoning effort default",
+ model: "default",
+ effort: null,
+ planActive: false,
+ autoReviewActive: false,
+ fastActive: false,
+ }),
+ currentModelForSuggestions: () => null,
+ togglePlan,
+ toggleAutoReview: vi.fn(),
+ toggleFast: vi.fn(),
+ renderIfDetached: vi.fn(),
+ onDraftChange: vi.fn(),
+ onComposerResize: vi.fn(),
+ onSubmit: vi.fn(),
+ onThreadScrollFromComposer: vi.fn(),
+ });
+
+ controller.render(parent);
+ expect(parent.querySelector(".codex-panel__composer-meta-icon")?.classList.contains("is-active")).toBe(false);
+
+ parent.querySelector(".codex-panel__composer-meta-icon")?.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
+
+ expect(togglePlan).toHaveBeenCalledOnce();
+ expect(parent.querySelector(".codex-panel__composer-meta-icon")?.classList.contains("is-active")).toBe(false);
+ });
});
function app(): App {
diff --git a/tests/features/chat/ui/renderers/composer.test.ts b/tests/features/chat/ui/renderers/composer.test.ts
index be739590..99fc7d29 100644
--- a/tests/features/chat/ui/renderers/composer.test.ts
+++ b/tests/features/chat/ui/renderers/composer.test.ts
@@ -15,6 +15,9 @@ function composerCallbacks() {
onUpdateSuggestions: vi.fn(),
onKeydown: vi.fn(),
onSendOrInterrupt: vi.fn(),
+ onTogglePlan: vi.fn(),
+ onToggleAutoReview: vi.fn(),
+ onToggleFast: vi.fn(),
onSuggestionHover: vi.fn(),
onSuggestionInsert: vi.fn(),
};
@@ -43,7 +46,7 @@ describe("composer renderer decisions", () => {
expect(composer.getAttribute("placeholder")).toBe("Ask Codex to work on “Renamed thread”...");
});
- it("renders composer meta as non-interactive context and runtime text", () => {
+ it("renders composer meta as interactive context and runtime text without changing normal text", () => {
const parent = document.createElement("div");
renderComposerShell(parent, "view", "", false, false, "Ask Codex to work on this task...", [], 0, composerCallbacks(), {
@@ -57,20 +60,33 @@ describe("composer renderer decisions", () => {
],
percent: "42%",
},
+ statusSummary: "Context 42%, plan on, auto-review off, fast on, model gpt-5.5, reasoning effort high",
model: "gpt-5.5",
effort: "high",
planActive: true,
autoReviewActive: false,
fastActive: true,
+ modelChoices: [
+ { label: "gpt-5.5", selected: true, onClick: vi.fn() },
+ { label: "gpt-5.4", onClick: vi.fn() },
+ ],
+ effortChoices: [
+ { label: "medium", onClick: vi.fn() },
+ { label: "high", selected: true, onClick: vi.fn() },
+ ],
});
const meta = parent.querySelector(".codex-panel__composer-meta");
- const statusItems = Array.from(parent.querySelectorAll(".codex-panel__composer-meta-status > span"));
+ const statusItems = Array.from(parent.querySelectorAll(".codex-panel__composer-meta-status-visual > span"));
const fields = Array.from(parent.querySelectorAll(".codex-panel__composer-meta-field"));
const contextDots = Array.from(parent.querySelectorAll(".codex-panel__composer-meta-context-dot"));
const modeIcons = Array.from(parent.querySelectorAll(".codex-panel__composer-meta-icon"));
+ const statusSummary = parent.querySelector(".codex-panel__composer-meta-summary");
+ const statusVisual = parent.querySelector(".codex-panel__composer-meta-status-visual");
expect(meta?.getAttribute("aria-hidden")).toBeNull();
- expect(meta?.textContent).toBe("|⣿⣶⣀⣀42%|gpt-5.5|high");
+ expect(statusSummary?.textContent).toBe("Context 42%, plan on, auto-review off, fast on, model gpt-5.5, reasoning effort high");
+ expect(statusVisual?.getAttribute("aria-hidden")).toBe("true");
+ expect(statusVisual?.textContent).toBe("|⣿⣶⣀⣀42%|gpt-5.5|high");
expect(statusItems.map((item) => item.className)).toEqual([
"codex-panel__composer-meta-modes",
"codex-panel__composer-meta-separator",
@@ -79,7 +95,7 @@ describe("composer renderer decisions", () => {
"codex-panel__composer-meta-field codex-panel__composer-meta-field--effort",
]);
expect(fields.map((field) => field.textContent)).toEqual(["|gpt-5.5", "|high"]);
- expect(parent.querySelector(".codex-panel__composer-meta-status")?.getAttribute("aria-hidden")).toBe("true");
+ expect(parent.querySelector(".codex-panel__composer-meta-status")?.getAttribute("aria-hidden")).toBeNull();
expect(parent.querySelector(".codex-panel__composer-meta-context")?.textContent).toBe("⣿⣶⣀⣀42%");
expect(contextDots.map((dot) => dot.textContent)).toEqual(["⣿", "⣶", "⣀", "⣀"]);
expect(contextDots.map((dot) => dot.classList.contains("is-placeholder"))).toEqual([false, false, true, true]);
@@ -87,10 +103,107 @@ describe("composer renderer decisions", () => {
expect(parent.querySelector(".codex-panel__composer-meta-effort")?.textContent).toBe("high");
expect(modeIcons.map((icon) => icon.dataset["icon"])).toEqual(["list-todo", "shield", "zap"]);
expect(modeIcons.map((icon) => icon.classList.contains("is-active"))).toEqual([true, false, true]);
+ expect(modeIcons.map((icon) => icon.tagName)).toEqual(["SPAN", "SPAN", "SPAN"]);
+ expect(modeIcons.map((icon) => icon.getAttribute("role"))).toEqual([null, null, null]);
+ expect(modeIcons.map((icon) => icon.getAttribute("tabindex"))).toEqual([null, null, null]);
+ expect(modeIcons.map((icon) => icon.getAttribute("aria-label"))).toEqual([null, null, null]);
+ expect(modeIcons.every((icon) => icon.classList.contains("codex-panel__composer-meta-trigger"))).toBe(true);
+ expect(modeIcons.every((icon) => !icon.className.includes("clickable-icon"))).toBe(true);
+ expect(parent.querySelector(".codex-panel__composer-meta-model")?.tagName).toBe("SPAN");
+ expect(parent.querySelector(".codex-panel__composer-meta-effort")?.tagName).toBe("SPAN");
+ expect(parent.querySelector(".codex-panel__composer-meta-model")?.getAttribute("role")).toBeNull();
+ expect(parent.querySelector(".codex-panel__composer-meta-effort")?.getAttribute("tabindex")).toBeNull();
expect(parent.querySelector(".codex-panel__composer-action.codex-panel__send")).not.toBeNull();
expect(parent.querySelector(".codex-panel__new-chat")).toBeNull();
});
+ it("toggles composer runtime controls and opens separate lightweight pickers", async () => {
+ const parent = document.createElement("div");
+ const callbacks = composerCallbacks();
+ const selectModel = vi.fn();
+ const selectEffort = vi.fn();
+
+ renderComposerShell(parent, "view", "", false, false, "Ask Codex to work on this task...", [], 0, callbacks, {
+ fatal: null,
+ context: {
+ cells: [
+ { text: "⣿", placeholder: false },
+ { text: "⣶", placeholder: false },
+ { text: "⣀", placeholder: true },
+ { text: "⣀", placeholder: true },
+ ],
+ percent: "42%",
+ },
+ statusSummary: "Context 42%, plan on, auto-review off, fast on, model gpt-5.5, reasoning effort high",
+ model: "gpt-5.5",
+ effort: "high",
+ planActive: true,
+ autoReviewActive: false,
+ fastActive: true,
+ modelChoices: [
+ { label: "gpt-5.5", selected: true, onClick: vi.fn() },
+ { label: "gpt-5.4", onClick: selectModel },
+ ],
+ effortChoices: [
+ { label: "medium", onClick: selectEffort },
+ { label: "high", selected: true, onClick: vi.fn() },
+ ],
+ });
+
+ const [planButton, reviewButton, fastButton] = Array.from(parent.querySelectorAll(".codex-panel__composer-meta-icon"));
+ planButton?.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
+ reviewButton?.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
+ fastButton?.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
+ expect(callbacks.onTogglePlan).toHaveBeenCalledOnce();
+ expect(callbacks.onToggleAutoReview).toHaveBeenCalledOnce();
+ expect(callbacks.onToggleFast).toHaveBeenCalledOnce();
+
+ parent.querySelector(".codex-panel__composer-meta-model")?.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
+ await waitForAsyncWork(() => {
+ expect(parent.querySelector(".codex-panel__composer-meta-popover--model")?.textContent).toBe("gpt-5.5gpt-5.4");
+ });
+ const modelOptions = Array.from(parent.querySelectorAll(".codex-panel__composer-meta-option"));
+ expect(modelOptions.map((option) => option.getAttribute("role"))).toEqual([null, null]);
+ expect(modelOptions.map((option) => option.getAttribute("tabindex"))).toEqual([null, null]);
+ expect(modelOptions.map((option) => option.getAttribute("aria-selected"))).toEqual([null, null]);
+ expect(modelOptions.every((option) => !option.classList.contains("is-selected"))).toBe(true);
+ expect(modelOptions.every((option) => !option.classList.contains("codex-panel-ui__nav-item"))).toBe(true);
+ expect(modelOptions.every((option) => !option.classList.contains("codex-panel__toolbar-panel-item"))).toBe(true);
+
+ parent.querySelector(".codex-panel__composer-meta-effort")?.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
+ await waitForAsyncWork(() => {
+ expect(parent.querySelector(".codex-panel__composer-meta-popover--model")).toBeNull();
+ expect(parent.querySelector(".codex-panel__composer-meta-popover--effort")?.textContent).toBe("mediumhigh");
+ });
+
+ parent.querySelector(".codex-panel__composer-meta-option")?.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
+ expect(selectEffort).toHaveBeenCalledOnce();
+ await waitForAsyncWork(() => {
+ expect(parent.querySelector(".codex-panel__composer-meta-popover")).toBeNull();
+ });
+
+ parent.querySelector(".codex-panel__composer-meta-model")?.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
+ await waitForAsyncWork(() => {
+ expect(parent.querySelector(".codex-panel__composer-meta-popover--model")).not.toBeNull();
+ });
+ parent
+ .querySelectorAll(".codex-panel__composer-meta-option")[1]
+ ?.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
+ expect(selectModel).toHaveBeenCalledOnce();
+ await waitForAsyncWork(() => {
+ expect(parent.querySelector(".codex-panel__composer-meta-popover")).toBeNull();
+ });
+
+ parent.querySelector(".codex-panel__composer-meta-model")?.dispatchEvent(new MouseEvent("mousedown", { bubbles: true }));
+ await waitForAsyncWork(() => {
+ expect(parent.querySelector(".codex-panel__composer-meta-popover--model")).not.toBeNull();
+ });
+ document.dispatchEvent(new Event("mousedown", { bubbles: true }));
+ await waitForAsyncWork(() => {
+ expect(parent.querySelector(".codex-panel__composer-meta-popover")).toBeNull();
+ });
+ });
+
it("hides composer meta fields only after measured overflow", async () => {
const parent = document.createElement("div");
@@ -105,6 +218,7 @@ describe("composer renderer decisions", () => {
],
percent: "42%",
},
+ statusSummary: "Context 42%, plan on, auto-review off, fast on, model gpt-5.5, reasoning effort high",
model: "gpt-5.5",
effort: "high",
planActive: true,
@@ -147,6 +261,7 @@ describe("composer renderer decisions", () => {
],
percent: "--%",
},
+ statusSummary: "Codex app-server disconnected",
model: "",
effort: null,
planActive: false,
diff --git a/tests/features/chat/view-connection.test.ts b/tests/features/chat/view-connection.test.ts
index b86229bc..9dbeeb29 100644
--- a/tests/features/chat/view-connection.test.ts
+++ b/tests/features/chat/view-connection.test.ts
@@ -336,6 +336,9 @@ describe("CodexChatView connection lifecycle", () => {
expect(composerSlotSnapshot({ ...state, effectiveConfig: effectiveConfig("gpt-updated") }, null)).not.toBe(base);
expect(composerSlotSnapshot({ ...state, requestedModel: { kind: "set", value: "gpt-requested" } }, null)).not.toBe(base);
+ expect(composerSlotSnapshot({ ...state, selectedCollaborationMode: "plan" }, null)).not.toBe(base);
+ expect(composerSlotSnapshot({ ...state, requestedApprovalsReviewer: { kind: "set", value: "auto_review" } }, null)).not.toBe(base);
+ expect(composerSlotSnapshot({ ...state, requestedServiceTier: { kind: "set", value: "fast" } }, null)).not.toBe(base);
expect(composerSlotSnapshot({ ...state, availableSkills: [skillFixture("reader")] }, null)).not.toBe(base);
});
diff --git a/tests/features/chat/view-model.test.ts b/tests/features/chat/view-model.test.ts
index 9ffad042..f6ec61b0 100644
--- a/tests/features/chat/view-model.test.ts
+++ b/tests/features/chat/view-model.test.ts
@@ -104,6 +104,7 @@ describe("chat view model", () => {
],
percent: "42%",
},
+ statusSummary: "Context 42%, plan on, auto-review on, fast on, model gpt-5.5, reasoning effort high",
model: "gpt-5.5",
effort: "high",
planActive: true,
@@ -139,6 +140,7 @@ describe("chat view model", () => {
],
percent: "--%",
},
+ statusSummary: "Context unavailable, plan off, auto-review off, fast off, model gpt-5.5, reasoning effort default",
model: "gpt-5.5",
effort: null,
});
@@ -163,6 +165,7 @@ describe("chat view model", () => {
],
percent: " 0%",
},
+ statusSummary: "Context 0%, plan off, auto-review off, fast off, model default, reasoning effort default",
});
});
@@ -181,6 +184,7 @@ describe("chat view model", () => {
],
percent: "--%",
},
+ statusSummary: "Codex app-server disconnected",
model: "",
effort: null,
planActive: false,
diff --git a/tests/styles.test.ts b/tests/styles.test.ts
index 116edf76..13e13de8 100644
--- a/tests/styles.test.ts
+++ b/tests/styles.test.ts
@@ -128,15 +128,30 @@ describe("chat toolbar CSS", () => {
const composerMetaStatus = /\.codex-panel__composer-meta-status \{(?[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
const composerMetaFatal = /\.codex-panel__composer-meta-fatal \{(?[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
+ expect(composerMetaStatus).toContain("position: relative");
expect(composerMetaStatus).toContain("padding-inline-start: calc(var(--size-4-1) / 2)");
expect(composerMetaFatal).toContain("padding-inline-start: calc(var(--size-4-1) / 2)");
});
+ it("keeps composer status accessible summary visually hidden", () => {
+ const summary = /\.codex-panel__composer-meta-summary \{(?[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
+ const visual = /\.codex-panel__composer-meta-status-visual \{(?[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
+
+ expect(summary).toContain("position: absolute");
+ expect(summary).toContain("width: var(--codex-panel-rail-width)");
+ expect(summary).toContain("height: var(--codex-panel-rail-width)");
+ expect(summary).toContain("clip-path: inset(50%)");
+ expect(summary).not.toContain("clip:");
+ expect(visual).toContain("display: flex");
+ expect(visual).toContain("flex: 0 0 auto");
+ expect(visual).toContain("gap: var(--size-4-2)");
+ });
+
it("keeps composer runtime mode icons compact and state-colored", () => {
const modes = /\.codex-panel__composer-meta-modes \{(?[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
const field = /\.codex-panel__composer-meta-field \{(?[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
const labels =
- /\.codex-panel__composer-meta-model,\n\.codex-panel__composer-meta-effort \{(?[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
+ /\.codex-panel__composer-meta-value\.codex-panel__composer-meta-value \{(?[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
const icon = /\.codex-panel__composer-meta-icon \{(?[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
const activeIcon = /\.codex-panel__composer-meta-icon\.is-active \{(?[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
const iconSvg = /\.codex-panel__composer-meta-icon svg \{(?[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
@@ -146,6 +161,7 @@ describe("chat toolbar CSS", () => {
expect(field).toContain("flex: 0 0 auto");
expect(field).toContain("gap: var(--size-4-2)");
expect(labels).toContain("white-space: nowrap");
+ expect(labels).toContain("display: inline-flex");
expect(labels).not.toContain("text-overflow");
expect(icon).toContain("width: var(--codex-panel-size-icon-xs)");
expect(icon).toContain("height: var(--codex-panel-size-icon-xs)");
@@ -160,6 +176,22 @@ describe("chat toolbar CSS", () => {
);
});
+ it("keeps composer status triggers visually unstyled", () => {
+ const popover = /\.codex-panel__composer-meta-popover \{(?[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
+ const option = /\.codex-panel__composer-meta-option \{(?[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
+
+ expect(styles).not.toContain(".codex-panel__composer-meta-trigger {");
+ expect(styles).not.toContain(".codex-panel__composer-meta-trigger:hover");
+ expect(styles).not.toContain("button.codex-panel__composer-meta-trigger");
+ expect(popover).toContain("position: absolute");
+ expect(popover).toContain("bottom: calc(100% + var(--codex-panel-panel-gap))");
+ expect(popover).toContain("width: max-content");
+ expect(popover).toContain("max-width: calc(100% - var(--codex-panel-composer-meta-popover-left))");
+ expect(styles).not.toContain(".codex-panel__composer-meta-option.is-selected");
+ expect(option).not.toContain("padding-left");
+ expect(option).not.toContain("grid-template-columns");
+ });
+
it("keeps nav inline input reset in the shared primitive", () => {
const navInlineInput =
/\.codex-panel-ui__nav-inline-input\.codex-panel-ui__nav-inline-input \{(?[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";