Simplify toolbar runtime debug details

This commit is contained in:
murashit 2026-06-15 15:29:09 +09:00
parent 87cac9ee85
commit bf4d3cc9e1
16 changed files with 92 additions and 517 deletions

View file

@ -83,7 +83,7 @@ By default, `Enter` sends and `Shift+Enter` inserts a newline. You can switch se
- Inspect context usage from the composer status row.
- Toggle Plan mode, fast mode, and approval auto-review for subsequent turns.
- Set the model and reasoning effort for subsequent turns when supported by Codex.
- Inspect usage limits, connection diagnostics, MCP servers, enabled skills, effective Codex config, and discovered hooks from the toolbar, settings, or slash commands.
- Inspect usage limits, connection diagnostics, MCP servers, enabled skills, runtime debug details, and discovered hooks from the toolbar, settings, or slash commands.
### Obsidian-native workflows

View file

@ -58,10 +58,6 @@ export function supportedEffortsForModelMetadata(model: ModelMetadata | null): R
);
}
export function defaultEffortForModelMetadata(model: ModelMetadata | null): ReasoningEffort | null {
return normalizeReasoningEffort(model?.defaultReasoningEffort);
}
export function sortedModelMetadata(models: readonly ModelMetadata[]): ModelMetadata[] {
return [...models]
.filter((model) => !model.hidden)

View file

@ -1,6 +1,6 @@
import { cloneRuntimeConfigSnapshot, emptyRuntimeConfigSnapshot, type RuntimeConfigSnapshot } from "../../../../domain/runtime/config";
import { findModelMetadataByIdOrName, type ModelMetadata } from "../../../../domain/catalog/metadata";
import type { ApprovalPolicy, ApprovalsReviewer } from "../../../../domain/runtime/policy";
import type { ApprovalsReviewer } from "../../../../domain/runtime/policy";
import { supportedEffortsForModelMetadata, type ReasoningEffort } from "../../../../domain/catalog/metadata";
import type { RuntimeSnapshot } from "./snapshot";
@ -34,10 +34,6 @@ export function currentApprovalsReviewer(snapshot: RuntimeSnapshot, config: Runt
return snapshot.activeApprovalsReviewer ?? config.approvalsReviewer;
}
export function currentApprovalPolicy(snapshot: RuntimeSnapshot, config: RuntimeConfigSnapshot): ApprovalPolicy | null {
return snapshot.activeApprovalPolicy ?? config.approvalPolicy;
}
export function autoReviewActive(snapshot: RuntimeSnapshot, config: RuntimeConfigSnapshot): boolean {
return isAutoReviewReviewer(currentApprovalsReviewer(snapshot, config));
}

View file

@ -3,7 +3,7 @@ import { h } from "preact";
import type { Thread } from "../../../../domain/threads/model";
import { getThreadTitle } from "../../../../domain/threads/model";
import { runtimeConfigSections, rateLimitSummary } from "../../presentation/runtime/status";
import { rateLimitSummary } from "../../presentation/runtime/status";
import { connectionDiagnosticSections } from "../../application/connection/diagnostics-display";
import type { RuntimeSnapshot } from "../../application/runtime/snapshot";
import { chatTurnBusy, type ChatState } from "../../application/state/root-reducer";
@ -12,7 +12,7 @@ import { Toolbar, type ToolbarActions, type ToolbarThreadRow, type ToolbarViewMo
import type { ChatPanelToolbarSurface } from "./model";
import { runtimeSnapshotForToolbarShellState } from "./runtime-snapshot";
type ToolbarState = Pick<ChatState, "connection" | "threadList" | "activeThread" | "ui">;
type ToolbarState = Pick<ChatState, "connection" | "threadList" | "activeThread" | "runtime" | "ui">;
interface ToolbarViewModelInput {
state: ToolbarState;
@ -68,7 +68,7 @@ function chatPanelToolbarProjection(input: ToolbarViewModelInput): ToolbarViewMo
historyOpen: projection.historyOpen,
statusPanelOpen: projection.statusPanelOpen,
rateLimit: limit,
configSections: runtimeConfigSections(snapshot, input.vaultPath),
debugDetails: runtimeDebugDetails(input),
openPanel: projection.openPanel,
threads: projection.threads,
connectLabel: input.connected ? "Reconnect" : "Connect",
@ -80,6 +80,20 @@ function chatPanelToolbarProjection(input: ToolbarViewModelInput): ToolbarViewMo
};
}
function runtimeDebugDetails(input: ToolbarViewModelInput): string {
return JSON.stringify(
{
vaultPath: input.vaultPath,
configuredCommand: input.configuredCommand,
runtimeConfig: input.state.connection.runtimeConfig,
runtime: input.state.runtime,
availableModels: input.state.connection.availableModels,
},
null,
2,
);
}
function toolbarStateProjection(input: Pick<ToolbarViewModelInput, "state" | "turnBusy" | "archiveExportEnabled">): ToolbarStateProjection {
const historyOpen = input.state.ui.toolbarPanel === "history";
const chatActionsOpen = input.state.ui.toolbarPanel === "chat-actions";

View file

@ -22,9 +22,3 @@ export function pendingRuntimeSettingLabel(
export function serviceTierLabel(value: string | null): string {
return value ?? "(Codex default)";
}
export function fastModeLabel(input: { requestedOff: boolean; active: boolean; serviceTier: string | null }): string {
if (input.requestedOff) return "off";
if (input.active) return "on";
return input.serviceTier ? "off" : "Codex default";
}

View file

@ -1,27 +1,15 @@
import type { RuntimeConfigSnapshot } from "../../../../domain/runtime/config";
import type { RateLimitWindow, SpendControlLimitSnapshot, ThreadTokenUsage } from "../../../../domain/runtime/metrics";
import { jsonPreview } from "../../../../utils";
import { sortedModelMetadata } from "../../../../domain/catalog/metadata";
import { defaultEffortForModelMetadata } from "../../../../domain/catalog/metadata";
import {
currentApprovalsReviewer,
currentApprovalPolicy,
currentServiceTier,
currentModel,
currentReasoningEffort,
autoReviewActive,
fastModeActive,
runtimeConfigOrDefault,
supportedReasoningEfforts,
} from "../../domain/runtime/effective";
import type { RuntimeSnapshot } from "../../domain/runtime/snapshot";
import { effectiveCollaborationMode } from "../../domain/runtime/pending-settings";
import {
collaborationModeLabel,
fastModeLabel as formatFastModeLabel,
pendingRuntimeSettingLabel,
serviceTierLabel as formatServiceTierLabel,
} from "./messages";
import { pendingRuntimeSettingLabel, serviceTierLabel as formatServiceTierLabel } from "./messages";
export interface ContextSummary {
label: string;
@ -46,11 +34,6 @@ interface RateLimitSummaryRow {
level: "ok" | "warn" | "danger";
}
export interface RuntimeConfigSection {
title: string;
rows: { key: string; value: string }[];
}
export interface StatusSummaryLinesInput {
activeThreadId: RuntimeSnapshot["activeThreadId"];
snapshot: RuntimeSnapshot;
@ -71,20 +54,11 @@ export interface EffortStatusLinesInput {
}
const CODEX_DEFAULT_LABEL = "(Codex default)";
const NOT_REPORTED_LABEL = "(not reported)";
function serviceTierLabel(snapshot: RuntimeSnapshot, config: RuntimeConfigSnapshot): string {
return formatServiceTierLabel(currentServiceTier(snapshot, config));
}
function fastModeLabel(snapshot: RuntimeSnapshot, config: RuntimeConfigSnapshot): string {
return formatFastModeLabel({
requestedOff: snapshot.requestedServiceTier.kind === "set" && snapshot.requestedServiceTier.value === "off",
active: fastModeActive(snapshot, config),
serviceTier: currentServiceTier(snapshot, config),
});
}
export function contextSummary(snapshot: RuntimeSnapshot): ContextSummary | null {
const usage = snapshot.tokenUsage;
const config = runtimeConfigOrDefault(snapshot.runtimeConfig);
@ -146,75 +120,6 @@ export function rateLimitSummary(snapshot: RuntimeSnapshot, nowMs: number): Rate
};
}
export function runtimeConfigSections(snapshot: RuntimeSnapshot, vaultPath: string): RuntimeConfigSection[] {
const config = runtimeConfigOrDefault(snapshot.runtimeConfig);
return [
{
title: "Scope",
rows: [
{ key: "cwd", value: vaultPath },
{ key: "profile", value: config.profile ?? "(default)" },
{ key: "model provider", value: stringValue(config.modelProvider, CODEX_DEFAULT_LABEL) },
],
},
{
title: "Runtime",
rows: [
{ key: "effective model", value: currentModel(snapshot, config) ?? CODEX_DEFAULT_LABEL },
{ key: "configured model", value: configuredModel(snapshot, config) ?? NOT_REPORTED_LABEL },
{ key: "thread model", value: activeRuntimeValueLabel(snapshot.activeModel) },
{ key: "requested model", value: pendingRuntimeSettingLabel(snapshot.requestedModel) },
{ key: "effective effort", value: currentReasoningEffort(snapshot, config) ?? CODEX_DEFAULT_LABEL },
{ key: "configured effort", value: configuredReasoningEffort(snapshot, config) ?? NOT_REPORTED_LABEL },
{ key: "thread effort", value: activeRuntimeValueLabel(snapshot.activeReasoningEffort) },
{ key: "requested effort", value: pendingRuntimeSettingLabel(snapshot.requestedReasoningEffort) },
{ key: "reasoning summary", value: stringValue(config.reasoningSummary, CODEX_DEFAULT_LABEL) },
{ key: "verbosity", value: stringValue(config.verbosity, CODEX_DEFAULT_LABEL) },
{ key: "effective mode", value: activeCollaborationModeLabel(snapshot.activeCollaborationMode) },
{
key: "requested mode",
value:
snapshot.selectedCollaborationMode === effectiveCollaborationMode(snapshot.activeCollaborationMode)
? "(none)"
: collaborationModeLabel(snapshot.selectedCollaborationMode),
},
{ key: "effective service tier", value: serviceTierLabel(snapshot, config) },
{ key: "configured service tier", value: config.serviceTier ?? CODEX_DEFAULT_LABEL },
{ key: "thread service tier", value: activeRuntimeValueLabel(snapshot.activeServiceTier) },
{ key: "requested service tier", value: pendingRuntimeSettingLabel(snapshot.requestedServiceTier) },
{ key: "fast mode", value: fastModeLabel(snapshot, config) },
{ key: "context window", value: tokenLimitLabel(config.modelContextWindow) },
{ key: "auto compact limit", value: tokenLimitLabel(config.autoCompactTokenLimit) },
],
},
{
title: "Policy",
rows: [
{ key: "effective approval", value: stringValue(currentApprovalPolicy(snapshot, config), CODEX_DEFAULT_LABEL) },
{ key: "configured approval", value: stringValue(config.approvalPolicy, CODEX_DEFAULT_LABEL) },
{ key: "thread approval", value: stringValue(snapshot.activeApprovalPolicy, NOT_REPORTED_LABEL) },
{ key: "active permissions", value: activePermissionProfileLabel(snapshot.activePermissionProfile) },
{ key: "effective reviewer", value: currentApprovalsReviewer(snapshot, config) ?? NOT_REPORTED_LABEL },
{ key: "auto-review", value: autoReviewActive(snapshot, config) ? "on" : "off" },
{ key: "configured reviewer", value: config.approvalsReviewer ?? CODEX_DEFAULT_LABEL },
{ key: "thread reviewer", value: activeRuntimeValueLabel(snapshot.activeApprovalsReviewer) },
{ key: "requested reviewer", value: pendingRuntimeSettingLabel(snapshot.requestedApprovalsReviewer) },
{ key: "sandbox", value: stringValue(config.sandboxMode, CODEX_DEFAULT_LABEL) },
{ key: "workspace network", value: stringValue(config.workspaceNetworkAccess, CODEX_DEFAULT_LABEL) },
{ key: "writable roots", value: writableRootsLabel(config.writableRoots) },
{ key: "web search", value: stringValue(config.webSearch, CODEX_DEFAULT_LABEL) },
],
},
{
title: "Features",
rows: [
{ key: "tool web search", value: stringValue(config.rawToolWebSearch, CODEX_DEFAULT_LABEL) },
{ key: "apps", value: enabledAppsLabel(config.rawApps) },
],
},
];
}
export function statusSummaryLines(input: StatusSummaryLinesInput): string[] {
const context = contextSummary(input.snapshot);
const limit = rateLimitSummary(input.snapshot, input.nowMs);
@ -251,32 +156,6 @@ function contextUsageTokens(usage: ThreadTokenUsage): number {
return usage.last.inputTokens > 0 ? usage.last.inputTokens : usage.last.totalTokens;
}
function configuredModel(snapshot: RuntimeSnapshot, config: RuntimeConfigSnapshot): string | null {
if (config.model) return config.model;
return sortedModelMetadata(snapshot.availableModels).find((model) => model.isDefault)?.model ?? null;
}
function configuredReasoningEffort(snapshot: RuntimeSnapshot, config: RuntimeConfigSnapshot): string | null {
if (config.rawReasoningEffort) return config.rawReasoningEffort;
const model = configuredModel(snapshot, config);
return defaultEffortForModelMetadata(
sortedModelMetadata(snapshot.availableModels).find((availableModel) => availableModel.model === model) ?? null,
);
}
function activeRuntimeValueLabel(value: string | null): string {
return value ?? NOT_REPORTED_LABEL;
}
function activeCollaborationModeLabel(value: RuntimeSnapshot["activeCollaborationMode"]): string {
return value ? collaborationModeLabel(value) : NOT_REPORTED_LABEL;
}
function activePermissionProfileLabel(profile: RuntimeSnapshot["activePermissionProfile"]): string {
if (!profile) return NOT_REPORTED_LABEL;
return profile.extends ? `${profile.id} extends ${profile.extends}` : profile.id;
}
function usageLimitStatusLines(limit: RateLimitSummary): string[] {
return ["Usage limits", ...limit.rows.map((row) => `${row.label}: ${row.value}${row.resetLabel ? ` (${row.resetLabel})` : ""}`)];
}
@ -365,33 +244,6 @@ function capitalize(value: string): string {
return value.length === 0 ? value : value.charAt(0).toUpperCase() + value.slice(1);
}
function tokenLimitLabel(value: unknown): string {
const tokens = toNumber(value);
return tokens === null ? CODEX_DEFAULT_LABEL : formatTokenCount(tokens);
}
function writableRootsLabel(value: unknown): string {
if (!Array.isArray(value)) return CODEX_DEFAULT_LABEL;
if (value.length === 0) return "none";
if (value.length === 1) return String(value[0]);
return `${String(value.length)} roots`;
}
function enabledAppsLabel(apps: unknown): string {
const appConfig = asRecord(apps);
const enabled = Object.entries(appConfig)
.filter(([key, app]) => key !== "_default" && asRecord(app)["enabled"] === true)
.map(([key]) => key)
.sort();
if (enabled.length > 0) return enabled.join(", ");
const defaultConfig = asRecord(appConfig["_default"]);
return stringValue(defaultConfig["enabled"], CODEX_DEFAULT_LABEL);
}
function asRecord(value: unknown): Record<string, unknown> {
return value && typeof value === "object" ? (value as Record<string, unknown>) : {};
}
function stringValue(value: unknown, fallback = ""): string {
if (typeof value === "string") return value;
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return String(value);
@ -399,12 +251,6 @@ function stringValue(value: unknown, fallback = ""): string {
return jsonPreview(value);
}
function toNumber(value: unknown): number | null {
if (typeof value === "number") return Number.isFinite(value) ? value : null;
if (typeof value === "bigint") return Number(value);
return null;
}
function formatTokenCount(value: number): string {
return new Intl.NumberFormat("en-US").format(Math.round(value));
}

View file

@ -1,7 +1,7 @@
import type { ButtonHTMLAttributes, ComponentChild as UiNode, TargetedKeyboardEvent } from "preact";
import { useLayoutEffect, useRef } from "preact/hooks";
import type { RuntimeConfigSection, RateLimitSummary } from "../presentation/runtime/status";
import type { RateLimitSummary } from "../presentation/runtime/status";
import { IconButton } from "../../../shared/ui/components";
type ButtonProps = ButtonHTMLAttributes & {
@ -38,7 +38,7 @@ export interface ToolbarViewModel {
historyOpen: boolean;
statusPanelOpen: boolean;
rateLimit: RateLimitSummary | null;
configSections: RuntimeConfigSection[];
debugDetails: string;
openPanel: "history" | "chat-actions" | "status" | null;
threads: ToolbarThreadRow[];
connectLabel: string;
@ -116,7 +116,7 @@ function StatusButton({ model, actions }: { model: ToolbarViewModel; actions: To
return (
<ToolbarIconButton
icon="waypoints"
label={model.statusPanelOpen ? "Hide Codex status and settings" : "Show Codex status and settings"}
label={model.statusPanelOpen ? "Hide status" : "Show status"}
className={["codex-panel__status-menu-toggle", model.statusPanelOpen ? "is-active" : ""].filter(Boolean).join(" ")}
aria-expanded={model.statusPanelOpen ? "true" : "false"}
onClick={actions.toggleStatusPanel}
@ -170,7 +170,7 @@ function StatusPanel({ model, actions }: { model: ToolbarViewModel; actions: Too
</div>
<RateLimitPanel rateLimit={model.rateLimit} />
<ConnectionDiagnostics sections={model.diagnostics} />
<RuntimeConfigPanel sections={model.configSections} />
<DebugDetails details={model.debugDetails} />
</>
);
}
@ -243,30 +243,12 @@ function DiagnosticSection({ section }: { section: ToolbarDiagnosticSection }):
);
}
function RuntimeConfigPanel({ sections }: { sections: RuntimeConfigSection[] }): UiNode {
function DebugDetails({ details }: { details: string }): UiNode {
return (
<div className="codex-panel__config">
<div className="codex-panel__config-title">Effective Codex config</div>
<dl className="codex-panel__config-list">
{sections.map((section) => (
<FragmentedConfigSection key={section.title} section={section} />
))}
</dl>
</div>
);
}
function FragmentedConfigSection({ section }: { section: RuntimeConfigSection }): UiNode {
return (
<>
<div className="codex-panel__config-section">{section.title}</div>
{section.rows.map((row) => (
<div key={`${row.key}\n${row.value}`} className="codex-panel__config-row">
<dt>{row.key}</dt>
<dd>{row.value}</dd>
</div>
))}
</>
<details className="codex-panel__debug-details">
<summary className="codex-panel__debug-details-summary">Debug details</summary>
<pre className="codex-panel__debug-details-content">{details}</pre>
</details>
);
}

View file

@ -17,7 +17,7 @@ export interface ThreadPickerHost {
openThreadInAvailableView(threadId: string): Promise<void>;
}
export type ThreadPickerCatalog = Pick<SharedThreadCatalog, "cachedThreads" | "refreshThreads">;
type ThreadPickerCatalog = Pick<SharedThreadCatalog, "cachedThreads" | "refreshThreads">;
interface ThreadSuggestion {
thread: Thread;

View file

@ -39,7 +39,7 @@ export interface CodexThreadsHost {
getOpenPanelSnapshots(): OpenCodexPanelSnapshot[];
}
export type ThreadsThreadCatalog = Pick<
type ThreadsThreadCatalog = Pick<
SharedThreadCatalog,
"archiveThreadInCatalog" | "renameThreadInCatalog" | "refreshFromOpenSurface" | "refreshThreads" | "cachedThreads"
>;

View file

@ -24,7 +24,7 @@ export interface SettingsDynamicDataHost {
threadCatalog: SettingsThreadCatalog;
}
export type SettingsThreadCatalog = Pick<SharedThreadCatalog, "refreshFromOpenSurface" | "cachedModels" | "publishModels">;
type SettingsThreadCatalog = Pick<SharedThreadCatalog, "refreshFromOpenSurface" | "cachedModels" | "publishModels">;
interface SettingsDynamicDataControllerCallbacks {
display(): void;

View file

@ -29,7 +29,6 @@
.codex-panel__limit-panel {
margin-top: var(--codex-panel-panel-gap);
padding-top: var(--codex-panel-item-gap);
border-top: var(--codex-panel-divider-muted);
}
.codex-panel__limit-panel-title {
@ -148,7 +147,6 @@
.codex-panel__connection-diagnostics {
margin-top: var(--codex-panel-panel-gap);
padding-top: var(--codex-panel-item-gap);
border-top: var(--codex-panel-divider-muted);
}
.codex-panel__connection-diagnostics-title {

View file

@ -28,60 +28,36 @@
display: none;
}
.codex-panel__toolbar-panel .codex-panel__config {
.codex-panel__debug-details {
margin-top: var(--codex-panel-panel-gap);
padding: var(--codex-panel-item-gap) 0 0;
border-top: var(--codex-panel-divider-muted);
color: var(--codex-panel-text-muted);
font-size: var(--font-ui-small);
}
.codex-panel__toolbar-panel .codex-panel__config-title {
.codex-panel__debug-details-summary {
padding: var(--codex-panel-section-label-padding);
color: var(--codex-panel-section-label-color);
font-size: var(--codex-panel-section-label-size);
font-weight: var(--codex-panel-section-label-weight);
line-height: var(--codex-panel-section-label-line-height);
cursor: default;
}
.codex-panel__toolbar-panel .codex-panel__config-list {
margin: 0;
overflow: visible;
border: 0;
border-radius: 0;
background: transparent;
.codex-panel__debug-details-summary:hover {
color: var(--nav-item-color-hover, var(--text-normal));
}
.codex-panel__toolbar-panel .codex-panel__config-section {
padding: var(--codex-panel-section-label-block-padding);
border-top: 0;
color: var(--codex-panel-section-label-color);
font-size: var(--codex-panel-section-label-size);
font-weight: var(--codex-panel-section-label-weight);
line-height: var(--codex-panel-section-label-line-height);
}
.codex-panel__toolbar-panel .codex-panel__config-section:first-child {
padding-top: 0;
}
.codex-panel__toolbar-panel .codex-panel__config-row {
display: grid;
grid-template-columns: minmax(96px, 36%) minmax(0, 1fr);
gap: var(--codex-panel-section-gap);
padding: var(--codex-panel-control-gap) var(--codex-panel-section-gap);
border-top: 0;
}
.codex-panel__toolbar-panel .codex-panel__config-row dt,
.codex-panel__toolbar-panel .codex-panel__config-row dd {
.codex-panel__debug-details-content {
margin: 0 var(--codex-panel-section-gap) var(--codex-panel-control-gap);
overflow: auto;
color: var(--codex-panel-text-muted);
font-size: var(--font-ui-smaller);
line-height: var(--codex-panel-compact-line-height);
line-height: var(--line-height-tight);
white-space: pre;
cursor: text;
user-select: text;
}
.codex-panel__config-title,
.codex-panel__approval-details summary,
.codex-panel__output summary,
.codex-panel-diff-file summary,
@ -102,56 +78,6 @@
color: var(--nav-item-color-hover, var(--text-normal));
}
.codex-panel__config-list {
overflow: hidden;
}
.codex-panel__config-row {
display: grid;
grid-template-columns: minmax(82px, 34%) minmax(0, 1fr);
gap: var(--codex-panel-section-gap);
align-items: start;
padding: var(--codex-panel-control-gap) var(--codex-panel-section-gap);
border-top: var(--codex-panel-divider-muted);
}
.codex-panel__config-row:first-child {
border-top: 0;
}
.codex-panel__config-section {
padding: var(--codex-panel-section-label-block-padding);
border-top: var(--codex-panel-divider-muted);
color: var(--codex-panel-section-label-color);
font-size: var(--codex-panel-section-label-size);
font-weight: var(--codex-panel-section-label-weight);
line-height: var(--codex-panel-section-label-line-height);
}
.codex-panel__config-section:first-child {
border-top: 0;
}
.codex-panel__config-row dt,
.codex-panel__config-row dd {
margin: 0;
min-width: 0;
font-size: var(--font-ui-smaller);
line-height: var(--codex-panel-compact-line-height);
}
.codex-panel__config-row dt {
color: var(--codex-panel-text-faint);
}
.codex-panel__config-row dd {
color: var(--codex-panel-text-muted);
overflow-wrap: anywhere;
cursor: text;
user-select: text;
white-space: pre-wrap;
}
.codex-panel__meta-grid {
display: grid;
grid-template-columns: max-content minmax(0, 1fr);

View file

@ -57,6 +57,27 @@ describe("chat panel surface projections", () => {
unmountUiRoot(parent);
});
it("renders raw runtime debug details from toolbar state sources", () => {
const state = createChatState();
state.ui.toolbarPanel = "status-panel";
state.connection.runtimeConfig = runtimeConfigFixture({ model: "gpt-debug", approval_policy: "on-request" });
state.connection.availableModels = [modelFixture("gpt-debug")];
state.runtime.requestedModel = { kind: "set", value: "gpt-debug" };
const parent = renderWithShellState(state, h(ChatPanelToolbar, { surface: toolbarSurfaceFixture(), actions: toolbarActionsFixture() }));
const debugContent = parent.querySelector(".codex-panel__debug-details-content")?.textContent;
if (!debugContent) throw new Error("Expected toolbar debug details");
const debugDetails = JSON.parse(debugContent) as Record<string, unknown>;
expect(debugDetails["vaultPath"]).toBe("/vault");
expect(debugDetails["configuredCommand"]).toBe("codex");
expect(debugDetails["runtimeConfig"]).toMatchObject({ model: "gpt-debug", approvalPolicy: "on-request" });
expect(debugDetails["runtime"]).toMatchObject({ requestedModel: { kind: "set", value: "gpt-debug" } });
expect(debugDetails["availableModels"]).toMatchObject([{ model: "gpt-debug" }]);
unmountUiRoot(parent);
});
it("builds composer meta from context and runtime state", () => {
const state = createChatState();
state.activeThread.id = "thread-1";

View file

@ -37,7 +37,7 @@ describe("Toolbar decisions", () => {
expect([...expectPresent(navButtons).children].map((button) => button.getAttribute("aria-label"))).toEqual([
"Show thread list",
"Show chat actions",
"Show Codex status and settings",
"Show status",
]);
expect(parent.querySelector(".codex-panel__plan-toggle")).toBeNull();
expect(parent.querySelector(".codex-panel__auto-review-toggle")).toBeNull();
@ -53,7 +53,7 @@ describe("Toolbar decisions", () => {
const statusButton = parent.querySelector(".codex-panel__status-menu-toggle");
expect(statusButton?.tagName).toBe("BUTTON");
expect(statusButton?.getAttribute("role")).toBeNull();
expect(statusButton?.getAttribute("aria-label")).toBe("Show Codex status and settings");
expect(statusButton?.getAttribute("aria-label")).toBe("Show status");
expect((statusButton as HTMLElement | null)?.dataset["icon"]).toBe("waypoints");
expect(statusButton?.classList.contains("nav-action-button")).toBe(true);
expect(statusButton?.classList.contains("clickable-icon")).toBe(true);
@ -75,7 +75,7 @@ describe("Toolbar decisions", () => {
expect(parent.querySelector(".codex-panel__history-toggle")?.classList.contains("is-active")).toBe(true);
expect(parent.querySelector(".codex-panel__new-chat")?.getAttribute("aria-label")).toBe("Hide chat actions");
expect(parent.querySelector(".codex-panel__new-chat")?.classList.contains("is-active")).toBe(true);
expect(parent.querySelector(".codex-panel__status-menu-toggle")?.getAttribute("aria-label")).toBe("Hide Codex status and settings");
expect(parent.querySelector(".codex-panel__status-menu-toggle")?.getAttribute("aria-label")).toBe("Hide status");
expect(parent.querySelector(".codex-panel__status-menu-toggle")?.classList.contains("is-active")).toBe(true);
});
@ -175,7 +175,7 @@ describe("Toolbar decisions", () => {
expect(parent.querySelector(".codex-panel__connection-diagnostics-title")?.textContent).toBe("Connection");
expect(parent.textContent).toContain("Process");
expect(parent.textContent).toContain("App Server Checks");
expect(parent.textContent).toContain("Effective Codex config");
expect(parent.textContent).toContain("Debug details");
expect(parent.textContent).toContain("Refresh status");
expect(parent.textContent).not.toContain("Refresh diagnostics");
expect(parent.textContent).not.toContain("Refresh thread list");
@ -188,7 +188,7 @@ describe("Toolbar decisions", () => {
expect(refreshStatus).toHaveBeenCalled();
});
it("renders effective config inside the status menu without a separate toggle", () => {
it("renders raw debug details inside a collapsed status menu section", () => {
const parent = document.createElement("div");
mountToolbar(
@ -196,16 +196,20 @@ describe("Toolbar decisions", () => {
toolbarModel({
statusPanelOpen: true,
openPanel: "status",
configSections: [{ title: "Runtime", rows: [{ key: "model", value: "gpt-5.5" }] }],
debugDetails: JSON.stringify({ runtimeConfig: { model: "gpt-5.5" } }, null, 2),
}),
toolbarActions(),
);
expect(parent.querySelector(".codex-panel__region--config")).toBeNull();
expect(parent.querySelector(".codex-panel__toolbar-panel .codex-panel__config")?.textContent).toContain("Effective Codex config");
const details = expectPresent(parent.querySelector<HTMLDetailsElement>(".codex-panel__debug-details"));
expect(details.open).toBe(false);
expect(details.querySelector("summary")?.textContent).toBe("Debug details");
expect(details.querySelector("pre")?.textContent).toContain('"model": "gpt-5.5"');
expect(parent.querySelector(".codex-panel__toolbar-panel .codex-panel__config")).toBeNull();
expect(parent.textContent).not.toContain("Effective Codex config");
expect(parent.textContent).not.toContain("Show effective config");
expect(parent.textContent).not.toContain("Hide effective config");
expect(parent.textContent).toContain("gpt-5.5");
});
it("renders thread list rename actions and an inline rename editor", () => {
@ -366,7 +370,7 @@ function toolbarModel(overrides: Partial<ToolbarViewModel> = {}): ToolbarViewMod
historyOpen: false,
statusPanelOpen: false,
rateLimit: null,
configSections: [],
debugDetails: "{}",
openPanel: null,
threads: [{ title: "Thread", threadId: "thread", selected: true, disabled: false, canArchive: true, rename: null }],
connectLabel: "Reconnect",

View file

@ -435,7 +435,7 @@ describe("CodexChatView connection lifecycle", () => {
await waitForAsyncWork(() => {
expect(view.containerEl.textContent).toContain("Restored thread");
});
requiredButton(view.containerEl, '[aria-label="Show Codex status and settings"]').click();
requiredButton(view.containerEl, '[aria-label="Show status"]').click();
await waitForAsyncWork(() => {
expect(view.containerEl.textContent).toContain("gpt-cached");
});

View file

@ -25,15 +25,7 @@ import {
pendingRuntimeSettingsPatch,
serviceTierRequestForThreadStart,
} from "../../src/features/chat/application/runtime/thread-settings-update";
import { contextSummary, runtimeConfigSections, rateLimitSummary } from "../../src/features/chat/presentation/runtime/status";
function runtimeRows(snapshot: RuntimeSnapshot): Record<string, string> {
return Object.fromEntries(
runtimeConfigSections(snapshot, "/vault")
.find((section) => section.title === "Runtime")
?.rows.map((row) => [row.key, row.value]) ?? [],
);
}
import { contextSummary, rateLimitSummary } from "../../src/features/chat/presentation/runtime/status";
describe("runtime settings", () => {
it("formats runtime override messages", () => {
@ -86,13 +78,7 @@ describe("runtime settings", () => {
collaborationModeWarning: null,
});
const runtimeRows = Object.fromEntries(
runtimeConfigSections(snapshot, "/vault")
.find((section) => section.title === "Runtime")
?.rows.map((row) => [row.key, row.value]) ?? [],
);
expect(runtimeRows["effective mode"]).toBe("(not reported)");
expect(runtimeRows["requested mode"]).toBe("(none)");
expect(snapshot.selectedCollaborationMode).toBe("default");
});
it("keeps model reset tied to config when active thread model differs", () => {
@ -280,8 +266,7 @@ describe("runtime settings", () => {
expect(currentModel(snapshot, snapshotConfig(snapshot))).toBe("gpt-profile");
expect(currentReasoningEffort(snapshot, snapshotConfig(snapshot))).toBe("high");
expect(currentServiceTier(snapshot, snapshotConfig(snapshot))).toBe("fast");
expect(runtimeRows(snapshot)["effective service tier"]).toBe("fast");
expect(runtimeRows(snapshot)["fast mode"]).toBe("on");
expect(fastModeActive(snapshot, snapshotConfig(snapshot))).toBe(true);
expect(serviceTierRequestForThreadStart(snapshot, snapshotConfig(snapshot))).toBe("fast");
});
@ -292,7 +277,7 @@ describe("runtime settings", () => {
});
expect(currentServiceTier(snapshot, snapshotConfig(snapshot))).toBe("flex");
expect(runtimeRows(snapshot)["fast mode"]).toBe("off");
expect(fastModeActive(snapshot, snapshotConfig(snapshot))).toBe(false);
});
it("treats the catalog Fast service tier id as fast mode while preserving the id", () => {
@ -307,9 +292,7 @@ describe("runtime settings", () => {
});
expect(currentServiceTier(snapshot, snapshotConfig(snapshot))).toBe("priority");
expect(runtimeRows(snapshot)["effective service tier"]).toBe("priority");
expect(fastModeActive(snapshot, snapshotConfig(snapshot))).toBe(true);
expect(runtimeRows(snapshot)["fast mode"]).toBe("on");
expect(fastRuntimeServiceTierRequestValue(snapshot, snapshotConfig(snapshot))).toBe("priority");
});
@ -324,9 +307,7 @@ describe("runtime settings", () => {
});
expect(currentServiceTier(snapshot, snapshotConfig(snapshot))).toBe("default");
expect(runtimeRows(snapshot)["effective service tier"]).toBe("default");
expect(fastModeActive(snapshot, snapshotConfig(snapshot))).toBe(false);
expect(runtimeRows(snapshot)["fast mode"]).toBe("off");
});
it("uses requested service tier above active and configured service tiers", () => {
@ -337,7 +318,7 @@ describe("runtime settings", () => {
});
expect(currentServiceTier(snapshot, snapshotConfig(snapshot))).toBeNull();
expect(runtimeRows(snapshot)["fast mode"]).toBe("off");
expect(fastModeActive(snapshot, snapshotConfig(snapshot))).toBe(false);
});
it("resolves requested approval reviewer without adding it to turn runtime settings", () => {
@ -363,105 +344,6 @@ describe("runtime settings", () => {
expect(pendingRuntimeSettingsPatch(snapshot, snapshotConfig(snapshot)).update).not.toHaveProperty("effort");
});
it("separates effective runtime, config defaults, and pending changes in status details", () => {
const sections = runtimeConfigSections(
runtimeSnapshot({
activeModel: "gpt-5-active",
activeReasoningEffort: "low",
activeCollaborationMode: "plan",
selectedCollaborationMode: "plan",
requestedModel: setPendingRuntimeSetting("gpt-5-pending"),
}),
"/vault",
);
const runtimeRows = Object.fromEntries(
sections.find((section) => section.title === "Runtime")?.rows.map((row) => [row.key, row.value]) ?? [],
);
expect(runtimeRows).toMatchObject({
"effective model": "gpt-5-pending",
"configured model": "gpt-5.5",
"thread model": "gpt-5-active",
"requested model": "gpt-5-pending",
"effective effort": "low",
"configured effort": "high",
"thread effort": "low",
"requested effort": "(none)",
"effective mode": "Plan",
"requested mode": "(none)",
"configured service tier": "flex",
"thread service tier": "(not reported)",
"requested service tier": "(none)",
});
const resetSections = runtimeConfigSections(
runtimeSnapshot({
requestedReasoningEffort: resetRuntimeSettingToConfig(),
}),
"/vault",
);
const resetRuntimeRows = Object.fromEntries(
resetSections.find((section) => section.title === "Runtime")?.rows.map((row) => [row.key, row.value]) ?? [],
);
expect(resetRuntimeRows["requested effort"]).toBe("(reset to config)");
});
it("labels unset config values as Codex defaults when no concrete value is reported", () => {
const sections = runtimeConfigSections(
runtimeSnapshot({
runtimeConfig: runtimeConfigFixture({}),
}),
"/vault",
);
const scopeRows = Object.fromEntries(
sections.find((section) => section.title === "Scope")?.rows.map((row) => [row.key, row.value]) ?? [],
);
const runtimeRows = Object.fromEntries(
sections.find((section) => section.title === "Runtime")?.rows.map((row) => [row.key, row.value]) ?? [],
);
const policyRows = Object.fromEntries(
sections.find((section) => section.title === "Policy")?.rows.map((row) => [row.key, row.value]) ?? [],
);
expect(scopeRows["model provider"]).toBe("(Codex default)");
expect(runtimeRows).toMatchObject({
"effective model": "(Codex default)",
"configured model": "(not reported)",
"thread model": "(not reported)",
"requested model": "(none)",
"configured service tier": "(Codex default)",
"thread service tier": "(not reported)",
"requested service tier": "(none)",
"fast mode": "Codex default",
});
expect(policyRows).toMatchObject({
"effective approval": "(Codex default)",
"configured approval": "(Codex default)",
"thread approval": "(not reported)",
"active permissions": "(not reported)",
"configured reviewer": "(Codex default)",
sandbox: "(Codex default)",
"web search": "(Codex default)",
});
});
it("uses catalog default reasoning efforts from model metadata", () => {
const model = { ...modelFixture("gpt-catalog-default"), isDefault: true, defaultReasoningEffort: "extreme" };
const sections = runtimeConfigSections(
runtimeSnapshot({
runtimeConfig: runtimeConfigFixture({}),
availableModels: [model],
}),
"/vault",
);
const runtimeRows = Object.fromEntries(
sections.find((section) => section.title === "Runtime")?.rows.map((row) => [row.key, row.value]) ?? [],
);
expect(runtimeRows["configured model"]).toBe("gpt-catalog-default");
expect(runtimeRows["configured effort"]).toBe("extreme");
});
it("uses the explicit config when finding supported reasoning efforts", () => {
const snapshot = runtimeSnapshot({
requestedModel: resetRuntimeSettingToConfig(),
@ -476,95 +358,11 @@ describe("runtime settings", () => {
expect(supportedReasoningEfforts(snapshot, explicitConfig)).toEqual(["high"]);
});
it("shows effective profile runtime and policy values in status details", () => {
const profileConfig = {
model: "gpt-profile",
model_provider: "profile-provider",
model_reasoning_effort: "high",
model_reasoning_summary: "detailed",
model_verbosity: "high",
approval_policy: "never",
approvals_reviewer: "auto_review",
web_search: "live",
service_tier: "fast",
};
const sections = runtimeConfigSections(
runtimeSnapshot({
runtimeConfig: runtimeConfigFixture(profileConfig, [configLayer({}, null), configLayer(profileConfig, "auto")]),
}),
"/vault",
);
const scopeRows = Object.fromEntries(
sections.find((section) => section.title === "Scope")?.rows.map((row) => [row.key, row.value]) ?? [],
);
const runtimeRows = Object.fromEntries(
sections.find((section) => section.title === "Runtime")?.rows.map((row) => [row.key, row.value]) ?? [],
);
const policyRows = Object.fromEntries(
sections.find((section) => section.title === "Policy")?.rows.map((row) => [row.key, row.value]) ?? [],
);
expect(scopeRows["profile"]).toBe("auto");
expect(scopeRows["model provider"]).toBe("profile-provider");
expect(runtimeRows).toMatchObject({
"effective model": "gpt-profile",
"configured model": "gpt-profile",
"thread model": "(not reported)",
"requested model": "(none)",
"effective effort": "high",
"configured effort": "high",
"thread effort": "(not reported)",
"requested effort": "(none)",
"reasoning summary": "detailed",
verbosity: "high",
"effective service tier": "fast",
"configured service tier": "fast",
"thread service tier": "(not reported)",
"requested service tier": "(none)",
"fast mode": "on",
});
expect(policyRows["effective approval"]).toBe("never");
expect(policyRows["configured approval"]).toBe("never");
expect(policyRows["thread approval"]).toBe("(not reported)");
expect(policyRows["active permissions"]).toBe("(not reported)");
expect(policyRows["effective reviewer"]).toBe("auto_review");
expect(policyRows["auto-review"]).toBe("on");
expect(policyRows["configured reviewer"]).toBe("auto_review");
expect(policyRows["thread reviewer"]).toBe("(not reported)");
expect(policyRows["requested reviewer"]).toBe("(none)");
expect(policyRows["web search"]).toBe("live");
});
it("shows active and configured reviewer inputs in status details", () => {
const sections = runtimeConfigSections(
runtimeSnapshot({
activeApprovalsReviewer: "guardian_subagent",
activeApprovalPolicy: "never",
activePermissionProfile: { id: ":workspace", extends: ":default" },
runtimeConfig: runtimeConfigFixture({ approvals_reviewer: "auto_review" }),
}),
"/vault",
);
const policyRows = Object.fromEntries(
sections.find((section) => section.title === "Policy")?.rows.map((row) => [row.key, row.value]) ?? [],
);
expect(policyRows).toMatchObject({
"effective approval": "never",
"thread approval": "never",
"active permissions": ":workspace extends :default",
"effective reviewer": "guardian_subagent",
"auto-review": "on",
"configured reviewer": "auto_review",
"thread reviewer": "guardian_subagent",
});
});
it("summarizes service tier and context meter state from one runtime snapshot", () => {
const snapshot = runtimeSnapshot({ requestedServiceTier: setPendingRuntimeSetting("fast"), activeThreadId: "thread" });
expect(runtimeRows(snapshot)["effective service tier"]).toBe("fast");
expect(runtimeRows(snapshot)["fast mode"]).toBe("on");
expect(currentServiceTier(snapshot, snapshotConfig(snapshot))).toBe("fast");
expect(fastModeActive(snapshot, snapshotConfig(snapshot))).toBe(true);
expect(contextSummary(snapshot)).toMatchObject({
label: "Context 0%",
title: "Context: 0 / 100,000 (0%). No turns in this thread yet.",
@ -604,8 +402,8 @@ describe("runtime settings", () => {
requestedServiceTier: setPendingRuntimeSetting("off"),
});
expect(runtimeRows(snapshot)["effective service tier"]).toBe("(Codex default)");
expect(runtimeRows(snapshot)["fast mode"]).toBe("off");
expect(currentServiceTier(snapshot, snapshotConfig(snapshot))).toBeNull();
expect(fastModeActive(snapshot, snapshotConfig(snapshot))).toBe(false);
expect(serviceTierRequestForThreadStart(snapshot, snapshotConfig(snapshot))).toBeNull();
});
@ -615,8 +413,8 @@ describe("runtime settings", () => {
requestedServiceTier: resetRuntimeSettingToConfig(),
});
expect(runtimeRows(snapshot)["effective service tier"]).toBe("fast");
expect(runtimeRows(snapshot)["fast mode"]).toBe("on");
expect(currentServiceTier(snapshot, snapshotConfig(snapshot))).toBe("fast");
expect(fastModeActive(snapshot, snapshotConfig(snapshot))).toBe(true);
expect(serviceTierRequestForThreadStart(snapshot, snapshotConfig(snapshot))).toBeNull();
});
@ -629,7 +427,7 @@ describe("runtime settings", () => {
availableModels: [model],
});
expect(runtimeRows(snapshot)["fast mode"]).toBe("on");
expect(fastModeActive(snapshot, snapshotConfig(snapshot))).toBe(true);
expect(serviceTierRequestForThreadStart(snapshot, snapshotConfig(snapshot))).toBe("priority");
});
@ -642,8 +440,8 @@ describe("runtime settings", () => {
it("passes through configured non-fast service tier ids", () => {
const snapshot = runtimeSnapshot({ runtimeConfig: runtimeConfigFixture({ service_tier: "flex" }) });
expect(runtimeRows(snapshot)["effective service tier"]).toBe("flex");
expect(runtimeRows(snapshot)["fast mode"]).toBe("off");
expect(currentServiceTier(snapshot, snapshotConfig(snapshot))).toBe("flex");
expect(fastModeActive(snapshot, snapshotConfig(snapshot))).toBe(false);
expect(serviceTierRequestForThreadStart(snapshot, snapshotConfig(snapshot))).toBe("flex");
});