diff --git a/src/app-server/client.ts b/src/app-server/client.ts index 33a65759..a8959579 100644 --- a/src/app-server/client.ts +++ b/src/app-server/client.ts @@ -170,7 +170,7 @@ export class AppServerClient { } readEffectiveConfig(cwd: string): Promise { - return this.request("config/read", { cwd, includeLayers: false }); + return this.request("config/read", { cwd, includeLayers: true }); } listHooks(cwd: string): Promise { diff --git a/src/panel/controller.ts b/src/panel/controller.ts index baa20662..560d3c27 100644 --- a/src/panel/controller.ts +++ b/src/panel/controller.ts @@ -427,8 +427,13 @@ export class PanelController { this.state.activeReasoningEffort = settings.effort ?? null; this.state.activeCollaborationMode = settings.collaborationMode.mode; this.state.requestedCollaborationMode = settings.collaborationMode.mode; - this.state.activeServiceTier = settings.serviceTier ?? null; - this.state.activeApprovalsReviewer = settings.approvalsReviewer ?? null; + const serviceTierOverride = this.state.activeServiceTierOverride || this.state.requestedServiceTier !== null; + const approvalsReviewerOverride = this.state.activeApprovalsReviewerOverride || this.state.requestedApprovalsReviewer !== null; + this.state.activeServiceTier = settings.serviceTier ?? (serviceTierOverride ? this.state.activeServiceTier : null); + this.state.activeServiceTierOverride = serviceTierOverride; + this.state.activeApprovalsReviewer = + settings.approvalsReviewer ?? (approvalsReviewerOverride ? this.state.activeApprovalsReviewer : null); + this.state.activeApprovalsReviewerOverride = approvalsReviewerOverride; } } diff --git a/src/panel/session-controller.ts b/src/panel/session-controller.ts index aec58c13..8bfa5394 100644 --- a/src/panel/session-controller.ts +++ b/src/panel/session-controller.ts @@ -48,7 +48,9 @@ export class PanelSessionController { this.host.state.activeModel = response.model ?? null; this.host.state.activeReasoningEffort = response.reasoningEffort ?? null; this.host.state.activeServiceTier = response.serviceTier ?? null; + this.host.state.activeServiceTierOverride = false; this.host.state.activeApprovalsReviewer = response.approvalsReviewer ?? null; + this.host.state.activeApprovalsReviewerOverride = false; this.host.state.activeThreadCliVersion = response.thread.cliVersion ?? null; this.host.state.tokenUsage = null; this.host.state.historyCursor = null; diff --git a/src/panel/state.ts b/src/panel/state.ts index 07400c25..a468c6f7 100644 --- a/src/panel/state.ts +++ b/src/panel/state.ts @@ -33,7 +33,9 @@ export interface PanelState { activeReasoningEffort: ReasoningEffort | null; activeCollaborationMode: ModeKind; activeServiceTier: string | null; + activeServiceTierOverride: boolean; activeApprovalsReviewer: ApprovalsReviewer | null; + activeApprovalsReviewerOverride: boolean; activeThreadCliVersion: string | null; appServerDiagnostics: AppServerDiagnostics; requestedModel: RuntimeOverride; @@ -78,7 +80,9 @@ export function createPanelState(): PanelState { activeReasoningEffort: null, activeCollaborationMode: "default", activeServiceTier: null, + activeServiceTierOverride: false, activeApprovalsReviewer: null, + activeApprovalsReviewerOverride: false, activeThreadCliVersion: null, appServerDiagnostics: createAppServerDiagnostics(), requestedModel: defaultRuntimeOverride(), @@ -128,7 +132,9 @@ export function clearActiveThreadState(state: PanelState): void { state.activeReasoningEffort = null; state.activeCollaborationMode = "default"; state.activeServiceTier = null; + state.activeServiceTierOverride = false; state.activeApprovalsReviewer = null; + state.activeApprovalsReviewerOverride = false; state.activeThreadCliVersion = null; state.tokenUsage = null; state.historyCursor = null; @@ -145,7 +151,9 @@ export function clearConnectionScopedState(state: PanelState): void { state.activeReasoningEffort = null; state.activeCollaborationMode = "default"; state.activeServiceTier = null; + state.activeServiceTierOverride = false; state.activeApprovalsReviewer = null; + state.activeApprovalsReviewerOverride = false; state.activeThreadCliVersion = null; state.rateLimit = null; state.listedThreads = []; diff --git a/src/panel/view.ts b/src/panel/view.ts index d2263700..8bfadebc 100644 --- a/src/panel/view.ts +++ b/src/panel/view.ts @@ -32,6 +32,7 @@ import { currentServiceTier, defaultRuntimeOverride, requestedTurnRuntimeSettings, + resolvedConfigValue, resetRuntimeOverride, runtimeOverridePayload, runtimeOverrideLabel, @@ -329,7 +330,9 @@ export class CodexPanelView extends ItemView { this.state.activeModel = response.model ?? null; this.state.activeReasoningEffort = response.reasoningEffort ?? null; this.state.activeServiceTier = response.serviceTier ?? null; + this.state.activeServiceTierOverride = false; this.state.activeApprovalsReviewer = response.approvalsReviewer ?? null; + this.state.activeApprovalsReviewerOverride = false; this.state.activeThreadCliVersion = response.thread.cliVersion ?? null; this.state.tokenUsage = null; this.state.displayItems = []; @@ -631,10 +634,12 @@ export class CodexPanelView extends ItemView { } if ("serviceTier" in update) { this.state.activeServiceTier = this.state.requestedServiceTier === "standard" ? "standard" : (update.serviceTier ?? null); + this.state.activeServiceTierOverride = true; this.state.requestedServiceTier = null; } if ("approvalsReviewer" in update) { this.state.activeApprovalsReviewer = update.approvalsReviewer ?? null; + this.state.activeApprovalsReviewerOverride = true; this.state.requestedApprovalsReviewer = null; } if (update.collaborationMode) { @@ -647,6 +652,7 @@ export class CodexPanelView extends ItemView { const next: ServiceTier = current === "fast" ? "standard" : "fast"; this.state.requestedServiceTier = next; this.state.activeServiceTier = next; + this.state.activeServiceTierOverride = true; this.state.runtimePicker = null; if (!(await this.applyPendingThreadSettings())) return; this.addSystemMessage(next === "fast" ? "Fast mode on for subsequent turns." : "Fast mode off for subsequent turns."); @@ -666,6 +672,7 @@ export class CodexPanelView extends ItemView { : "auto_review"; this.state.requestedApprovalsReviewer = next; this.state.activeApprovalsReviewer = next; + this.state.activeApprovalsReviewerOverride = true; this.state.runtimePicker = null; if (!(await this.applyPendingThreadSettings())) return; this.addSystemMessage(next === "auto_review" ? "Auto-review on for subsequent turns." : "Auto-review off for subsequent turns."); @@ -1009,7 +1016,7 @@ export class CodexPanelView extends ItemView { return [ `Model: ${currentModel(snapshot, config) ?? "(from default)"}`, `Override: ${runtimeOverrideLabel(this.state.requestedModel)}`, - `Provider: ${statusValue(config.model_provider, "(from default)")}`, + `Provider: ${statusValue(resolvedConfigValue(config, "model_provider"), "(from default)")}`, `Effort: ${currentReasoningEffort(snapshot, config) ?? "(from default)"}`, `Mode: ${this.collaborationModeLabel()}`, `Service tier: ${serviceTierLabel(snapshot, config)}`, @@ -1067,7 +1074,9 @@ export class CodexPanelView extends ItemView { activeReasoningEffort: this.state.activeReasoningEffort, activeCollaborationMode: this.state.activeCollaborationMode, activeServiceTier: this.state.activeServiceTier, + activeServiceTierOverride: this.state.activeServiceTierOverride, activeApprovalsReviewer: this.state.activeApprovalsReviewer, + activeApprovalsReviewerOverride: this.state.activeApprovalsReviewerOverride, requestedModel: this.state.requestedModel, requestedReasoningEffort: this.state.requestedReasoningEffort, requestedApprovalsReviewer: this.state.requestedApprovalsReviewer, diff --git a/src/runtime/state.ts b/src/runtime/state.ts index d0052097..c75ebfe5 100644 --- a/src/runtime/state.ts +++ b/src/runtime/state.ts @@ -21,7 +21,9 @@ export interface RuntimeSnapshot { activeReasoningEffort: ReasoningEffort | null; activeCollaborationMode: ModeKind; activeServiceTier: string | null; + activeServiceTierOverride: boolean; activeApprovalsReviewer: ApprovalsReviewer | null; + activeApprovalsReviewerOverride: boolean; requestedModel: RuntimeOverride; requestedReasoningEffort: RuntimeOverride; requestedApprovalsReviewer: ApprovalsReviewer | null; @@ -39,16 +41,26 @@ export interface TurnRuntimeSettings { } export function configRecord(effectiveConfig: ConfigReadResponse | null): Record { - return asRecord(effectiveConfig?.config); + return fillMissingConfigValues(asRecord(effectiveConfig?.config), effectiveConfig?.layers ?? null); +} + +export function selectedProfileConfig(config: Record): Record { + const profileName = typeof config.profile === "string" && config.profile.length > 0 ? config.profile : null; + return profileName ? asRecord(asRecord(config.profiles)[profileName]) : {}; +} + +export function resolvedConfigValue(config: Record, key: string): unknown { + const profileValue = selectedProfileConfig(config)[key]; + return profileValue === undefined || profileValue === null ? config[key] : profileValue; } export function currentServiceTier(snapshot: RuntimeSnapshot, config = configRecord(snapshot.effectiveConfig)): string | null { - return ( - snapshot.requestedServiceTier ?? - parseServiceTier(snapshot.activeServiceTier) ?? - snapshot.activeServiceTier ?? - configuredServiceTier(config) - ); + const active = parseServiceTier(snapshot.activeServiceTier) ?? snapshot.activeServiceTier; + const configured = configuredServiceTier(config); + if (snapshot.requestedServiceTier !== null) return snapshot.requestedServiceTier; + if (snapshot.activeServiceTierOverride) return active ?? configured; + if (configured && (active === null || active === "standard")) return configured; + return active ?? configured; } export function requestedOrConfiguredServiceTier( @@ -59,7 +71,7 @@ export function requestedOrConfiguredServiceTier( } export function currentModel(snapshot: RuntimeSnapshot, config = configRecord(snapshot.effectiveConfig)): string | null { - const model = config.model; + const model = resolvedConfigValue(config, "model"); const configModel = typeof model === "string" && model.length > 0 ? model : null; if (snapshot.requestedModel.kind === "set") return snapshot.requestedModel.value; if (snapshot.requestedModel.kind === "resetPending" && configModel) return configModel; @@ -68,7 +80,7 @@ export function currentModel(snapshot: RuntimeSnapshot, config = configRecord(sn export function currentReasoningEffort(snapshot: RuntimeSnapshot, config = configRecord(snapshot.effectiveConfig)): ReasoningEffort | null { if (snapshot.requestedReasoningEffort.kind === "set") return snapshot.requestedReasoningEffort.value; - const effort = config.model_reasoning_effort; + const effort = resolvedConfigValue(config, "model_reasoning_effort"); if (snapshot.requestedReasoningEffort.kind === "resetPending") return isReasoningEffort(effort) ? effort : null; return snapshot.activeReasoningEffort ?? (isReasoningEffort(effort) ? effort : null); } @@ -77,12 +89,20 @@ export function currentApprovalsReviewer( snapshot: RuntimeSnapshot, config = configRecord(snapshot.effectiveConfig), ): ApprovalsReviewer | null { - const configured = config.approvals_reviewer; - return snapshot.requestedApprovalsReviewer ?? snapshot.activeApprovalsReviewer ?? (isApprovalsReviewer(configured) ? configured : null); + const configured = configuredApprovalsReviewer(config); + if (snapshot.requestedApprovalsReviewer !== null) return snapshot.requestedApprovalsReviewer; + if (snapshot.activeApprovalsReviewerOverride) return snapshot.activeApprovalsReviewer ?? configured; + if (configured && (!snapshot.activeApprovalsReviewer || snapshot.activeApprovalsReviewer === "user")) return configured; + return snapshot.activeApprovalsReviewer ?? configured; } export function autoReviewActive(snapshot: RuntimeSnapshot, config = configRecord(snapshot.effectiveConfig)): boolean { - return currentApprovalsReviewer(snapshot, config) === "auto_review"; + return isAutoReviewReviewer(currentApprovalsReviewer(snapshot, config)); +} + +export function configuredApprovalsReviewer(config: Record): ApprovalsReviewer | null { + const value = resolvedConfigValue(config, "approvals_reviewer"); + return isApprovalsReviewer(value) ? value : null; } export function requestedTurnRuntimeSettings(snapshot: RuntimeSnapshot): TurnRuntimeSettings { @@ -146,14 +166,43 @@ export function runtimeOverrideLabel(override: RuntimeOverride): string { return "(none)"; } -function configuredServiceTier(config: Record): ServiceTier | null { - return parseServiceTier(config.service_tier); +export function configuredServiceTier(config: Record): ServiceTier | null { + return parseServiceTier(resolvedConfigValue(config, "service_tier")); } function isApprovalsReviewer(value: unknown): value is ApprovalsReviewer { return value === "user" || value === "auto_review" || value === "guardian_subagent"; } +function isAutoReviewReviewer(value: ApprovalsReviewer | null): boolean { + return value === "auto_review" || value === "guardian_subagent"; +} + function asRecord(value: unknown): Record { return value && typeof value === "object" ? (value as Record) : {}; } + +function fillMissingConfigValues(config: Record, layers: ConfigReadResponse["layers"]): Record { + if (!layers || layers.length === 0) return config; + const fallback = rawLayerConfigRecord(layers); + if (Object.keys(fallback).length === 0) return config; + + const merged = { ...config }; + for (const [key, value] of Object.entries(fallback)) { + if (merged[key] === undefined || merged[key] === null) { + merged[key] = value; + } + } + return merged; +} + +function rawLayerConfigRecord(layers: NonNullable): Record { + const merged: Record = {}; + for (const layer of layers) { + const config = asRecord(layer.config); + for (const [key, value] of Object.entries(config)) { + if (value !== undefined && value !== null) merged[key] = value; + } + } + return merged; +} diff --git a/src/runtime/view.ts b/src/runtime/view.ts index 2a9e40b8..fed0fdc1 100644 --- a/src/runtime/view.ts +++ b/src/runtime/view.ts @@ -4,9 +4,14 @@ import { jsonPreview } from "../utils"; import { sortedAvailableModels } from "./model"; import { configRecord, + configuredApprovalsReviewer, + configuredServiceTier, + currentApprovalsReviewer, currentModel, currentReasoningEffort, + autoReviewActive, fastModeLabel, + resolvedConfigValue, runtimeOverrideLabel, serviceTierLabel, type RuntimeSnapshot, @@ -102,7 +107,7 @@ export function rateLimitSummary(snapshot: RuntimeSnapshot, nowMs = Date.now()): export function effectiveConfigSections(snapshot: RuntimeSnapshot, vaultPath: string): EffectiveConfigSection[] { const config = configRecord(snapshot.effectiveConfig); const features = asRecord(config.features); - const tools = asRecord(config.tools); + const tools = asRecord(resolvedConfigValue(config, "tools")); const workspaceWrite = asRecord(config.sandbox_workspace_write); return [ { @@ -110,7 +115,7 @@ export function effectiveConfigSections(snapshot: RuntimeSnapshot, vaultPath: st rows: [ { key: "cwd", value: vaultPath }, { key: "profile", value: stringValue(config.profile, "(default)") }, - { key: "model provider", value: stringValue(config.model_provider, "(from default)") }, + { key: "model provider", value: stringValue(resolvedConfigValue(config, "model_provider"), "(from default)") }, ], }, { @@ -122,8 +127,8 @@ export function effectiveConfigSections(snapshot: RuntimeSnapshot, vaultPath: st { key: "effort", value: currentReasoningEffort(snapshot, config) ?? "(from default)" }, { key: "config effort", value: configuredReasoningEffort(snapshot, config) ?? "(not reported)" }, { key: "effort change", value: runtimeOverrideLabel(snapshot.requestedReasoningEffort) }, - { key: "reasoning summary", value: stringValue(config.model_reasoning_summary, "(from default)") }, - { key: "verbosity", value: stringValue(config.model_verbosity, "(from default)") }, + { key: "reasoning summary", value: stringValue(resolvedConfigValue(config, "model_reasoning_summary"), "(from default)") }, + { key: "verbosity", value: stringValue(resolvedConfigValue(config, "model_verbosity"), "(from default)") }, { key: "mode", value: snapshot.activeCollaborationMode === "plan" ? "Plan" : "Default" }, { key: "mode change", @@ -133,6 +138,8 @@ export function effectiveConfigSections(snapshot: RuntimeSnapshot, vaultPath: st : modeLabel(snapshot.requestedCollaborationMode), }, { key: "service tier", value: serviceTierLabel(snapshot, config) }, + { key: "config service tier", value: configuredServiceTier(config) ?? "(not reported)" }, + { key: "active service tier", value: activeRuntimeValueLabel(snapshot.activeServiceTier, snapshot.activeServiceTierOverride) }, { key: "fast mode", value: fastModeLabel(snapshot, config) }, { key: "context window", value: tokenLimitLabel(config.model_context_window) }, { key: "auto compact limit", value: tokenLimitLabel(config.model_auto_compact_token_limit) }, @@ -141,12 +148,18 @@ export function effectiveConfigSections(snapshot: RuntimeSnapshot, vaultPath: st { title: "Policy", rows: [ - { key: "approval", value: stringValue(config.approval_policy, "(from default)") }, - { key: "reviewer", value: stringValue(config.approvals_reviewer, "(from default)") }, + { key: "approval", value: stringValue(resolvedConfigValue(config, "approval_policy"), "(from default)") }, + { key: "reviewer", value: currentApprovalsReviewer(snapshot, config) ?? "(not reported)" }, + { key: "auto-review", value: autoReviewActive(snapshot, config) ? "on" : "off" }, + { key: "config reviewer", value: configuredApprovalsReviewer(config) ?? "(from default)" }, + { + key: "active reviewer", + value: activeRuntimeValueLabel(snapshot.activeApprovalsReviewer, snapshot.activeApprovalsReviewerOverride), + }, { key: "sandbox", value: stringValue(config.sandbox_mode, "(from default)") }, { key: "workspace network", value: stringValue(workspaceWrite.network_access, "(from default)") }, { key: "writable roots", value: writableRootsLabel(workspaceWrite.writable_roots) }, - { key: "web search", value: stringValue(config.web_search, "(from default)") }, + { key: "web search", value: stringValue(resolvedConfigValue(config, "web_search"), "(from default)") }, ], }, { @@ -166,13 +179,15 @@ function contextUsageTokens(usage: ThreadTokenUsage): number { } function configuredModel(snapshot: RuntimeSnapshot, config: Record): string | null { - if (typeof config.model === "string" && config.model.length > 0) return config.model; + const model = resolvedConfigValue(config, "model"); + if (typeof model === "string" && model.length > 0) return model; return sortedAvailableModels(snapshot.availableModels).find((model) => model.isDefault)?.model ?? null; } function configuredReasoningEffort(snapshot: RuntimeSnapshot, config: Record): string | null { - if (typeof config.model_reasoning_effort === "string" && config.model_reasoning_effort.length > 0) { - return config.model_reasoning_effort; + const effort = resolvedConfigValue(config, "model_reasoning_effort"); + if (typeof effort === "string" && effort.length > 0) { + return effort; } const model = configuredModel(snapshot, config); return ( @@ -184,6 +199,10 @@ function modeLabel(mode: RuntimeSnapshot["requestedCollaborationMode"]): string return mode === "plan" ? "Plan" : "Default"; } +function activeRuntimeValueLabel(value: string | null, override: boolean): string { + return `${value ?? "(not reported)"}${override ? " (override)" : ""}`; +} + function rateLimitWindowSummary( fallbackLabel: string, window: RateLimitWindow | null, diff --git a/tests/app-server/app-server-client.test.ts b/tests/app-server/app-server-client.test.ts index b69cd441..f53fc69f 100644 --- a/tests/app-server/app-server-client.test.ts +++ b/tests/app-server/app-server-client.test.ts @@ -377,6 +377,19 @@ describe("AppServerClient", () => { await update; }); + it("requests effective config with raw layers for display fallbacks", async () => { + const { client, transport } = await connectedClient(); + + const reading = client.readEffectiveConfig("/vault"); + expect(transport.sent[2]).toMatchObject({ + id: 2, + method: "config/read", + params: { cwd: "/vault", includeLayers: true }, + }); + transport.emitLine({ id: 2, result: { config: {}, origins: {}, layers: [] } }); + await reading; + }); + it("sends model list request payloads", async () => { const { client, transport } = await connectedClient(); diff --git a/tests/panel/panel-controller.test.ts b/tests/panel/panel-controller.test.ts index 178de2bb..43ff19b1 100644 --- a/tests/panel/panel-controller.test.ts +++ b/tests/panel/panel-controller.test.ts @@ -1029,6 +1029,41 @@ describe("PanelController", () => { expect(state.displayItems).toEqual([]); }); + it("preserves explicit standard service tier when settings notifications report null", () => { + const state = createPanelState(); + state.activeThreadId = "thread-active"; + state.activeServiceTier = "standard"; + state.activeServiceTierOverride = true; + const controller = controllerForState(state); + + controller.handleNotification({ + method: "thread/settings/updated", + params: { + threadId: "thread-active", + threadSettings: { + cwd: "/workspace/active", + approvalPolicy: "on-request", + approvalsReviewer: "user", + sandboxPolicy: { type: "readOnly", networkAccess: false }, + activePermissionProfile: null, + model: "gpt-5.5", + modelProvider: "openai", + serviceTier: null, + effort: "high", + summary: null, + collaborationMode: { + mode: "default", + settings: { model: "gpt-5.5", reasoning_effort: "high", developer_instructions: null }, + }, + personality: null, + }, + }, + } satisfies Extract); + + expect(state.activeServiceTier).toBe("standard"); + expect(state.activeServiceTierOverride).toBe(true); + }); + it("keeps goal notifications out of the message stream", () => { const state = createPanelState(); state.activeThreadId = "thread-active"; diff --git a/tests/runtime/runtime-settings.test.ts b/tests/runtime/runtime-settings.test.ts index c55d7f37..b2ef158d 100644 --- a/tests/runtime/runtime-settings.test.ts +++ b/tests/runtime/runtime-settings.test.ts @@ -11,6 +11,7 @@ import { } from "../../src/runtime/settings"; import { autoReviewActive, + configRecord, currentApprovalsReviewer, currentModel, currentReasoningEffort, @@ -119,6 +120,128 @@ describe("runtime settings", () => { ).toBe("guardian_subagent"); }); + it("lets config auto-review beat an implicit initial user reviewer", () => { + const snapshot = runtimeSnapshot({ + activeApprovalsReviewer: "user", + activeApprovalsReviewerOverride: false, + effectiveConfig: { config: { approvals_reviewer: "auto_review" } } as unknown as RuntimeSnapshot["effectiveConfig"], + }); + + expect(currentApprovalsReviewer(snapshot)).toBe("auto_review"); + expect(autoReviewActive(snapshot)).toBe(true); + }); + + it("uses raw config layers when typed config omits approval reviewer", () => { + const config = configRecord({ + config: { + approvals_reviewer: null, + }, + layers: [ + { + name: { type: "user", file: "/home/me/.codex/config.toml", profile: null }, + version: "1", + config: { approvals_reviewer: "auto_review" }, + disabledReason: null, + }, + ], + } as unknown as RuntimeSnapshot["effectiveConfig"]); + const snapshot = runtimeSnapshot({ + activeApprovalsReviewer: "user", + activeApprovalsReviewerOverride: false, + effectiveConfig: { config } as unknown as RuntimeSnapshot["effectiveConfig"], + }); + + expect(config.approvals_reviewer).toBe("auto_review"); + expect(autoReviewActive(snapshot, config)).toBe(true); + }); + + it("treats guardian subagent reviewer as active auto-review", () => { + const snapshot = runtimeSnapshot({ + activeApprovalsReviewer: "guardian_subagent", + activeApprovalsReviewerOverride: false, + }); + + expect(currentApprovalsReviewer(snapshot)).toBe("guardian_subagent"); + expect(autoReviewActive(snapshot)).toBe(true); + }); + + it("keeps an explicit user reviewer override above config auto-review", () => { + const snapshot = runtimeSnapshot({ + activeApprovalsReviewer: "user", + activeApprovalsReviewerOverride: true, + effectiveConfig: { config: { approvals_reviewer: "auto_review" } } as unknown as RuntimeSnapshot["effectiveConfig"], + }); + + expect(currentApprovalsReviewer(snapshot)).toBe("user"); + expect(autoReviewActive(snapshot)).toBe(false); + }); + + it("resolves approval reviewer from the selected config profile", () => { + const snapshot = runtimeSnapshot({ + effectiveConfig: { + config: { + approvals_reviewer: "user", + profile: "auto", + profiles: { + auto: { approvals_reviewer: "auto_review" }, + }, + }, + } as unknown as RuntimeSnapshot["effectiveConfig"], + }); + + expect(currentApprovalsReviewer(snapshot)).toBe("auto_review"); + expect(autoReviewActive(snapshot)).toBe(true); + }); + + it("resolves model, effort, and fast mode from the selected config profile", () => { + const snapshot = runtimeSnapshot({ + effectiveConfig: { + config: { + model: "gpt-root", + model_reasoning_effort: "low", + service_tier: "standard", + profile: "fast-profile", + profiles: { + "fast-profile": { + model: "gpt-profile", + model_reasoning_effort: "high", + service_tier: "fast", + }, + }, + }, + } as unknown as RuntimeSnapshot["effectiveConfig"], + }); + + expect(currentModel(snapshot)).toBe("gpt-profile"); + expect(currentReasoningEffort(snapshot)).toBe("high"); + expect(currentServiceTier(snapshot)).toBe("fast"); + expect(serviceTierLabel(snapshot)).toBe("fast"); + expect(fastModeLabel(snapshot)).toBe("on"); + expect(requestedOrConfiguredServiceTier(snapshot)).toBe("fast"); + }); + + it("lets config fast mode beat an implicit initial standard service tier", () => { + const snapshot = runtimeSnapshot({ + activeServiceTier: "standard", + activeServiceTierOverride: false, + effectiveConfig: { config: { service_tier: "fast" } } as unknown as RuntimeSnapshot["effectiveConfig"], + }); + + expect(currentServiceTier(snapshot)).toBe("fast"); + expect(fastModeLabel(snapshot)).toBe("on"); + }); + + it("keeps an explicit standard service tier override above config fast mode", () => { + const snapshot = runtimeSnapshot({ + activeServiceTier: "standard", + activeServiceTierOverride: true, + effectiveConfig: { config: { service_tier: "fast" } } as unknown as RuntimeSnapshot["effectiveConfig"], + }); + + expect(currentServiceTier(snapshot)).toBe("standard"); + expect(fastModeLabel(snapshot)).toBe("off"); + }); + it("resolves requested approval reviewer without adding it to turn runtime settings", () => { const snapshot = runtimeSnapshot({ requestedApprovalsReviewer: "auto_review" }); @@ -170,6 +293,8 @@ describe("runtime settings", () => { "effort change": "(none)", mode: "Plan", "mode change": "(none)", + "config service tier": "standard", + "active service tier": "(not reported)", }); const resetSections = effectiveConfigSections( @@ -184,6 +309,90 @@ describe("runtime settings", () => { expect(resetRuntimeRows["effort change"]).toBe("(reset to config)"); }); + it("shows selected profile runtime and policy values in status details", () => { + const sections = effectiveConfigSections( + runtimeSnapshot({ + effectiveConfig: { + config: { + model: "gpt-root", + model_provider: "root-provider", + model_reasoning_effort: "low", + model_reasoning_summary: "auto", + model_verbosity: "low", + approval_policy: "on-request", + web_search: "disabled", + service_tier: "standard", + profile: "auto", + profiles: { + auto: { + 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", + }, + }, + }, + } as unknown as RuntimeSnapshot["effectiveConfig"], + }), + "/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("profile-provider"); + expect(runtimeRows).toMatchObject({ + model: "gpt-profile", + "config model": "gpt-profile", + effort: "high", + "config effort": "high", + "reasoning summary": "detailed", + verbosity: "high", + "service tier": "fast", + "config service tier": "fast", + "active service tier": "(not reported)", + "fast mode": "on", + }); + expect(policyRows.approval).toBe("never"); + expect(policyRows.reviewer).toBe("auto_review"); + expect(policyRows["auto-review"]).toBe("on"); + expect(policyRows["config reviewer"]).toBe("auto_review"); + expect(policyRows["active reviewer"]).toBe("(not reported)"); + expect(policyRows["web search"]).toBe("live"); + }); + + it("shows active and configured reviewer inputs in status details", () => { + const sections = effectiveConfigSections( + runtimeSnapshot({ + activeApprovalsReviewer: "guardian_subagent", + activeApprovalsReviewerOverride: false, + effectiveConfig: { config: { approvals_reviewer: "auto_review" } } as unknown as RuntimeSnapshot["effectiveConfig"], + }), + "/vault", + ); + const policyRows = Object.fromEntries( + sections.find((section) => section.title === "Policy")?.rows.map((row) => [row.key, row.value]) ?? [], + ); + + expect(policyRows).toMatchObject({ + reviewer: "guardian_subagent", + "auto-review": "on", + "config reviewer": "auto_review", + "active reviewer": "guardian_subagent", + }); + }); + it("summarizes service tier and context meter state from one runtime snapshot", () => { const snapshot = runtimeSnapshot({ requestedServiceTier: "fast", activeThreadId: "thread" }); @@ -324,7 +533,9 @@ function runtimeSnapshot(overrides: Partial = {}): RuntimeSnaps activeReasoningEffort: null, activeCollaborationMode: "default", activeServiceTier: null, + activeServiceTierOverride: false, activeApprovalsReviewer: null, + activeApprovalsReviewerOverride: false, requestedModel: { kind: "default" }, requestedReasoningEffort: { kind: "default" }, requestedApprovalsReviewer: null,