diff --git a/README.md b/README.md index 7601f2a5..fa46914b 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/domain/catalog/metadata.ts b/src/domain/catalog/metadata.ts index f9c17fcf..05825f1f 100644 --- a/src/domain/catalog/metadata.ts +++ b/src/domain/catalog/metadata.ts @@ -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) diff --git a/src/features/chat/domain/runtime/effective.ts b/src/features/chat/domain/runtime/effective.ts index d33a08bc..b79f6fa0 100644 --- a/src/features/chat/domain/runtime/effective.ts +++ b/src/features/chat/domain/runtime/effective.ts @@ -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)); } diff --git a/src/features/chat/panel/surface/toolbar-projection.tsx b/src/features/chat/panel/surface/toolbar-projection.tsx index 6394c654..b37100a8 100644 --- a/src/features/chat/panel/surface/toolbar-projection.tsx +++ b/src/features/chat/panel/surface/toolbar-projection.tsx @@ -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; +type ToolbarState = Pick; 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): ToolbarStateProjection { const historyOpen = input.state.ui.toolbarPanel === "history"; const chatActionsOpen = input.state.ui.toolbarPanel === "chat-actions"; diff --git a/src/features/chat/presentation/runtime/messages.ts b/src/features/chat/presentation/runtime/messages.ts index f5f8b5df..f7e16bc7 100644 --- a/src/features/chat/presentation/runtime/messages.ts +++ b/src/features/chat/presentation/runtime/messages.ts @@ -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"; -} diff --git a/src/features/chat/presentation/runtime/status.ts b/src/features/chat/presentation/runtime/status.ts index 90d83b13..eb8f871c 100644 --- a/src/features/chat/presentation/runtime/status.ts +++ b/src/features/chat/presentation/runtime/status.ts @@ -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 { - return value && typeof value === "object" ? (value as Record) : {}; -} - 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)); } diff --git a/src/features/chat/ui/toolbar.tsx b/src/features/chat/ui/toolbar.tsx index 03a3d81f..72e8555f 100644 --- a/src/features/chat/ui/toolbar.tsx +++ b/src/features/chat/ui/toolbar.tsx @@ -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 ( - + ); } @@ -243,30 +243,12 @@ function DiagnosticSection({ section }: { section: ToolbarDiagnosticSection }): ); } -function RuntimeConfigPanel({ sections }: { sections: RuntimeConfigSection[] }): UiNode { +function DebugDetails({ details }: { details: string }): UiNode { return ( -
-
Effective Codex config
-
- {sections.map((section) => ( - - ))} -
-
- ); -} - -function FragmentedConfigSection({ section }: { section: RuntimeConfigSection }): UiNode { - return ( - <> -
{section.title}
- {section.rows.map((row) => ( -
-
{row.key}
-
{row.value}
-
- ))} - +
+ Debug details +
{details}
+
); } diff --git a/src/features/thread-picker/modal.ts b/src/features/thread-picker/modal.ts index fcc4b040..34dd1e8c 100644 --- a/src/features/thread-picker/modal.ts +++ b/src/features/thread-picker/modal.ts @@ -17,7 +17,7 @@ export interface ThreadPickerHost { openThreadInAvailableView(threadId: string): Promise; } -export type ThreadPickerCatalog = Pick; +type ThreadPickerCatalog = Pick; interface ThreadSuggestion { thread: Thread; diff --git a/src/features/threads-view/session.ts b/src/features/threads-view/session.ts index 96f7a0cf..bf486ceb 100644 --- a/src/features/threads-view/session.ts +++ b/src/features/threads-view/session.ts @@ -39,7 +39,7 @@ export interface CodexThreadsHost { getOpenPanelSnapshots(): OpenCodexPanelSnapshot[]; } -export type ThreadsThreadCatalog = Pick< +type ThreadsThreadCatalog = Pick< SharedThreadCatalog, "archiveThreadInCatalog" | "renameThreadInCatalog" | "refreshFromOpenSurface" | "refreshThreads" | "cachedThreads" >; diff --git a/src/settings/dynamic-data-controller.ts b/src/settings/dynamic-data-controller.ts index 0a3e595f..50e06d81 100644 --- a/src/settings/dynamic-data-controller.ts +++ b/src/settings/dynamic-data-controller.ts @@ -24,7 +24,7 @@ export interface SettingsDynamicDataHost { threadCatalog: SettingsThreadCatalog; } -export type SettingsThreadCatalog = Pick; +type SettingsThreadCatalog = Pick; interface SettingsDynamicDataControllerCallbacks { display(): void; diff --git a/src/styles/21-chat-status-diagnostics.css b/src/styles/21-chat-status-diagnostics.css index 5d0d2c8f..63b59d94 100644 --- a/src/styles/21-chat-status-diagnostics.css +++ b/src/styles/21-chat-status-diagnostics.css @@ -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 { diff --git a/src/styles/30-chat-layout.css b/src/styles/30-chat-layout.css index 05e3a2e9..d4ec0927 100644 --- a/src/styles/30-chat-layout.css +++ b/src/styles/30-chat-layout.css @@ -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); diff --git a/tests/features/chat/panel/surface/projection.test.ts b/tests/features/chat/panel/surface/projection.test.ts index a58c13ec..465fa051 100644 --- a/tests/features/chat/panel/surface/projection.test.ts +++ b/tests/features/chat/panel/surface/projection.test.ts @@ -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; + + 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"; diff --git a/tests/features/chat/ui/renderers/toolbar.test.ts b/tests/features/chat/ui/renderers/toolbar.test.ts index d8225d0d..5902005d 100644 --- a/tests/features/chat/ui/renderers/toolbar.test.ts +++ b/tests/features/chat/ui/renderers/toolbar.test.ts @@ -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(".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 = {}): 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", diff --git a/tests/features/chat/view-connection.test.ts b/tests/features/chat/view-connection.test.ts index 51aafca1..8f5e5468 100644 --- a/tests/features/chat/view-connection.test.ts +++ b/tests/features/chat/view-connection.test.ts @@ -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"); }); diff --git a/tests/runtime/runtime-settings.test.ts b/tests/runtime/runtime-settings.test.ts index bfa1838e..3ab8b26d 100644 --- a/tests/runtime/runtime-settings.test.ts +++ b/tests/runtime/runtime-settings.test.ts @@ -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 { - 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"); });