From b104c38f3ad84f54bc88884f6670488ae196d3d2 Mon Sep 17 00:00:00 2001 From: SSU-WEI HUANG Date: Sun, 12 Jul 2026 18:41:10 +0800 Subject: [PATCH] fix(web): show Codex reasoning effort in SessionHeader (#1016) Surface the same reasoning label already shown in the composer StatusBar in the top SessionHeader for codex/opencode sessions. Also show an explicit Fast badge only when serviceTier is fast (#1004-aligned). Closes #1015 (header display portion). Co-authored-by: Cursor --- .../components/AssistantChat/StatusBar.tsx | 9 ++------ web/src/components/SessionHeader.tsx | 18 +++++++++++++++ web/src/lib/codexStatusLabels.test.ts | 23 +++++++++++++++++++ web/src/lib/codexStatusLabels.ts | 11 +++++++++ 4 files changed, 54 insertions(+), 7 deletions(-) create mode 100644 web/src/lib/codexStatusLabels.test.ts create mode 100644 web/src/lib/codexStatusLabels.ts diff --git a/web/src/components/AssistantChat/StatusBar.tsx b/web/src/components/AssistantChat/StatusBar.tsx index 7032e7b6..32582ef1 100644 --- a/web/src/components/AssistantChat/StatusBar.tsx +++ b/web/src/components/AssistantChat/StatusBar.tsx @@ -10,6 +10,7 @@ import type { AgentState, CodexCollaborationMode, PermissionMode } from '@/types import type { ConversationStatus } from '@/realtime/types' import type { ThreadGoal } from '@/types/api' import { getContextBudgetTokens } from '@/chat/modelConfig' +import { formatCodexReasoningLabel, shouldShowCodexReasoningLabel } from '@/lib/codexStatusLabels' import { isFastServiceTier } from './codexFastMode' import { useTranslation } from '@/lib/use-translation' @@ -124,12 +125,6 @@ function formatTokenCount(value: number): string { return String(value) } -function formatCodexReasoningLabel(effort?: string | null): string { - const normalized = effort?.trim().toLowerCase() - if (!normalized || normalized === 'default') return 'reasoning default' - return `reasoning ${normalized}` -} - function isCodexFastMode(model?: string | null, effort?: string | null): boolean { const normalizedEffort = effort?.trim().toLowerCase() if (normalizedEffort === 'none' || normalizedEffort === 'minimal' || normalizedEffort === 'low') { @@ -212,7 +207,7 @@ export function StatusBar(props: { const collaborationModeLabel = displayCollaborationMode ? getCodexCollaborationModeLabel(displayCollaborationMode) : null - const codexReasoningLabel = (props.agentFlavor === 'codex' || props.agentFlavor === 'opencode') + const codexReasoningLabel = shouldShowCodexReasoningLabel(props.agentFlavor) ? formatCodexReasoningLabel(props.modelReasoningEffort) : null // Prefer the explicit service tier (the real Fast-mode toggle) when set; diff --git a/web/src/components/SessionHeader.tsx b/web/src/components/SessionHeader.tsx index c80ac68a..5470307c 100644 --- a/web/src/components/SessionHeader.tsx +++ b/web/src/components/SessionHeader.tsx @@ -8,9 +8,11 @@ import { SessionExportDialog } from '@/components/SessionExportDialog' import { RenameSessionDialog } from '@/components/RenameSessionDialog' import { ConfirmDialog } from '@/components/ui/ConfirmDialog' import { formatReopenError } from '@/lib/reopenError' +import { formatCodexReasoningLabel, shouldShowCodexReasoningLabel } from '@/lib/codexStatusLabels' import { getSessionModelLabel } from '@/lib/sessionModelLabel' import { useTranslation } from '@/lib/use-translation' import { AgentFlavorIcon } from '@/components/AgentFlavorIcon' +import { isFastServiceTier } from '@/components/AssistantChat/codexFastMode' function getSessionTitle(session: Session): string { if (session.metadata?.name) { @@ -111,6 +113,12 @@ export function SessionHeader(props: { const title = useMemo(() => getSessionTitle(session), [session]) const worktreeBranch = session.metadata?.worktree?.branch const modelLabel = getSessionModelLabel(session) + const agentFlavor = session.metadata?.flavor ?? null + const reasoningLabel = shouldShowCodexReasoningLabel(agentFlavor) + ? formatCodexReasoningLabel(session.modelReasoningEffort) + : null + // Match expected Fast badge semantics (#1004): only explicit service tier, no effort/model heuristics. + const showFastBadge = agentFlavor === 'codex' && isFastServiceTier(session.serviceTier) const [menuOpen, setMenuOpen] = useState(false) const [menuAnchorPoint, setMenuAnchorPoint] = useState<{ x: number; y: number }>({ x: 0, y: 0 }) @@ -198,6 +206,16 @@ export function SessionHeader(props: { {t(modelLabel.key)}: {modelLabel.value} ) : null} + {reasoningLabel ? ( + + {reasoningLabel} + + ) : null} + {showFastBadge ? ( + + fast + + ) : null} {worktreeBranch ? ( {t('session.item.worktree')}: {worktreeBranch} ) : null} diff --git a/web/src/lib/codexStatusLabels.test.ts b/web/src/lib/codexStatusLabels.test.ts new file mode 100644 index 00000000..779f5a07 --- /dev/null +++ b/web/src/lib/codexStatusLabels.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from 'vitest' +import { formatCodexReasoningLabel, shouldShowCodexReasoningLabel } from './codexStatusLabels' + +describe('codexStatusLabels', () => { + it('formats unset and default effort as reasoning default', () => { + expect(formatCodexReasoningLabel(null)).toBe('reasoning default') + expect(formatCodexReasoningLabel(undefined)).toBe('reasoning default') + expect(formatCodexReasoningLabel('default')).toBe('reasoning default') + expect(formatCodexReasoningLabel(' DEFAULT ')).toBe('reasoning default') + }) + + it('formats selected efforts', () => { + expect(formatCodexReasoningLabel('xhigh')).toBe('reasoning xhigh') + expect(formatCodexReasoningLabel('Ultra')).toBe('reasoning ultra') + }) + + it('only shows the label for codex and opencode', () => { + expect(shouldShowCodexReasoningLabel('codex')).toBe(true) + expect(shouldShowCodexReasoningLabel('opencode')).toBe(true) + expect(shouldShowCodexReasoningLabel('claude')).toBe(false) + expect(shouldShowCodexReasoningLabel(null)).toBe(false) + }) +}) diff --git a/web/src/lib/codexStatusLabels.ts b/web/src/lib/codexStatusLabels.ts new file mode 100644 index 00000000..d4960c6b --- /dev/null +++ b/web/src/lib/codexStatusLabels.ts @@ -0,0 +1,11 @@ +/** Labels shared by SessionHeader and composer StatusBar for Codex/OpenCode. */ + +export function formatCodexReasoningLabel(effort?: string | null): string { + const normalized = effort?.trim().toLowerCase() + if (!normalized || normalized === 'default') return 'reasoning default' + return `reasoning ${normalized}` +} + +export function shouldShowCodexReasoningLabel(agentFlavor: string | null | undefined): boolean { + return agentFlavor === 'codex' || agentFlavor === 'opencode' +}