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 <cursoragent@cursor.com>
This commit is contained in:
SSU-WEI HUANG
2026-07-12 18:41:10 +08:00
committed by GitHub
co-authored by Cursor
parent d0c8391261
commit b104c38f3a
4 changed files with 54 additions and 7 deletions
@@ -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;
+18
View File
@@ -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}
</span>
) : null}
{reasoningLabel ? (
<span data-testid="session-header-reasoning">
{reasoningLabel}
</span>
) : null}
{showFastBadge ? (
<span data-testid="session-header-fast" className="text-[#34C759]">
fast
</span>
) : null}
{worktreeBranch ? (
<span>{t('session.item.worktree')}: {worktreeBranch}</span>
) : null}
+23
View File
@@ -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)
})
})
+11
View File
@@ -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'
}