mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
feat(web): refine composer status bar (#1281)
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import { I18nProvider } from '@/lib/i18n-context'
|
||||
import { StatusBar } from './StatusBar'
|
||||
|
||||
describe('StatusBar context details popover', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear()
|
||||
})
|
||||
|
||||
it('keeps stable connection labels in English and offsets the whole left status', () => {
|
||||
localStorage.setItem('hapi-lang', 'zh-CN')
|
||||
const { rerender } = render(
|
||||
<I18nProvider>
|
||||
<StatusBar active thinking={false} agentState={null} />
|
||||
</I18nProvider>
|
||||
)
|
||||
|
||||
const onlineLabel = screen.getByText('online')
|
||||
expect(onlineLabel.className.split(' ')).not.toContain('top-px')
|
||||
expect(onlineLabel.previousElementSibling?.className.split(' ')).not.toContain('top-px')
|
||||
expect(onlineLabel.parentElement?.className.split(' ')).toContain('top-px')
|
||||
expect(onlineLabel.parentElement?.className.split(' ')).toContain('sm:top-0.5')
|
||||
|
||||
rerender(
|
||||
<I18nProvider>
|
||||
<StatusBar active={false} thinking={false} agentState={null} />
|
||||
</I18nProvider>
|
||||
)
|
||||
|
||||
const offlineLabel = screen.getByText('offline')
|
||||
expect(offlineLabel.className.split(' ')).toContain('text-[#999]')
|
||||
expect(offlineLabel.className.split(' ')).not.toContain('top-px')
|
||||
expect(offlineLabel.previousElementSibling?.className.split(' ')).toContain('bg-[#999]')
|
||||
expect(offlineLabel.previousElementSibling?.className.split(' ')).not.toContain('top-px')
|
||||
expect(offlineLabel.parentElement?.className.split(' ')).toContain('top-px')
|
||||
expect(offlineLabel.parentElement?.className.split(' ')).toContain('sm:top-0.5')
|
||||
|
||||
rerender(
|
||||
<I18nProvider>
|
||||
<StatusBar active thinking agentState={null} />
|
||||
</I18nProvider>
|
||||
)
|
||||
|
||||
const thinkingLabel = screen.getByText(/…$/)
|
||||
expect(thinkingLabel.className.split(' ')).not.toContain('top-px')
|
||||
expect(thinkingLabel.previousElementSibling?.className.split(' ')).not.toContain('top-px')
|
||||
expect(thinkingLabel.parentElement?.className.split(' ')).toContain('top-px')
|
||||
expect(thinkingLabel.parentElement?.className.split(' ')).toContain('sm:top-0.5')
|
||||
})
|
||||
|
||||
it('uses an effort-only reasoning label on mobile and the full label on desktop', () => {
|
||||
render(
|
||||
<I18nProvider>
|
||||
<StatusBar
|
||||
active
|
||||
thinking={false}
|
||||
agentState={null}
|
||||
agentFlavor="codex"
|
||||
modelReasoningEffort="xhigh"
|
||||
/>
|
||||
</I18nProvider>
|
||||
)
|
||||
|
||||
expect(screen.getByText('xhigh').className.split(' ')).toContain('sm:hidden')
|
||||
const desktopLabel = screen.getByText('reasoning xhigh')
|
||||
expect(desktopLabel.className.split(' ')).toContain('hidden')
|
||||
expect(desktopLabel.className.split(' ')).toContain('sm:inline')
|
||||
})
|
||||
|
||||
it('opens from the mobile-accessible context trigger and keeps the requested detail order', async () => {
|
||||
localStorage.setItem('hapi-lang', 'zh-CN')
|
||||
render(
|
||||
<I18nProvider>
|
||||
<StatusBar
|
||||
active
|
||||
thinking={false}
|
||||
agentState={null}
|
||||
contextSize={90_000}
|
||||
contextCacheRead={86_000}
|
||||
contextWindow={258_000}
|
||||
/>
|
||||
</I18nProvider>
|
||||
)
|
||||
|
||||
const connectionLabel = screen.getByText('online')
|
||||
const leftStatusGroup = connectionLabel.parentElement?.parentElement
|
||||
const statusBar = leftStatusGroup?.parentElement
|
||||
const rightStatusGroup = statusBar?.lastElementChild
|
||||
expect(statusBar?.className.split(' ')).toContain('items-baseline')
|
||||
expect(leftStatusGroup?.className.split(' ')).toContain('items-baseline')
|
||||
expect(rightStatusGroup?.className.split(' ')).toContain('items-baseline')
|
||||
expect(connectionLabel.className.split(' ')).not.toContain('top-px')
|
||||
expect(connectionLabel.previousElementSibling?.className.split(' ')).not.toContain('top-px')
|
||||
expect(connectionLabel.parentElement?.className.split(' ')).toContain('top-px')
|
||||
expect(connectionLabel.parentElement?.className.split(' ')).toContain('sm:top-0.5')
|
||||
expect(leftStatusGroup?.className.split(' ')).toContain('gap-2')
|
||||
expect(leftStatusGroup?.className.split(' ')).not.toContain('sm:gap-3')
|
||||
expect(rightStatusGroup?.className.split(' ')).toContain('gap-2')
|
||||
|
||||
const trigger = screen.getByRole('button', { name: '上下文详情' })
|
||||
expect(trigger.className.split(' ')).not.toContain('relative')
|
||||
expect(trigger.className.split(' ')).not.toContain('-top-px')
|
||||
expect(trigger.className.split(' ')).toContain('text-[10px]')
|
||||
expect(trigger.className.split(' ')).toContain('leading-4')
|
||||
expect(trigger.className.split(' ')).toContain('text-[var(--app-hint)]')
|
||||
expect(trigger.textContent).toBe('ctx 258k (65% left)35% · 90k / 258k')
|
||||
expect(trigger.className.split(' ')).not.toContain('hidden')
|
||||
const progressTrack = trigger.querySelector('[aria-hidden="true"]')
|
||||
expect((progressTrack?.firstElementChild as HTMLElement | null)?.style.width).toBe('35%')
|
||||
|
||||
fireEvent.click(trigger)
|
||||
|
||||
const cacheLine = await screen.findByText('缓存:86k')
|
||||
const details = cacheLine.parentElement
|
||||
expect(details?.textContent).toBe('缓存:86k使用:90k(35%)剩余:168k(65%)')
|
||||
expect(screen.queryByText('上下文详情')).toBeNull()
|
||||
})
|
||||
|
||||
it('localizes the popover content without localizing the external left label', async () => {
|
||||
localStorage.setItem('hapi-lang', 'en')
|
||||
render(
|
||||
<I18nProvider>
|
||||
<StatusBar
|
||||
active
|
||||
thinking={false}
|
||||
agentState={null}
|
||||
contextSize={90_000}
|
||||
contextCacheRead={86_000}
|
||||
contextWindow={258_000}
|
||||
/>
|
||||
</I18nProvider>
|
||||
)
|
||||
|
||||
const trigger = screen.getByRole('button', { name: 'Context details' })
|
||||
expect(trigger.textContent).toContain('ctx 258k (65% left)')
|
||||
|
||||
fireEvent.click(trigger)
|
||||
|
||||
const cacheLine = await screen.findByText('Cache: 86k')
|
||||
expect(cacheLine.parentElement?.textContent).toBe('Cache: 86kUsed: 90k (35%)Remaining: 168k (65%)')
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,39 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { shouldShowCodexFastBadge } from './StatusBar'
|
||||
import {
|
||||
formatCompactContextUsageLabel,
|
||||
formatContextUsageLabel,
|
||||
getContextUsageDetails,
|
||||
shouldShowCodexFastBadge
|
||||
} from './StatusBar'
|
||||
|
||||
describe('context usage labels', () => {
|
||||
it('keeps the desktop label compact and expresses used capacity', () => {
|
||||
expect(formatContextUsageLabel(90_000, 258_000)).toBe('35% · 90k / 258k')
|
||||
})
|
||||
|
||||
it('uses the compact parenthesized mobile label with a fixed English suffix', () => {
|
||||
expect(formatCompactContextUsageLabel(186_000, 262_000)).toBe('ctx 262k (29% left)')
|
||||
})
|
||||
|
||||
it('orders cache, used, and remaining metrics for the desktop details', () => {
|
||||
expect(getContextUsageDetails(90_000, 258_000, 86_000)).toEqual({
|
||||
cacheRead: '86k',
|
||||
used: '90k',
|
||||
usedPercentage: 35,
|
||||
remaining: '168k',
|
||||
remainingPercentage: 65
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps external and detailed percentages complementary at rounding midpoints', () => {
|
||||
expect(formatContextUsageLabel(69, 200)).toBe('35% · 69 / 200')
|
||||
expect(formatCompactContextUsageLabel(69, 200)).toBe('ctx 200 (65% left)')
|
||||
expect(getContextUsageDetails(69, 200, 0)).toMatchObject({
|
||||
usedPercentage: 35,
|
||||
remainingPercentage: 65
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('shouldShowCodexFastBadge', () => {
|
||||
it('uses only the effective service tier', () => {
|
||||
|
||||
@@ -5,12 +5,17 @@ import {
|
||||
isPermissionModeAllowedForFlavor
|
||||
} from '@hapi/protocol'
|
||||
import type { PermissionModeTone } from '@hapi/protocol'
|
||||
import * as Popover from '@radix-ui/react-popover'
|
||||
import { useMemo } from 'react'
|
||||
import type { AgentState, CodexCollaborationMode, PermissionMode } from '@/types/api'
|
||||
import type { ConversationStatus } from '@/realtime/types'
|
||||
import type { ThreadGoal } from '@/types/api'
|
||||
import { getContextBudgetTokens } from '@/chat/modelConfig'
|
||||
import { formatCodexReasoningLabel, shouldShowCodexReasoningLabel } from '@/lib/codexStatusLabels'
|
||||
import {
|
||||
formatCodexReasoningLabel,
|
||||
formatCompactCodexReasoningLabel,
|
||||
shouldShowCodexReasoningLabel
|
||||
} from '@/lib/codexStatusLabels'
|
||||
import { isFastServiceTier } from './codexFastMode'
|
||||
import { useTranslation } from '@/lib/use-translation'
|
||||
|
||||
@@ -105,17 +110,16 @@ function getConnectionStatus(
|
||||
}
|
||||
}
|
||||
|
||||
function getContextWarning(contextSize: number, maxContextSize: number, t: (key: string, params?: Record<string, string | number>) => string): { text: string; color: string } | null {
|
||||
function getContextWarning(contextSize: number, maxContextSize: number): { color: string } | null {
|
||||
const percentageUsed = (contextSize / maxContextSize) * 100
|
||||
const percentageRemaining = Math.max(0, 100 - percentageUsed)
|
||||
|
||||
const percent = Math.round(percentageRemaining)
|
||||
if (percentageRemaining <= 5) {
|
||||
return { text: t('misc.percentLeft', { percent }), color: 'text-red-500' }
|
||||
return { color: 'text-red-500' }
|
||||
} else if (percentageRemaining <= 10) {
|
||||
return { text: t('misc.percentLeft', { percent }), color: 'text-amber-500' }
|
||||
return { color: 'text-amber-500' }
|
||||
} else {
|
||||
return { text: t('misc.percentLeft', { percent }), color: 'text-[var(--app-hint)]' }
|
||||
return { color: 'text-[var(--app-hint)]' }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,6 +129,57 @@ function formatTokenCount(value: number): string {
|
||||
return String(value)
|
||||
}
|
||||
|
||||
function getContextPercentages(contextSize: number, maxContextSize: number): {
|
||||
usedPercentage: number
|
||||
remainingPercentage: number
|
||||
} {
|
||||
const usedPercentage = Math.min(100, Math.max(0, Math.round((contextSize / maxContextSize) * 100)))
|
||||
return { usedPercentage, remainingPercentage: 100 - usedPercentage }
|
||||
}
|
||||
|
||||
export function formatContextUsageLabel(contextSize: number, maxContextSize: number | null | undefined): string {
|
||||
if (!maxContextSize) return `${formatTokenCount(contextSize)} used`
|
||||
const { usedPercentage } = getContextPercentages(contextSize, maxContextSize)
|
||||
return `${usedPercentage}% · ${formatTokenCount(contextSize)} / ${formatTokenCount(maxContextSize)}`
|
||||
}
|
||||
|
||||
export function formatCompactContextUsageLabel(contextSize: number, maxContextSize: number | null | undefined): string {
|
||||
if (!maxContextSize) return `ctx ${formatTokenCount(contextSize)}`
|
||||
const { remainingPercentage } = getContextPercentages(contextSize, maxContextSize)
|
||||
return `ctx ${formatTokenCount(maxContextSize)} (${remainingPercentage}% left)`
|
||||
}
|
||||
|
||||
export function getContextUsageDetails(
|
||||
contextSize: number,
|
||||
maxContextSize: number | null | undefined,
|
||||
contextCacheRead: number | null | undefined
|
||||
): {
|
||||
cacheRead: string | null
|
||||
used: string
|
||||
usedPercentage: number | null
|
||||
remaining: string | null
|
||||
remainingPercentage: number | null
|
||||
} {
|
||||
if (!maxContextSize) {
|
||||
return {
|
||||
cacheRead: contextCacheRead && contextCacheRead > 0 ? formatTokenCount(contextCacheRead) : null,
|
||||
used: formatTokenCount(contextSize),
|
||||
usedPercentage: null,
|
||||
remaining: null,
|
||||
remainingPercentage: null
|
||||
}
|
||||
}
|
||||
|
||||
const { usedPercentage, remainingPercentage } = getContextPercentages(contextSize, maxContextSize)
|
||||
return {
|
||||
cacheRead: contextCacheRead && contextCacheRead > 0 ? formatTokenCount(contextCacheRead) : null,
|
||||
used: formatTokenCount(contextSize),
|
||||
usedPercentage,
|
||||
remaining: formatTokenCount(Math.max(0, maxContextSize - contextSize)),
|
||||
remainingPercentage
|
||||
}
|
||||
}
|
||||
|
||||
export function shouldShowCodexFastBadge(
|
||||
agentFlavor: string | null | undefined,
|
||||
serviceTier: string | null | undefined
|
||||
@@ -160,28 +215,26 @@ export function StatusBar(props: {
|
||||
if (props.contextSize === undefined) return null
|
||||
const maxContextSize = props.contextWindow ?? getContextBudgetTokens(props.model, props.agentFlavor)
|
||||
if (!maxContextSize) return null
|
||||
return getContextWarning(props.contextSize, maxContextSize, t)
|
||||
return getContextWarning(props.contextSize, maxContextSize)
|
||||
},
|
||||
[props.contextSize, props.contextWindow, props.model, props.agentFlavor, t]
|
||||
[props.contextSize, props.contextWindow, props.model, props.agentFlavor]
|
||||
)
|
||||
const contextUsageLabel = useMemo(() => {
|
||||
if (props.contextSize === undefined) return null
|
||||
const maxContextSize = props.contextWindow ?? getContextBudgetTokens(props.model, props.agentFlavor)
|
||||
if (!maxContextSize) return `ctx ${formatTokenCount(props.contextSize)}`
|
||||
const percentageUsed = Math.min(100, Math.round((props.contextSize / maxContextSize) * 100))
|
||||
return `ctx ${formatTokenCount(props.contextSize)}/${formatTokenCount(maxContextSize)} (${percentageUsed}%)`
|
||||
return formatContextUsageLabel(props.contextSize, maxContextSize)
|
||||
}, [props.contextSize, props.contextWindow, props.model, props.agentFlavor])
|
||||
const compactContextUsageLabel = useMemo(() => {
|
||||
if (props.contextSize === undefined) return null
|
||||
const maxContextSize = props.contextWindow ?? getContextBudgetTokens(props.model, props.agentFlavor)
|
||||
if (!maxContextSize) return `ctx ${formatTokenCount(props.contextSize)}`
|
||||
const percentageLeft = Math.max(0, Math.round(100 - (props.contextSize / maxContextSize) * 100))
|
||||
return `ctx ${formatTokenCount(maxContextSize).toUpperCase()}, ${percentageLeft}% left`
|
||||
return formatCompactContextUsageLabel(props.contextSize, maxContextSize)
|
||||
}, [props.contextSize, props.contextWindow, props.model, props.agentFlavor])
|
||||
const cacheHitLabel = useMemo(() => {
|
||||
if (!props.contextCacheRead || props.contextCacheRead <= 0) return null
|
||||
return `cache ${formatTokenCount(props.contextCacheRead)}`
|
||||
}, [props.contextCacheRead])
|
||||
const contextUsageDetails = useMemo(() => {
|
||||
if (props.contextSize === undefined) return null
|
||||
const maxContextSize = props.contextWindow ?? getContextBudgetTokens(props.model, props.agentFlavor)
|
||||
return getContextUsageDetails(props.contextSize, maxContextSize, props.contextCacheRead)
|
||||
}, [props.contextSize, props.contextCacheRead, props.contextWindow, props.model, props.agentFlavor])
|
||||
const contextUsedPercentage = contextUsageDetails?.usedPercentage ?? null
|
||||
|
||||
const permissionMode = props.permissionMode
|
||||
const displayPermissionMode = permissionMode
|
||||
@@ -199,9 +252,13 @@ export function StatusBar(props: {
|
||||
const collaborationModeLabel = displayCollaborationMode
|
||||
? getCodexCollaborationModeLabel(displayCollaborationMode)
|
||||
: null
|
||||
const codexReasoningLabel = shouldShowCodexReasoningLabel(props.agentFlavor)
|
||||
const displaysCodexReasoning = shouldShowCodexReasoningLabel(props.agentFlavor)
|
||||
const codexReasoningLabel = displaysCodexReasoning
|
||||
? formatCodexReasoningLabel(props.modelReasoningEffort)
|
||||
: null
|
||||
const compactCodexReasoningLabel = displaysCodexReasoning
|
||||
? formatCompactCodexReasoningLabel(props.modelReasoningEffort)
|
||||
: null
|
||||
const codexFastMode = shouldShowCodexFastBadge(props.agentFlavor, props.serviceTier)
|
||||
const goalLabel = props.agentFlavor === 'codex' && props.threadGoal
|
||||
? props.threadGoal.status === 'active'
|
||||
@@ -210,9 +267,9 @@ export function StatusBar(props: {
|
||||
: null
|
||||
|
||||
return (
|
||||
<div className="flex min-w-0 items-center justify-between gap-2 px-2 pb-1">
|
||||
<div className="flex min-w-0 items-baseline gap-2 sm:gap-3">
|
||||
<div className="flex shrink-0 items-center gap-1.5">
|
||||
<div className="flex min-w-0 items-baseline justify-between gap-2 px-2 pb-1">
|
||||
<div className="flex min-w-0 items-baseline gap-2">
|
||||
<div className="relative top-px sm:top-0.5 flex shrink-0 items-center gap-1.5">
|
||||
<span
|
||||
className={`h-2 w-2 rounded-full ${connectionStatus.dotColor} ${connectionStatus.isPulsing ? 'animate-pulse' : ''}`}
|
||||
/>
|
||||
@@ -221,26 +278,72 @@ export function StatusBar(props: {
|
||||
</span>
|
||||
</div>
|
||||
{contextUsageLabel ? (
|
||||
<span className={`min-w-0 whitespace-nowrap text-[10px] ${contextWarning?.color ?? 'text-[var(--app-hint)]'}`}>
|
||||
<span className="sm:hidden">
|
||||
{compactContextUsageLabel}
|
||||
</span>
|
||||
<span className="hidden sm:inline">
|
||||
{contextUsageLabel}{contextWarning ? ` · ${contextWarning.text}` : ''}
|
||||
</span>
|
||||
</span>
|
||||
) : null}
|
||||
{cacheHitLabel ? (
|
||||
<span className="hidden whitespace-nowrap text-[10px] text-[var(--app-hint)] sm:inline">
|
||||
{cacheHitLabel}
|
||||
</span>
|
||||
<Popover.Root>
|
||||
<Popover.Trigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t('misc.contextDetails')}
|
||||
className={`min-w-0 cursor-pointer whitespace-nowrap rounded-sm bg-transparent p-0 text-[10px] leading-4 outline-none focus-visible:ring-1 focus-visible:ring-[var(--app-link)] ${contextWarning?.color ?? 'text-[var(--app-hint)]'}`}
|
||||
>
|
||||
<span className="sm:hidden">{compactContextUsageLabel}</span>
|
||||
<span className="hidden items-center gap-2 sm:inline-flex">
|
||||
{contextUsedPercentage !== null ? (
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className="h-1 w-12 shrink-0 overflow-hidden rounded-full bg-[var(--app-link-muted)]"
|
||||
>
|
||||
<span
|
||||
className="block h-full rounded-full bg-current"
|
||||
style={{ width: `${contextUsedPercentage}%` }}
|
||||
/>
|
||||
</span>
|
||||
) : null}
|
||||
<span>{contextUsageLabel}</span>
|
||||
</span>
|
||||
</button>
|
||||
</Popover.Trigger>
|
||||
<Popover.Portal>
|
||||
<Popover.Content
|
||||
side="top"
|
||||
align="start"
|
||||
sideOffset={6}
|
||||
collisionPadding={8}
|
||||
className="z-50 rounded-md border border-[var(--app-border)] bg-[var(--app-bg)] px-3 py-2 shadow-lg"
|
||||
>
|
||||
<div className="flex max-w-[min(22rem,calc(100vw-1rem))] flex-col gap-1 text-xs leading-tight text-[var(--app-fg)]">
|
||||
{contextUsageDetails?.cacheRead ? (
|
||||
<span className="break-words">
|
||||
{t('misc.contextCache', { value: contextUsageDetails.cacheRead })}
|
||||
</span>
|
||||
) : null}
|
||||
<span className="break-words">
|
||||
{contextUsageDetails?.usedPercentage === null
|
||||
? t('misc.contextUsedTokens', { value: contextUsageDetails.used })
|
||||
: t('misc.contextUsed', {
|
||||
value: contextUsageDetails?.used ?? '',
|
||||
percent: contextUsageDetails?.usedPercentage ?? 0
|
||||
})}
|
||||
</span>
|
||||
{contextUsageDetails?.remaining && contextUsageDetails.remainingPercentage !== null ? (
|
||||
<span className="break-words">
|
||||
{t('misc.contextRemaining', {
|
||||
value: contextUsageDetails.remaining,
|
||||
percent: contextUsageDetails.remainingPercentage
|
||||
})}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</Popover.Content>
|
||||
</Popover.Portal>
|
||||
</Popover.Root>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="flex min-w-0 shrink-0 items-center gap-2">
|
||||
<div className="flex min-w-0 shrink-0 items-baseline gap-2">
|
||||
{codexReasoningLabel ? (
|
||||
<span className="whitespace-nowrap text-xs text-[var(--app-hint)]">
|
||||
{codexReasoningLabel}
|
||||
<span className="sm:hidden">{compactCodexReasoningLabel}</span>
|
||||
<span className="hidden sm:inline">{codexReasoningLabel}</span>
|
||||
</span>
|
||||
) : null}
|
||||
{codexFastMode ? (
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { formatCodexReasoningLabel, shouldShowCodexReasoningLabel } from './codexStatusLabels'
|
||||
import {
|
||||
formatCodexReasoningLabel,
|
||||
formatCompactCodexReasoningLabel,
|
||||
shouldShowCodexReasoningLabel
|
||||
} from './codexStatusLabels'
|
||||
|
||||
describe('codexStatusLabels', () => {
|
||||
it('formats unset and default effort as reasoning default', () => {
|
||||
@@ -14,6 +18,13 @@ describe('codexStatusLabels', () => {
|
||||
expect(formatCodexReasoningLabel('Ultra')).toBe('reasoning ultra')
|
||||
})
|
||||
|
||||
it('formats compact effort-only labels', () => {
|
||||
expect(formatCompactCodexReasoningLabel(null)).toBe('default')
|
||||
expect(formatCompactCodexReasoningLabel('default')).toBe('default')
|
||||
expect(formatCompactCodexReasoningLabel('xhigh')).toBe('xhigh')
|
||||
expect(formatCompactCodexReasoningLabel(' Ultra ')).toBe('ultra')
|
||||
})
|
||||
|
||||
it('only shows the label for codex and opencode', () => {
|
||||
expect(shouldShowCodexReasoningLabel('codex')).toBe(true)
|
||||
expect(shouldShowCodexReasoningLabel('opencode')).toBe(true)
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
/** Labels shared by SessionHeader and composer StatusBar for Codex/OpenCode. */
|
||||
|
||||
export function formatCodexReasoningLabel(effort?: string | null): string {
|
||||
export function formatCompactCodexReasoningLabel(effort?: string | null): string {
|
||||
const normalized = effort?.trim().toLowerCase()
|
||||
if (!normalized || normalized === 'default') return 'reasoning default'
|
||||
return `reasoning ${normalized}`
|
||||
if (!normalized || normalized === 'default') return 'default'
|
||||
return normalized
|
||||
}
|
||||
|
||||
export function formatCodexReasoningLabel(effort?: string | null): string {
|
||||
return `reasoning ${formatCompactCodexReasoningLabel(effort)}`
|
||||
}
|
||||
|
||||
export function shouldShowCodexReasoningLabel(agentFlavor: string | null | undefined): boolean {
|
||||
|
||||
@@ -879,8 +879,12 @@ export default {
|
||||
'misc.typeAMessage': 'Type a message...',
|
||||
'misc.offline': 'offline',
|
||||
'misc.permissionRequired': 'permission required',
|
||||
'misc.percentLeft': '{percent}% left',
|
||||
'misc.online': 'online',
|
||||
'misc.contextDetails': 'Context details',
|
||||
'misc.contextCache': 'Cache: {value}',
|
||||
'misc.contextUsed': 'Used: {value} ({percent}%)',
|
||||
'misc.contextUsedTokens': 'Used: {value}',
|
||||
'misc.contextRemaining': 'Remaining: {value} ({percent}%)',
|
||||
|
||||
// Web Share Target picker
|
||||
'share.title': 'Share to HAPI',
|
||||
|
||||
@@ -881,10 +881,14 @@ export default {
|
||||
'misc.releaseToLoadOlder': '松开加载',
|
||||
'misc.typeMessage': "输入 'continue' 继续...",
|
||||
'misc.typeAMessage': '输入消息...',
|
||||
'misc.offline': '离线',
|
||||
'misc.offline': 'offline',
|
||||
'misc.permissionRequired': '需要权限',
|
||||
'misc.percentLeft': '剩余 {percent}%',
|
||||
'misc.online': '在线',
|
||||
'misc.online': 'online',
|
||||
'misc.contextDetails': '上下文详情',
|
||||
'misc.contextCache': '缓存:{value}',
|
||||
'misc.contextUsed': '使用:{value}({percent}%)',
|
||||
'misc.contextUsedTokens': '使用:{value}',
|
||||
'misc.contextRemaining': '剩余:{value}({percent}%)',
|
||||
|
||||
// Web Share Target 分享面板
|
||||
'share.title': '分享到 HAPI',
|
||||
|
||||
Reference in New Issue
Block a user