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'
+}