remove , using instead

This commit is contained in:
weishu
2026-03-16 18:29:09 +08:00
parent 02c8e12e80
commit 329d28a93c
40 changed files with 384 additions and 285 deletions
+1 -2
View File
@@ -9,7 +9,6 @@ import type {
MachinePathsExistsResponse,
MachinesResponse,
MessagesResponse,
ModelMode,
PermissionMode,
PushSubscriptionPayload,
PushUnsubscribePayload,
@@ -313,7 +312,7 @@ export class ApiClient {
})
}
async setModelMode(sessionId: string, model: ModelMode): Promise<void> {
async setModel(sessionId: string, model: string | null): Promise<void> {
await this.request(`/api/sessions/${encodeURIComponent(sessionId)}/model`, {
method: 'POST',
body: JSON.stringify({ model })
+8 -8
View File
@@ -2,15 +2,15 @@ import { describe, expect, it } from 'vitest'
import { getContextBudgetTokens } from './modelConfig'
describe('getContextBudgetTokens', () => {
it('uses the existing 200k budget for default Claude modes', () => {
expect(getContextBudgetTokens(undefined)).toBe(190_000)
expect(getContextBudgetTokens('default')).toBe(190_000)
expect(getContextBudgetTokens('sonnet')).toBe(190_000)
expect(getContextBudgetTokens('opus')).toBe(190_000)
it('uses the large budget only for explicit 1m Claude presets', () => {
expect(getContextBudgetTokens('sonnet[1m]', 'claude')).toBe(990_000)
})
it('uses the 1m budget for Claude 1m modes', () => {
expect(getContextBudgetTokens('sonnet[1m]')).toBe(990_000)
expect(getContextBudgetTokens('opus[1m]')).toBe(990_000)
it('uses the default Claude budget for full Claude model names', () => {
expect(getContextBudgetTokens('claude-sonnet-4-6', 'claude')).toBe(190_000)
})
it('returns null for non-Claude sessions', () => {
expect(getContextBudgetTokens('gpt-5.4', 'codex')).toBeNull()
})
})
+23 -12
View File
@@ -1,4 +1,4 @@
import type { ModelMode } from '@/types/api'
import { isClaudeModelPreset } from '@hapi/protocol'
/**
* Context windows vary by model/provider and may change over time.
@@ -11,19 +11,30 @@ import type { ModelMode } from '@/types/api'
* and use this only as a fallback.
*/
const CONTEXT_HEADROOM_TOKENS = 10_000
const DEFAULT_CLAUDE_CONTEXT_WINDOW_TOKENS = 200_000
const LARGE_CLAUDE_CONTEXT_WINDOW_TOKENS = 1_000_000
const MODEL_CONTEXT_WINDOWS: Record<ModelMode, number> = {
// Claude Code modes used in this app. 1M variants get the larger budget.
default: 200_000,
sonnet: 200_000,
'sonnet[1m]': 1_000_000,
opus: 200_000,
'opus[1m]': 1_000_000
}
export function getContextBudgetTokens(model: string | null | undefined, flavor?: string | null): number | null {
if (flavor !== 'claude') {
return null
}
const trimmedModel = model?.trim()
const windowTokens = (() => {
if (!trimmedModel) {
return DEFAULT_CLAUDE_CONTEXT_WINDOW_TOKENS
}
if (isClaudeModelPreset(trimmedModel)) {
return trimmedModel.endsWith('[1m]')
? LARGE_CLAUDE_CONTEXT_WINDOW_TOKENS
: DEFAULT_CLAUDE_CONTEXT_WINDOW_TOKENS
}
if (trimmedModel.startsWith('claude-')) {
return DEFAULT_CLAUDE_CONTEXT_WINDOW_TOKENS
}
return null
})()
export function getContextBudgetTokens(modelMode: ModelMode | undefined): number | null {
const mode: ModelMode = modelMode ?? 'default'
const windowTokens = MODEL_CONTEXT_WINDOWS[mode]
if (!windowTokens) return null
return Math.max(1, windowTokens - CONTEXT_HEADROOM_TOKENS)
}
@@ -1,4 +1,4 @@
import { getPermissionModeOptionsForFlavor, MODEL_MODE_LABELS, MODEL_MODES } from '@hapi/protocol'
import { getPermissionModeOptionsForFlavor } from '@hapi/protocol'
import { ComposerPrimitive, useAssistantApi, useAssistantState } from '@assistant-ui/react'
import {
type ChangeEvent as ReactChangeEvent,
@@ -12,7 +12,7 @@ import {
useRef,
useState
} from 'react'
import type { AgentState, ModelMode, PermissionMode } from '@/types/api'
import type { AgentState, PermissionMode } from '@/types/api'
import type { Suggestion } from '@/hooks/useActiveSuggestions'
import type { ConversationStatus } from '@/realtime/types'
import { useActiveWord } from '@/hooks/useActiveWord'
@@ -28,6 +28,7 @@ import { StatusBar } from '@/components/AssistantChat/StatusBar'
import { ComposerButtons } from '@/components/AssistantChat/ComposerButtons'
import { AttachmentItem } from '@/components/AssistantChat/AttachmentItem'
import { useTranslation } from '@/lib/use-translation'
import { getClaudeComposerModelOptions, getNextClaudeComposerModel } from './claudeModelOptions'
export interface TextInputState {
text: string
@@ -39,7 +40,7 @@ const defaultSuggestionHandler = async (): Promise<Suggestion[]> => []
export function HappyComposer(props: {
disabled?: boolean
permissionMode?: PermissionMode
modelMode?: ModelMode
model?: string | null
active?: boolean
allowSendWhenInactive?: boolean
thinking?: boolean
@@ -48,7 +49,7 @@ export function HappyComposer(props: {
controlledByUser?: boolean
agentFlavor?: string | null
onPermissionModeChange?: (mode: PermissionMode) => void
onModelModeChange?: (mode: ModelMode) => void
onModelChange?: (model: string | null) => void
onSwitchToRemote?: () => void
onTerminal?: () => void
autocompletePrefixes?: string[]
@@ -63,7 +64,7 @@ export function HappyComposer(props: {
const {
disabled = false,
permissionMode: rawPermissionMode,
modelMode: rawModelMode,
model: rawModel,
active = true,
allowSendWhenInactive = false,
thinking = false,
@@ -72,7 +73,7 @@ export function HappyComposer(props: {
controlledByUser = false,
agentFlavor,
onPermissionModeChange,
onModelModeChange,
onModelChange,
onSwitchToRemote,
onTerminal,
autocompletePrefixes = ['@', '/', '$'],
@@ -85,7 +86,7 @@ export function HappyComposer(props: {
// Use ?? so missing values fall back to default (destructuring defaults only handle undefined)
const permissionMode = rawPermissionMode ?? 'default'
const modelMode = rawModelMode ?? 'default'
const model = rawModel ?? null
const api = useAssistantApi()
const composerText = useAssistantState(({ composer }) => composer.text)
@@ -245,6 +246,10 @@ export function HappyComposer(props: {
() => getPermissionModeOptionsForFlavor(agentFlavor),
[agentFlavor]
)
const claudeModelOptions = useMemo(
() => getClaudeComposerModelOptions(model),
[model]
)
const permissionModes = useMemo(
() => permissionModeOptions.map((option) => option.mode),
[permissionModeOptions]
@@ -324,18 +329,16 @@ export function HappyComposer(props: {
useEffect(() => {
const handleGlobalKeyDown = (e: globalThis.KeyboardEvent) => {
if (e.key === 'm' && (e.metaKey || e.ctrlKey) && onModelModeChange && isClaudeFlavor(agentFlavor)) {
if (e.key === 'm' && (e.metaKey || e.ctrlKey) && onModelChange && isClaudeFlavor(agentFlavor)) {
e.preventDefault()
const currentIndex = MODEL_MODES.indexOf(modelMode as typeof MODEL_MODES[number])
const nextIndex = (currentIndex + 1) % MODEL_MODES.length
onModelModeChange(MODEL_MODES[nextIndex])
onModelChange(getNextClaudeComposerModel(model))
haptic('light')
}
}
window.addEventListener('keydown', handleGlobalKeyDown)
return () => window.removeEventListener('keydown', handleGlobalKeyDown)
}, [modelMode, onModelModeChange, haptic, agentFlavor])
}, [model, onModelChange, haptic, agentFlavor])
const handleChange = useCallback((e: ReactChangeEvent<HTMLTextAreaElement>) => {
const selection = {
@@ -390,15 +393,15 @@ export function HappyComposer(props: {
haptic('light')
}, [onPermissionModeChange, controlsDisabled, haptic])
const handleModelChange = useCallback((mode: ModelMode) => {
if (!onModelModeChange || controlsDisabled) return
onModelModeChange(mode)
const handleModelChange = useCallback((nextModel: string | null) => {
if (!onModelChange || controlsDisabled) return
onModelChange(nextModel)
setShowSettings(false)
haptic('light')
}, [onModelModeChange, controlsDisabled, haptic])
}, [onModelChange, controlsDisabled, haptic])
const showPermissionSettings = Boolean(onPermissionModeChange && permissionModeOptions.length > 0)
const showModelSettings = Boolean(onModelModeChange && isClaudeFlavor(agentFlavor))
const showModelSettings = Boolean(onModelChange && isClaudeFlavor(agentFlavor))
const showSettingsButton = Boolean(showPermissionSettings || showModelSettings)
const showAbortButton = true
const voiceEnabled = Boolean(onVoiceToggle)
@@ -458,9 +461,9 @@ export function HappyComposer(props: {
<div className="px-3 pb-1 text-xs font-semibold text-[var(--app-hint)]">
{t('misc.model')}
</div>
{MODEL_MODES.map((mode) => (
{claudeModelOptions.map((option) => (
<button
key={mode}
key={option.value ?? 'auto'}
type="button"
disabled={controlsDisabled}
className={`flex w-full items-center gap-2 px-3 py-2 text-left text-sm transition-colors ${
@@ -468,22 +471,22 @@ export function HappyComposer(props: {
? 'cursor-not-allowed opacity-50'
: 'cursor-pointer hover:bg-[var(--app-secondary-bg)]'
}`}
onClick={() => handleModelChange(mode)}
onClick={() => handleModelChange(option.value)}
onMouseDown={(e) => e.preventDefault()}
>
<div
className={`flex h-4 w-4 items-center justify-center rounded-full border-2 ${
modelMode === mode
model === option.value
? 'border-[var(--app-link)]'
: 'border-[var(--app-hint)]'
}`}
>
{modelMode === mode && (
{model === option.value && (
<div className="h-2 w-2 rounded-full bg-[var(--app-link)]" />
)}
</div>
<span className={modelMode === mode ? 'text-[var(--app-link)]' : ''}>
{MODEL_MODE_LABELS[mode]}
<span className={model === option.value ? 'text-[var(--app-link)]' : ''}>
{option.label}
</span>
</button>
))}
@@ -513,15 +516,17 @@ export function HappyComposer(props: {
showSettings,
showPermissionSettings,
showModelSettings,
claudeModelOptions,
suggestions,
selectedIndex,
controlsDisabled,
permissionMode,
modelMode,
model,
permissionModeOptions,
handlePermissionChange,
handleModelChange,
handleSuggestionSelect
handleSuggestionSelect,
t
])
return (
@@ -535,7 +540,7 @@ export function HappyComposer(props: {
thinking={thinking}
agentState={agentState}
contextSize={contextSize}
modelMode={modelMode}
model={model}
permissionMode={permissionMode}
agentFlavor={agentFlavor}
voiceStatus={voiceStatus}
@@ -1,7 +1,7 @@
import { getPermissionModeLabel, getPermissionModeTone, isPermissionModeAllowedForFlavor } from '@hapi/protocol'
import type { PermissionModeTone } from '@hapi/protocol'
import { useMemo } from 'react'
import type { AgentState, ModelMode, PermissionMode } from '@/types/api'
import type { AgentState, PermissionMode } from '@/types/api'
import type { ConversationStatus } from '@/realtime/types'
import { getContextBudgetTokens } from '@/chat/modelConfig'
import { useTranslation } from '@/lib/use-translation'
@@ -106,7 +106,7 @@ export function StatusBar(props: {
thinking: boolean
agentState: AgentState | null | undefined
contextSize?: number
modelMode?: ModelMode
model?: string | null
permissionMode?: PermissionMode
agentFlavor?: string | null
voiceStatus?: ConversationStatus
@@ -120,11 +120,11 @@ export function StatusBar(props: {
const contextWarning = useMemo(
() => {
if (props.contextSize === undefined) return null
const maxContextSize = getContextBudgetTokens(props.modelMode)
const maxContextSize = getContextBudgetTokens(props.model, props.agentFlavor)
if (!maxContextSize) return null
return getContextWarning(props.contextSize, maxContextSize, t)
},
[props.contextSize, props.modelMode, t]
[props.contextSize, props.model, props.agentFlavor, t]
)
const permissionMode = props.permissionMode
@@ -0,0 +1,31 @@
import { describe, expect, it } from 'vitest'
import { getClaudeComposerModelOptions, getNextClaudeComposerModel } from './claudeModelOptions'
describe('getClaudeComposerModelOptions', () => {
it('includes the active non-preset Claude model in the options list', () => {
expect(getClaudeComposerModelOptions('claude-opus-4-1-20250805')).toEqual([
{ value: null, label: 'Auto' },
{ value: 'claude-opus-4-1-20250805', label: 'claude-opus-4-1-20250805' },
{ value: 'sonnet', label: 'Sonnet' },
{ value: 'sonnet[1m]', label: 'Sonnet 1M' },
{ value: 'opus', label: 'Opus' },
{ value: 'opus[1m]', label: 'Opus 1M' },
])
})
it('does not duplicate preset Claude models', () => {
expect(getClaudeComposerModelOptions('opus')).toEqual([
{ value: null, label: 'Auto' },
{ value: 'sonnet', label: 'Sonnet' },
{ value: 'sonnet[1m]', label: 'Sonnet 1M' },
{ value: 'opus', label: 'Opus' },
{ value: 'opus[1m]', label: 'Opus 1M' },
])
})
})
describe('getNextClaudeComposerModel', () => {
it('cycles from a non-preset Claude model to the next selectable model instead of auto', () => {
expect(getNextClaudeComposerModel('claude-opus-4-1-20250805')).toBe('sonnet')
})
})
@@ -0,0 +1,51 @@
import { CLAUDE_MODEL_PRESETS, getClaudeModelLabel } from '@hapi/protocol'
export type ClaudeComposerModelOption = {
value: string | null
label: string
}
function normalizeClaudeComposerModel(model?: string | null): string | null {
const trimmedModel = model?.trim()
if (!trimmedModel || trimmedModel === 'auto' || trimmedModel === 'default') {
return null
}
return trimmedModel
}
export function getClaudeComposerModelOptions(currentModel?: string | null): ClaudeComposerModelOption[] {
const normalizedCurrentModel = normalizeClaudeComposerModel(currentModel)
const options: ClaudeComposerModelOption[] = [
{ value: null, label: 'Auto' }
]
if (
normalizedCurrentModel
&& !CLAUDE_MODEL_PRESETS.includes(normalizedCurrentModel as typeof CLAUDE_MODEL_PRESETS[number])
) {
options.push({
value: normalizedCurrentModel,
label: getClaudeModelLabel(normalizedCurrentModel) ?? normalizedCurrentModel
})
}
options.push(...CLAUDE_MODEL_PRESETS.map((model) => ({
value: model,
label: getClaudeModelLabel(model) ?? model
})))
return options
}
export function getNextClaudeComposerModel(currentModel?: string | null): string | null {
const normalizedCurrentModel = normalizeClaudeComposerModel(currentModel)
const options = getClaudeComposerModelOptions(normalizedCurrentModel)
const currentIndex = options.findIndex((option) => option.value === normalizedCurrentModel)
if (currentIndex === -1) {
return options[0]?.value ?? null
}
return options[(currentIndex + 1) % options.length]?.value ?? null
}
+5 -5
View File
@@ -1,4 +1,4 @@
import { getModelModeLabel, MODEL_MODES } from '@hapi/protocol'
import { CLAUDE_MODEL_PRESETS, getClaudeModelLabel } from '@hapi/protocol'
import { describe, expect, it } from 'vitest'
import { MODEL_OPTIONS } from './types'
@@ -13,9 +13,9 @@ describe('Claude model options', () => {
])
})
it('exposes friendly labels for session model modes', () => {
expect(MODEL_MODES).toEqual(['default', 'sonnet', 'sonnet[1m]', 'opus', 'opus[1m]'])
expect(getModelModeLabel('sonnet[1m]')).toBe('Sonnet 1M')
expect(getModelModeLabel('opus[1m]')).toBe('Opus 1M')
it('exposes friendly labels for Claude model presets', () => {
expect(CLAUDE_MODEL_PRESETS).toEqual(['sonnet', 'sonnet[1m]', 'opus', 'opus[1m]'])
expect(getClaudeModelLabel('sonnet[1m]')).toBe('Sonnet 1M')
expect(getClaudeModelLabel('opus[1m]')).toBe('Opus 1M')
})
})
+8 -8
View File
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useNavigate } from '@tanstack/react-router'
import { AssistantRuntimeProvider } from '@assistant-ui/react'
import type { ApiClient } from '@/api/client'
import type { AttachmentMetadata, DecryptedMessage, ModelMode, PermissionMode, Session } from '@/types/api'
import type { AttachmentMetadata, DecryptedMessage, PermissionMode, Session } from '@/types/api'
import type { ChatBlock, NormalizedMessage } from '@/chat/types'
import type { Suggestion } from '@/hooks/useActiveSuggestions'
import { normalizeDecryptedMessage } from '@/chat/normalize'
@@ -46,7 +46,7 @@ export function SessionChat(props: {
const blocksByIdRef = useRef<Map<string, ChatBlock>>(new Map())
const [forceScrollToken, setForceScrollToken] = useState(0)
const agentFlavor = props.session.metadata?.flavor ?? null
const { abortSession, switchSession, setPermissionMode, setModelMode } = useSessionActions(
const { abortSession, switchSession, setPermissionMode, setModel } = useSessionActions(
props.api,
props.session.id,
agentFlavor
@@ -206,16 +206,16 @@ export function SessionChat(props: {
}, [setPermissionMode, props.onRefresh, haptic])
// Model mode change handler
const handleModelModeChange = useCallback(async (mode: ModelMode) => {
const handleModelChange = useCallback(async (model: string | null) => {
try {
await setModelMode(mode)
await setModel(model)
haptic.notification('success')
props.onRefresh()
} catch (e) {
haptic.notification('error')
console.error('Failed to set model mode:', e)
console.error('Failed to set model:', e)
}
}, [setModelMode, props.onRefresh, haptic])
}, [setModel, props.onRefresh, haptic])
// Abort handler
const handleAbort = useCallback(async () => {
@@ -314,7 +314,7 @@ export function SessionChat(props: {
<HappyComposer
disabled={props.isSending}
permissionMode={props.session.permissionMode}
modelMode={props.session.modelMode}
model={props.session.model}
agentFlavor={agentFlavor}
active={props.session.active}
allowSendWhenInactive
@@ -323,7 +323,7 @@ export function SessionChat(props: {
contextSize={reduced.latestUsage?.contextSize}
controlledByUser={props.session.agentState?.controlledByUser === true}
onPermissionModeChange={handlePermissionModeChange}
onModelModeChange={handleModelModeChange}
onModelChange={handleModelChange}
onSwitchToRemote={handleSwitchToRemote}
onTerminal={props.session.active ? handleViewTerminal : undefined}
autocompleteSuggestions={props.autocompleteSuggestions}
+5 -5
View File
@@ -1,7 +1,7 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { isPermissionModeAllowedForFlavor } from '@hapi/protocol'
import type { ApiClient } from '@/api/client'
import type { ModelMode, PermissionMode } from '@/types/api'
import type { PermissionMode } from '@/types/api'
import { queryKeys } from '@/lib/query-keys'
import { clearMessageWindow } from '@/lib/message-window-store'
import { isKnownFlavor } from '@/lib/agentFlavorUtils'
@@ -15,7 +15,7 @@ export function useSessionActions(
archiveSession: () => Promise<void>
switchSession: () => Promise<void>
setPermissionMode: (mode: PermissionMode) => Promise<void>
setModelMode: (mode: ModelMode) => Promise<void>
setModel: (model: string | null) => Promise<void>
renameSession: (name: string) => Promise<void>
deleteSession: () => Promise<void>
isPending: boolean
@@ -72,11 +72,11 @@ export function useSessionActions(
})
const modelMutation = useMutation({
mutationFn: async (mode: ModelMode) => {
mutationFn: async (model: string | null) => {
if (!api || !sessionId) {
throw new Error('Session unavailable')
}
await api.setModelMode(sessionId, mode)
await api.setModel(sessionId, model)
},
onSuccess: () => void invalidateSession(),
})
@@ -111,7 +111,7 @@ export function useSessionActions(
archiveSession: archiveMutation.mutateAsync,
switchSession: switchMutation.mutateAsync,
setPermissionMode: permissionMutation.mutateAsync,
setModelMode: modelMutation.mutateAsync,
setModel: modelMutation.mutateAsync,
renameSession: renameMutation.mutateAsync,
deleteSession: deleteMutation.mutateAsync,
isPending: abortMutation.isPending
+4 -9
View File
@@ -30,7 +30,7 @@ const RECONNECT_MAX_DELAY_MS = 30_000
const RECONNECT_JITTER_MS = 500
const INVALIDATION_BATCH_MS = 16
type SessionPatch = Partial<Pick<Session, 'active' | 'thinking' | 'activeAt' | 'updatedAt' | 'model' | 'permissionMode' | 'modelMode'>>
type SessionPatch = Partial<Pick<Session, 'active' | 'thinking' | 'activeAt' | 'updatedAt' | 'model' | 'permissionMode'>>
function sortSessionSummaries(left: SessionSummary, right: SessionSummary): number {
if (left.active !== right.active) {
@@ -81,7 +81,7 @@ function getSessionPatch(value: unknown): SessionPatch | null {
patch.updatedAt = value.updatedAt
hasKnownPatch = true
}
if (typeof value.model === 'string') {
if (value.model === null || typeof value.model === 'string') {
patch.model = value.model
hasKnownPatch = true
}
@@ -89,10 +89,6 @@ function getSessionPatch(value: unknown): SessionPatch | null {
patch.permissionMode = value.permissionMode as Session['permissionMode']
hasKnownPatch = true
}
if (typeof value.modelMode === 'string') {
patch.modelMode = value.modelMode as Session['modelMode']
hasKnownPatch = true
}
return hasKnownPatch ? patch : null
}
@@ -101,7 +97,7 @@ function hasUnknownSessionPatchKeys(value: unknown): boolean {
if (!hasRecordShape(value)) {
return false
}
const knownKeys = new Set(['active', 'thinking', 'activeAt', 'updatedAt', 'model', 'permissionMode', 'modelMode'])
const knownKeys = new Set(['active', 'thinking', 'activeAt', 'updatedAt', 'model', 'permissionMode'])
return Object.keys(value).some((key) => !knownKeys.has(key))
}
@@ -386,8 +382,7 @@ export function useSSE(options: {
thinking: patch.thinking ?? current.thinking,
activeAt: patch.activeAt ?? current.activeAt,
updatedAt: patch.updatedAt ?? current.updatedAt,
model: patch.model ?? current.model,
modelMode: patch.modelMode ?? current.modelMode
model: Object.prototype.hasOwnProperty.call(patch, 'model') ? patch.model ?? null : current.model
}
patched = true
-1
View File
@@ -46,7 +46,6 @@ export default {
'session.item.path': 'path',
'session.item.agent': 'agent',
'session.item.model': 'model',
'session.item.modelMode': 'mode',
'session.item.worktree': 'worktree',
'session.item.pending': 'pending',
'session.item.thinking': 'thinking',
-1
View File
@@ -46,7 +46,6 @@ export default {
'session.item.path': '路径',
'session.item.agent': '代理',
'session.item.model': '模型',
'session.item.modelMode': '模式',
'session.item.worktree': '工作树',
'session.item.pending': '待处理',
'session.item.thinking': '思考中',
+5 -5
View File
@@ -3,20 +3,20 @@ import { getSessionModelLabel } from './sessionModelLabel'
describe('getSessionModelLabel', () => {
it('prefers the explicit session model', () => {
expect(getSessionModelLabel({ model: 'gpt-5.4', modelMode: 'default' })).toEqual({
expect(getSessionModelLabel({ model: 'gpt-5.4' })).toEqual({
key: 'session.item.model',
value: 'gpt-5.4'
})
})
it('falls back to Claude model mode when no explicit model exists', () => {
expect(getSessionModelLabel({ modelMode: 'opus' })).toEqual({
key: 'session.item.modelMode',
it('renders friendly labels for known Claude aliases', () => {
expect(getSessionModelLabel({ model: 'opus' })).toEqual({
key: 'session.item.model',
value: 'Opus'
})
})
it('returns null when neither model nor mode is available', () => {
it('returns null when no model is available', () => {
expect(getSessionModelLabel({})).toBeNull()
})
})
+6 -12
View File
@@ -1,10 +1,11 @@
import { getModelModeLabel } from '@hapi/protocol'
import type { Session, SessionSummary } from '@/types/api'
import { getClaudeModelLabel } from '@hapi/protocol'
type SessionModelSource = Pick<Session, 'model' | 'modelMode'> | Pick<SessionSummary, 'model' | 'modelMode'>
type SessionModelSource = {
model?: string | null
}
export type SessionModelLabel = {
key: 'session.item.model' | 'session.item.modelMode'
key: 'session.item.model'
value: string
}
@@ -13,14 +14,7 @@ export function getSessionModelLabel(session: SessionModelSource): SessionModelL
if (explicitModel) {
return {
key: 'session.item.model',
value: explicitModel
}
}
if (session.modelMode) {
return {
key: 'session.item.modelMode',
value: getModelModeLabel(session.modelMode)
value: getClaudeModelLabel(explicitModel) ?? explicitModel
}
}
-1
View File
@@ -9,7 +9,6 @@ import type {
export type {
AgentState,
AttachmentMetadata,
ModelMode,
PermissionMode,
Session,
SessionSummary,