Remove toolbar runtime controls

This commit is contained in:
murashit 2026-06-05 12:32:12 +09:00
parent 75ee797ed9
commit fa0f3c30c5
16 changed files with 43 additions and 289 deletions

View file

@ -78,7 +78,6 @@ export interface ChatState {
historyCursor: string | null;
loadingHistory: boolean;
composerDraft: string;
runtimePicker: "model" | "effort" | null;
availableModels: readonly Model[];
availableSkills: readonly SkillMetadata[];
reportedLogs: ReadonlySet<string>;
@ -159,7 +158,7 @@ export type ChatAction =
}
| {
type: "ui/panel-set";
panel: "history" | "status-panel" | "model" | "effort" | null;
panel: "history" | "status-panel" | null;
toggle?: boolean;
}
| { type: "ui/messages-pinned-set"; pinned: boolean }
@ -562,7 +561,6 @@ export function clearConnectionScopedState(state: ChatState): ChatState {
availableModels: [],
availableSkills: [],
appServerDiagnostics: createAppServerDiagnostics(),
runtimePicker: null,
});
}
@ -635,9 +633,8 @@ function initialComposerState(): Pick<
};
}
function initialPanelUiState(): Pick<ChatState, "runtimePicker" | "openDetails"> {
function initialPanelUiState(): Pick<ChatState, "openDetails"> {
return {
runtimePicker: null,
openDetails: new Set(),
};
}
@ -723,19 +720,14 @@ function updatedTurnDiffs(turnDiffs: ReadonlyMap<string, string>, turnId: string
return next;
}
function setPanelState(state: ChatState, panel: "history" | "status-panel" | "model" | "effort" | null, toggle: boolean): ChatState {
const currentPanel = state.openDetails.has("history")
? "history"
: state.openDetails.has("status-panel")
? "status-panel"
: state.runtimePicker;
function setPanelState(state: ChatState, panel: "history" | "status-panel" | null, toggle: boolean): ChatState {
const currentPanel = state.openDetails.has("history") ? "history" : state.openDetails.has("status-panel") ? "status-panel" : null;
const nextPanel = toggle && currentPanel === panel ? null : panel;
return patchChatState(state, {
openDetails:
nextPanel === "history" || nextPanel === "status-panel"
? new Set([nextPanel])
: new Set([...state.openDetails].filter((key) => key !== "history" && key !== "status-panel")),
runtimePicker: nextPanel === "model" || nextPanel === "effort" ? nextPanel : null,
});
}

View file

@ -1,14 +1,6 @@
import type { EffectiveConfigSection, RateLimitSummary } from "../../runtime/view";
export type ToolbarPanelKind = "history" | "status" | "runtime";
export interface ToolbarChoice {
label: string;
selected?: boolean;
disabled?: boolean;
meta?: string;
onClick: () => void;
}
export type ToolbarPanelKind = "history" | "status";
export interface ToolbarThreadRow {
title: string;
@ -38,16 +30,10 @@ export interface ToolbarViewModel {
newChatDisabled: boolean;
historyOpen: boolean;
statusPanelOpen: boolean;
runtimeOpen: boolean;
planActive: boolean;
autoReviewActive: boolean;
fastActive: boolean;
rateLimit: RateLimitSummary | null;
configSections: EffectiveConfigSection[];
openPanel: ToolbarPanelKind | null;
threads: ToolbarThreadRow[];
modelChoices: ToolbarChoice[];
effortChoices: ToolbarChoice[];
connectLabel: string;
diagnostics: ToolbarDiagnosticSection[];
}

View file

@ -44,11 +44,6 @@ export class ToolbarPanelController {
this.host.scheduleRender();
}
toggleRuntime(picker: NonNullable<ChatState["runtimePicker"]>): void {
this.dispatch({ type: "ui/panel-set", panel: picker, toggle: true });
this.host.scheduleRender();
}
closeForThreadSelection(): void {
this.archiveConfirmThreadId = null;
}
@ -90,7 +85,7 @@ export class ToolbarPanelController {
}
private hasOpenPanel(): boolean {
return this.state.openDetails.has("history") || this.state.openDetails.has("status-panel") || this.state.runtimePicker !== null;
return this.state.openDetails.has("history") || this.state.openDetails.has("status-panel");
}
private close(): void {

View file

@ -3,8 +3,7 @@ import type { ButtonHTMLAttributes, ComponentChild as UiNode, Ref } from "preact
import { useLayoutEffect, useRef, useState } from "preact/hooks";
import type { ComposerSuggestion } from "../composer/suggestions";
import type { ComposerMetaViewModel } from "../view-model";
import type { ToolbarChoice } from "../toolbar-model";
import type { ComposerMetaViewModel, RuntimeChoice } from "../view-model";
import { IconButton } from "../../../shared/ui/components";
import { renderUiRoot } from "../../../shared/ui/ui-root";
import { syncTextareaHeight } from "../../../shared/ui/textarea-autogrow";
@ -421,7 +420,7 @@ function ComposerMetaChoicePopover({
onClose,
}: {
kind: ComposerMetaPickerKind;
choices: ToolbarChoice[];
choices: RuntimeChoice[];
left: number;
onClose: () => void;
}): UiNode {
@ -437,7 +436,7 @@ function ComposerMetaChoicePopover({
);
}
function ComposerMetaChoice({ choice, onClose }: { choice: ToolbarChoice; onClose: () => void }): UiNode {
function ComposerMetaChoice({ choice, onClose }: { choice: RuntimeChoice; onClose: () => void }): UiNode {
const onSelect = () => {
if (choice.disabled) return;
choice.onClick();

View file

@ -13,11 +13,7 @@ type ButtonProps = ButtonHTMLAttributes & {
export interface ToolbarActions {
startNewThread: () => void;
toggleHistory: () => void;
toggleAutoReview: () => void;
toggleStatusPanel: () => void;
togglePlan: () => void;
toggleFast: () => void;
toggleRuntime: () => void;
connect: () => void;
refreshStatus: () => void;
resumeThread: (threadId: string) => void;
@ -53,7 +49,6 @@ function Toolbar({ model, actions }: { model: ToolbarViewModel; actions: Toolbar
disabled={model.newChatDisabled}
onClick={actions.startNewThread}
/>
<RuntimeButtons model={model} actions={actions} />
<StatusButton model={model} actions={actions} />
</div>
</div>
@ -82,60 +77,6 @@ function ToolbarIconButton({
);
}
function RuntimeButtons({ model, actions }: { model: ToolbarViewModel; actions: ToolbarActions }): UiNode {
return (
<>
<RuntimeIcon
icon="list-todo"
label="Toggle plan mode"
className="codex-panel__plan-toggle"
active={model.planActive}
onClick={actions.togglePlan}
/>
<RuntimeIcon
icon="shield"
label="Toggle auto-review"
className="codex-panel__auto-review-toggle"
active={model.autoReviewActive}
onClick={actions.toggleAutoReview}
/>
<RuntimeIcon icon="zap" label="Toggle fast mode" active={model.fastActive} onClick={actions.toggleFast} />
<ToolbarIconButton
icon="bot"
label="Change model and reasoning effort"
className={["codex-panel__runtime-model", model.runtimeOpen ? "is-active" : ""].filter(Boolean).join(" ")}
aria-label="Change model and reasoning effort"
aria-expanded={model.runtimeOpen ? "true" : "false"}
onClick={actions.toggleRuntime}
/>
</>
);
}
function RuntimeIcon({
icon,
label,
className,
active,
onClick,
}: {
icon: string;
label: string;
className?: string;
active: boolean;
onClick: () => void;
}): UiNode {
return (
<ToolbarIconButton
icon={icon}
label={label}
className={`codex-panel__runtime-icon ${className ?? ""} ${active ? "is-active" : ""}`}
aria-pressed={active ? "true" : "false"}
onClick={onClick}
/>
);
}
function StatusButton({ model, actions }: { model: ToolbarViewModel; actions: ToolbarActions }): UiNode {
return (
<ToolbarIconButton
@ -153,7 +94,6 @@ function ToolbarPanel({ model, actions }: { model: ToolbarViewModel; actions: To
return (
<div className={`codex-panel__toolbar-panel codex-panel__toolbar-panel--${model.openPanel}`}>
{model.openPanel === "history" ? <ThreadList threads={model.threads} actions={actions} /> : null}
{model.openPanel === "runtime" ? <RuntimePicker model={model} /> : null}
{model.openPanel === "status" ? <StatusPanel model={model} actions={actions} /> : null}
</div>
);
@ -267,39 +207,6 @@ function FragmentedConfigSection({ section }: { section: EffectiveConfigSection
);
}
function RuntimePicker({ model }: { model: ToolbarViewModel }): UiNode {
return (
<div className="codex-panel__runtime-picker" role="listbox">
<div className="codex-panel__runtime-picker-label">Reasoning effort</div>
{model.effortChoices.map((choice) => (
<ToolbarPanelItem
key={`effort:${choice.label}`}
label={choice.label}
selected={choice.selected}
disabled={choice.disabled}
meta={choice.meta}
onClick={choice.onClick}
className="codex-panel__runtime-choice"
role="option"
/>
))}
<div className="codex-panel__runtime-picker-label">Model</div>
{model.modelChoices.map((choice) => (
<ToolbarPanelItem
key={`model:${choice.label}`}
label={choice.label}
selected={choice.selected}
disabled={choice.disabled}
meta={choice.meta}
onClick={choice.onClick}
className="codex-panel__runtime-choice"
role="option"
/>
))}
</div>
);
}
function ThreadList({ threads, actions }: { threads: ToolbarThreadRow[]; actions: ToolbarActions }): UiNode {
if (threads.length === 0) {
return (

View file

@ -18,7 +18,7 @@ import { codexPanelDisplayTitle, explicitThreadName, getThreadTitle } from "../.
import { connectionDiagnosticSections } from "./diagnostics";
import type { ChatState } from "./chat-state";
import { statusValue, usageLimitStatusLines } from "./status-lines";
import type { ToolbarChoice, ToolbarThreadRow, ToolbarViewModel } from "./toolbar-model";
import type { ToolbarThreadRow, ToolbarViewModel } from "./toolbar-model";
export interface RuntimeSnapshotInput {
state: ChatState;
@ -33,8 +33,16 @@ export interface ComposerMetaViewModel {
planActive: boolean;
autoReviewActive: boolean;
fastActive: boolean;
modelChoices?: ToolbarChoice[];
effortChoices?: ToolbarChoice[];
modelChoices?: RuntimeChoice[];
effortChoices?: RuntimeChoice[];
}
export interface RuntimeChoice {
label: string;
selected?: boolean;
disabled?: boolean;
meta?: string;
onClick: () => void;
}
export interface ComposerContextMeterCellViewModel {
@ -56,8 +64,6 @@ export interface ToolbarViewModelInput {
configuredCommand: string;
archiveConfirmThreadId: string | null;
archiveExportEnabled: boolean;
modelChoices: ToolbarChoice[];
effortChoices: ToolbarChoice[];
renameState: (threadId: string) => ToolbarThreadRow["rename"];
}
@ -67,7 +73,7 @@ export interface ConnectionDiagnosticsModelInput {
configuredCommand: string;
}
export interface RuntimeToolbarChoicesInput {
export interface RuntimeComposerChoicesInput {
state: ChatState;
snapshot: RuntimeSnapshot;
setRequestedModel: (model: string | null) => void;
@ -103,11 +109,14 @@ export function runtimeSnapshotForChatState({ state }: RuntimeSnapshotInput): Ru
};
}
export function runtimeToolbarChoices(input: RuntimeToolbarChoicesInput): Pick<ToolbarViewModel, "modelChoices" | "effortChoices"> {
export function runtimeComposerChoices(input: RuntimeComposerChoicesInput): {
modelChoices: RuntimeChoice[];
effortChoices: RuntimeChoice[];
} {
const config = readRuntimeConfig(input.state.effectiveConfig);
const activeModel = currentModel(input.snapshot, config);
const models = sortedAvailableModels(input.state.availableModels);
const modelChoices: ToolbarChoice[] = models.slice(0, 12).map((model) => ({
const modelChoices: RuntimeChoice[] = models.slice(0, 12).map((model) => ({
label: model.model,
selected: activeModel === model.model,
onClick: () => {
@ -123,7 +132,7 @@ export function runtimeToolbarChoices(input: RuntimeToolbarChoicesInput): Pick<T
}
const activeEffort = currentReasoningEffort(input.snapshot, config);
const effortChoices: ToolbarChoice[] = supportedReasoningEfforts(input.snapshot).map((effort) => ({
const effortChoices: RuntimeChoice[] = supportedReasoningEfforts(input.snapshot).map((effort) => ({
label: effort,
selected: activeEffort === effort,
onClick: () => {
@ -225,22 +234,16 @@ function onOffLabel(active: boolean): string {
export function toolbarViewModel(input: ToolbarViewModelInput): ToolbarViewModel {
const { state, snapshot } = input;
const config = readRuntimeConfig(state.effectiveConfig);
const limit = rateLimitSummary(snapshot);
const historyOpen = state.openDetails.has("history");
const statusPanelOpen = state.openDetails.has("status-panel");
const runtimeOpen = state.runtimePicker !== null;
return {
newChatDisabled: input.turnBusy,
historyOpen,
statusPanelOpen,
runtimeOpen,
planActive: state.selectedCollaborationMode === "plan",
autoReviewActive: autoReviewActive(snapshot, config),
fastActive: fastModeActive(snapshot, config),
rateLimit: limit,
configSections: effectiveConfigSections(snapshot, input.vaultPath),
openPanel: historyOpen ? "history" : runtimeOpen ? "runtime" : statusPanelOpen ? "status" : null,
openPanel: historyOpen ? "history" : statusPanelOpen ? "status" : null,
threads: toolbarThreadRows({
threads: state.listedThreads,
activeThreadId: state.activeThreadId,
@ -249,8 +252,6 @@ export function toolbarViewModel(input: ToolbarViewModelInput): ToolbarViewModel
archiveExportEnabled: input.archiveExportEnabled,
renameState: input.renameState,
}),
modelChoices: input.modelChoices,
effortChoices: input.effortChoices,
connectLabel: input.connected ? "Reconnect" : "Connect",
diagnostics: connectionDiagnosticsModel({
state,

View file

@ -38,7 +38,6 @@ export function toolbarSlotSnapshot(state: ChatState, connected: boolean): ChatP
state.requestedApprovalsReviewer,
state.requestedModel,
state.requestedReasoningEffort,
state.runtimePicker,
openDetailsSignature(state.openDetails),
state.threadsLoaded,
threadListSignature(state.listedThreads),

View file

@ -26,7 +26,7 @@ import {
composerPlaceholder as buildComposerPlaceholder,
effortStatusLines as buildEffortStatusLines,
modelStatusLines as buildModelStatusLines,
runtimeToolbarChoices,
runtimeComposerChoices,
runtimeSnapshotForChatState,
statusSummaryLines as buildStatusSummaryLines,
toolbarViewModel as buildToolbarViewModel,
@ -558,7 +558,7 @@ export class CodexChatView extends ItemView {
private composerMetaViewModel() {
return {
...buildComposerMetaViewModel(this.state, this.runtimeSnapshot()),
...runtimeToolbarChoices({
...runtimeComposerChoices({
state: this.state,
snapshot: this.runtimeSnapshot(),
setRequestedModel: (model) => void this.setRequestedModelFromUi(model),
@ -587,15 +587,9 @@ export class CodexChatView extends ItemView {
toggleHistory: () => {
this.toolbarPanels.toggleHistory();
},
toggleAutoReview: () => void this.runtimeSettings.toggleAutoReview(),
toggleStatusPanel: () => {
this.toolbarPanels.toggleStatus();
},
togglePlan: () => void this.runtimeSettings.toggleCollaborationMode(),
toggleFast: () => void this.runtimeSettings.toggleFastMode(),
toggleRuntime: () => {
this.toolbarPanels.toggleRuntime("model");
},
connect: () => void this.reconnectActions.reconnectFromToolbar(),
refreshStatus: () => void this.refreshStatusPanel(),
resumeThread: (threadId) => void this.threadSelection.selectThreadFromToolbar(threadId),
@ -627,12 +621,6 @@ export class CodexChatView extends ItemView {
configuredCommand: this.plugin.settings.codexPath,
archiveConfirmThreadId: this.toolbarPanels.archiveConfirmId(),
archiveExportEnabled: this.plugin.settings.archiveExportEnabled,
...runtimeToolbarChoices({
state: this.state,
snapshot: this.runtimeSnapshot(),
setRequestedModel: (model) => void this.setRequestedModelFromUi(model),
setRequestedReasoningEffort: (effort) => void this.setRequestedReasoningEffortFromUi(effort),
}),
renameState: (threadId) => this.threadRename.editState(threadId),
});
}

View file

@ -16,16 +16,6 @@
white-space: nowrap;
}
.codex-panel__runtime-icon {
flex: 0 0 auto;
}
.codex-panel__runtime-picker {
display: flex;
flex-direction: column;
gap: var(--nav-item-margin-bottom, var(--codex-panel-panel-gap));
}
.codex-panel__toolbar-panel-item {
display: flex;
align-items: center;
@ -76,10 +66,3 @@
box-shadow: none;
outline: none;
}
.codex-panel__runtime-picker-label {
padding: var(--size-4-1) var(--size-4-2) var(--size-2-1);
color: var(--text-faint);
font-size: var(--codex-panel-toolbar-meta-size);
font-weight: var(--font-medium);
}

View file

@ -290,13 +290,11 @@ describe("chatReducer", () => {
let state = createChatState();
state = chatReducer(state, { type: "ui/panel-set", panel: "history" });
state = chatReducer(state, { type: "ui/panel-set", panel: "model" });
expect(state.openDetails.size).toBe(0);
expect(state.runtimePicker).toBe("model");
expect(state.openDetails.has("history")).toBe(true);
state = chatReducer(state, { type: "ui/panel-set", panel: "status-panel" });
expect(state.openDetails.has("history")).toBe(false);
expect(state.openDetails.has("status-panel")).toBe(true);
expect(state.runtimePicker).toBeNull();
});
it("updates remembered details and user input drafts through typed UI actions", () => {

View file

@ -116,7 +116,6 @@ describe("ChatConnectionController", () => {
threadsLoaded: false,
availableModels: [],
availableSkills: [],
runtimePicker: null,
});
});

View file

@ -70,13 +70,13 @@ describe("PlanImplementationController", () => {
const { controller, ensureConnected, sendTurnText, stateStore } = createController();
const plan = planItem("plan");
resumeThread(stateStore, [plan]);
stateStore.dispatch({ type: "ui/panel-set", panel: "model" });
stateStore.dispatch({ type: "ui/panel-set", panel: "status-panel" });
await controller.implement(plan);
expect(ensureConnected).toHaveBeenCalledOnce();
expect(stateStore.getState().selectedCollaborationMode).toBe("default");
expect(stateStore.getState().runtimePicker).toBeNull();
expect(stateStore.getState().openDetails.has("status-panel")).toBe(false);
expect(sendTurnText).toHaveBeenCalledWith("Please implement this plan.");
});

View file

@ -18,10 +18,9 @@ describe("toolbar renderer decisions", () => {
const parent = document.createElement("div");
const startNewThread = vi.fn();
const toggleHistory = vi.fn();
const toggleAutoReview = vi.fn();
const baseModel = toolbarModel();
renderToolbar(parent, baseModel, toolbarActions({ startNewThread, toggleHistory, toggleAutoReview }));
renderToolbar(parent, baseModel, toolbarActions({ startNewThread, toggleHistory }));
const navHeader = parent.querySelector(".codex-panel__toolbar-primary");
expect(navHeader?.classList.contains("nav-header")).toBe(true);
@ -32,12 +31,11 @@ describe("toolbar renderer decisions", () => {
expect([...expectPresent(navButtons).children].map((button) => button.getAttribute("aria-label"))).toEqual([
"Show thread list",
"Start new chat",
"Toggle plan mode",
"Toggle auto-review",
"Toggle fast mode",
"Change model and reasoning effort",
"Show panel menu",
]);
expect(parent.querySelector(".codex-panel__plan-toggle")).toBeNull();
expect(parent.querySelector(".codex-panel__auto-review-toggle")).toBeNull();
expect(parent.querySelector(".codex-panel__runtime-model")).toBeNull();
const newChatButton = parent.querySelector<HTMLButtonElement>(".codex-panel__new-chat");
expect(newChatButton?.getAttribute("aria-label")).toBe("Start new chat");
expect(newChatButton?.dataset["icon"]).toBe("messages-square");
@ -59,27 +57,6 @@ describe("toolbar renderer decisions", () => {
expect(historyButton?.classList.contains("clickable-icon")).toBe(true);
historyButton?.click();
expect(toggleHistory).toHaveBeenCalled();
const planButton = parent.querySelector<HTMLButtonElement>(".codex-panel__plan-toggle");
expect(planButton?.getAttribute("aria-label")).toBe("Toggle plan mode");
expect(planButton?.dataset["icon"]).toBe("list-todo");
expect(planButton?.getAttribute("aria-pressed")).toBe("false");
expect(planButton?.classList.contains("codex-panel__runtime-icon")).toBe(true);
const autoReviewButton = parent.querySelector<HTMLButtonElement>(".codex-panel__auto-review-toggle");
expect(autoReviewButton?.getAttribute("aria-label")).toBe("Toggle auto-review");
expect(autoReviewButton?.getAttribute("aria-pressed")).toBe("false");
expect(autoReviewButton?.classList.contains("codex-panel__runtime-icon")).toBe(true);
expect(autoReviewButton?.classList.contains("nav-action-button")).toBe(true);
expect(autoReviewButton?.classList.contains("codex-panel-ui__toolbar-action")).toBe(true);
expect(autoReviewButton?.classList.contains("codex-panel-ui__toolbar-control")).toBe(false);
expect(autoReviewButton?.classList.contains("clickable-icon")).toBe(true);
autoReviewButton?.click();
expect(toggleAutoReview).toHaveBeenCalled();
parent.empty();
renderToolbar(parent, toolbarModel({ autoReviewActive: true }), toolbarActions());
expect(parent.querySelector(".codex-panel__status-menu-toggle")?.getAttribute("aria-label")).toBe("Show panel menu");
expect(parent.querySelector(".codex-panel__auto-review-toggle")?.getAttribute("aria-pressed")).toBe("true");
parent.empty();
renderToolbar(parent, toolbarModel({ newChatDisabled: true }), toolbarActions());
expect(parent.querySelector<HTMLButtonElement>(".codex-panel__new-chat")?.disabled).toBe(true);
@ -90,36 +67,6 @@ describe("toolbar renderer decisions", () => {
expect(parent.querySelector(".codex-panel__history-toggle")?.classList.contains("is-active")).toBe(true);
expect(parent.querySelector(".codex-panel__status-menu-toggle")?.getAttribute("aria-label")).toBe("Hide panel menu");
expect(parent.querySelector(".codex-panel__status-menu-toggle")?.classList.contains("is-active")).toBe(true);
expect(parent.querySelector(".codex-panel__runtime-model")?.getAttribute("aria-label")).toBe("Change model and reasoning effort");
expect(parent.querySelector<HTMLElement>(".codex-panel__runtime-model")?.dataset["icon"]).toBe("bot");
expect(parent.querySelector(".codex-panel__runtime-model")?.classList.contains("is-active")).toBe(true);
expect(parent.querySelector(".codex-panel__runtime-model")?.classList.contains("nav-action-button")).toBe(true);
});
it("keeps frequently changed effort choices first inside the runtime menu", () => {
const parent = document.createElement("div");
renderToolbar(
parent,
toolbarModel({
modelChoices: [{ label: "gpt-5.5", selected: true, onClick: vi.fn() }],
effortChoices: [{ label: "high", selected: true, onClick: vi.fn() }],
}),
toolbarActions(),
);
expect([...parent.querySelectorAll(".codex-panel__runtime-picker-label")].map((label) => label.textContent)).toEqual([
"Reasoning effort",
"Model",
]);
expect([...parent.querySelectorAll(".codex-panel__runtime-choice")].map((choice) => choice.textContent)).toEqual(["high", "gpt-5.5"]);
for (const choice of parent.querySelectorAll(".codex-panel__runtime-choice")) {
expect(choice.getAttribute("role")).toBe("option");
expect(choice.getAttribute("aria-selected")).toBe("true");
expect(choice.querySelector<HTMLElement>(".codex-panel__toolbar-panel-check")).toBeNull();
expect(choice.classList.contains("selected")).toBe(false);
expect(choice.classList.contains("is-selected")).toBe(true);
}
});
it("keeps context out of the toolbar and Codex limits inside the status menu", () => {
@ -381,16 +328,10 @@ function toolbarModel(overrides: Partial<ToolbarViewModel> = {}): ToolbarViewMod
newChatDisabled: false,
historyOpen: false,
statusPanelOpen: false,
runtimeOpen: true,
planActive: false,
autoReviewActive: false,
fastActive: false,
rateLimit: null,
configSections: [],
openPanel: "runtime",
openPanel: null,
threads: [{ title: "Thread", threadId: "thread", selected: true, disabled: false, canArchive: true, rename: null }],
modelChoices: [{ label: "Default", selected: true, onClick: vi.fn() }],
effortChoices: [{ label: "Default", selected: true, onClick: vi.fn() }],
connectLabel: "Reconnect",
diagnostics: [{ title: "Process", rows: [{ label: "Codex App Server", value: "codex-cli/test" }] }],
...overrides,
@ -401,11 +342,7 @@ function toolbarActions(overrides: Partial<Parameters<typeof renderToolbar>[2]>
return {
toggleHistory: vi.fn(),
startNewThread: vi.fn(),
toggleAutoReview: vi.fn(),
toggleStatusPanel: vi.fn(),
togglePlan: vi.fn(),
toggleFast: vi.fn(),
toggleRuntime: vi.fn(),
connect: vi.fn(),
refreshStatus: vi.fn(),
resumeThread: vi.fn(),

View file

@ -70,7 +70,7 @@ describe("ChatPanelShell", () => {
await act(async () => {
store.dispatch({ type: "status/set", status: "Working" });
store.dispatch({ type: "ui/panel-set", panel: "model" });
store.dispatch({ type: "ui/panel-set", panel: "status-panel" });
store.dispatch({ type: "system/message-added", item: { id: "system-1", kind: "system", role: "system", text: "Model set." } });
await settleShellEffects();
});

View file

@ -9,7 +9,7 @@ import {
composerMetaViewModel,
composerPlaceholder,
effortStatusLines,
runtimeToolbarChoices,
runtimeComposerChoices,
modelStatusLines,
runtimeSnapshotForChatState,
statusSummaryLines,
@ -38,8 +38,6 @@ describe("chat view model", () => {
configuredCommand: "codex",
archiveConfirmThreadId: "thread-2",
archiveExportEnabled: true,
modelChoices: [{ label: "gpt-5.5", selected: true, onClick: () => undefined }],
effortChoices: [{ label: "high", selected: true, onClick: () => undefined }],
renameState: (threadId) => (threadId === "thread-1" ? { draft: "Active", generating: false } : null),
});
@ -49,32 +47,6 @@ describe("chat view model", () => {
{ threadId: "thread-1", title: "Active", selected: true, disabled: false, rename: { draft: "Active" } },
{ threadId: "thread-2", title: "Other", selected: false, disabled: true, archiveConfirm: { active: true } },
]);
expect(model.modelChoices).toHaveLength(1);
});
it("marks fast active for the catalog Fast service tier id", () => {
const state = createChatState();
state.activeModel = "gpt-5.5";
state.activeServiceTier = "priority";
state.effectiveConfig = effectiveConfigFixture({ model: "gpt-5.5", model_reasoning_effort: "high" });
state.availableModels = [modelFixture("gpt-5.5", "priority")];
const model = toolbarViewModel({
state,
snapshot: runtimeSnapshotForChatState({ state }),
connected: true,
turnBusy: false,
vaultPath: "/vault",
configuredCommand: "codex",
archiveConfirmThreadId: null,
archiveExportEnabled: true,
modelChoices: [],
effortChoices: [],
renameState: () => null,
});
expect(model.fastActive).toBe(true);
expect(model.newChatDisabled).toBe(false);
});
it("builds composer meta from context and runtime state", () => {
@ -211,14 +183,14 @@ describe("chat view model", () => {
expect(effortStatusLines(state, snapshot)).toContain("Supported: high");
});
it("builds runtime toolbar choices from immutable chat state snapshots", () => {
it("builds runtime composer choices from immutable chat state snapshots", () => {
const state = createChatState();
state.effectiveConfig = effectiveConfigFixture({ model: "gpt-5.5", model_reasoning_effort: "high" });
state.availableModels = [modelFixture("gpt-5.5"), modelFixture("gpt-5-mini")];
const selectedModels: (string | null)[] = [];
const selectedEfforts: string[] = [];
const choices = runtimeToolbarChoices({
const choices = runtimeComposerChoices({
state,
snapshot: runtimeSnapshotForChatState({ state }),
setRequestedModel: (model) => {

View file

@ -98,11 +98,9 @@ describe("chat toolbar CSS", () => {
it("keeps chat thread row actions inset from the row edge", () => {
const threadList = /\.codex-panel__threads \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
const navRow = /\.codex-panel-ui__nav-row \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
const runtimePicker = /\.codex-panel__runtime-picker \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
const statusPanelItems = /\.codex-panel__status-panel-items \{(?<body>[^}]+)\}/.exec(styles)?.groups?.["body"] ?? "";
expect(threadList).toContain("gap: var(--nav-item-margin-bottom, var(--codex-panel-panel-gap))");
expect(runtimePicker).toContain("gap: var(--nav-item-margin-bottom, var(--codex-panel-panel-gap))");
expect(statusPanelItems).toContain("gap: var(--nav-item-margin-bottom, var(--codex-panel-panel-gap))");
expect(navRow).toContain("--codex-panel-nav-item-background-hover: transparent");
expect(navRow).toContain("padding-inline-end: var(--size-4-2)");