unify model selection

This commit is contained in:
weishu
2026-03-16 18:29:09 +08:00
parent 1f02bb7de1
commit 02c8e12e80
27 changed files with 454 additions and 41 deletions
+7 -5
View File
@@ -1,4 +1,3 @@
import { getModelModeLabel } from '@hapi/protocol'
import { useId, useMemo, useRef, useState } from 'react'
import type { Session } from '@/types/api'
import type { ApiClient } from '@/api/client'
@@ -7,6 +6,7 @@ import { useSessionActions } from '@/hooks/mutations/useSessionActions'
import { SessionActionMenu } from '@/components/SessionActionMenu'
import { RenameSessionDialog } from '@/components/RenameSessionDialog'
import { ConfirmDialog } from '@/components/ui/ConfirmDialog'
import { getSessionModelLabel } from '@/lib/sessionModelLabel'
import { useTranslation } from '@/lib/use-translation'
function getSessionTitle(session: Session): string {
@@ -71,7 +71,7 @@ export function SessionHeader(props: {
const { session, api, onSessionDeleted } = props
const title = useMemo(() => getSessionTitle(session), [session])
const worktreeBranch = session.metadata?.worktree?.branch
const modelModeLabel = getModelModeLabel(session.modelMode ?? 'default')
const modelLabel = getSessionModelLabel(session)
const [menuOpen, setMenuOpen] = useState(false)
const [menuAnchorPoint, setMenuAnchorPoint] = useState<{ x: number; y: number }>({ x: 0, y: 0 })
@@ -140,9 +140,11 @@ export function SessionHeader(props: {
<span aria-hidden="true"></span>
{session.metadata?.flavor?.trim() || 'unknown'}
</span>
<span>
{t('session.item.modelMode')}: {modelModeLabel}
</span>
{modelLabel ? (
<span>
{t(modelLabel.key)}: {modelLabel.value}
</span>
) : null}
{worktreeBranch ? (
<span>{t('session.item.worktree')}: {worktreeBranch}</span>
) : null}
+5 -3
View File
@@ -1,4 +1,3 @@
import { getModelModeLabel } from '@hapi/protocol'
import { useEffect, useMemo, useState } from 'react'
import type { SessionSummary } from '@/types/api'
import type { ApiClient } from '@/api/client'
@@ -8,6 +7,7 @@ import { useSessionActions } from '@/hooks/mutations/useSessionActions'
import { SessionActionMenu } from '@/components/SessionActionMenu'
import { RenameSessionDialog } from '@/components/RenameSessionDialog'
import { ConfirmDialog } from '@/components/ui/ConfirmDialog'
import { getSessionModelLabel } from '@/lib/sessionModelLabel'
import { useTranslation } from '@/lib/use-translation'
type SessionGroup = {
@@ -199,7 +199,7 @@ function SessionItem(props: {
})
const sessionName = getSessionTitle(s)
const modelModeLabel = getModelModeLabel(s.modelMode ?? 'default')
const modelLabel = getSessionModelLabel(s)
const statusDotClass = s.active
? (s.thinking ? 'bg-[#007AFF]' : 'bg-[var(--app-badge-success-text)]')
: 'bg-[var(--app-hint)]'
@@ -261,7 +261,9 @@ function SessionItem(props: {
</span>
{getAgentLabel(s)}
</span>
<span>{t('session.item.modelMode')}: {modelModeLabel}</span>
{modelLabel ? (
<span>{t(modelLabel.key)}: {modelLabel.value}</span>
) : null}
{s.metadata?.worktree?.branch ? (
<span>{t('session.item.worktree')}: {s.metadata.worktree.branch}</span>
) : null}
+7 -2
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' | 'permissionMode' | 'modelMode'>>
type SessionPatch = Partial<Pick<Session, 'active' | 'thinking' | 'activeAt' | 'updatedAt' | 'model' | 'permissionMode' | 'modelMode'>>
function sortSessionSummaries(left: SessionSummary, right: SessionSummary): number {
if (left.active !== right.active) {
@@ -81,6 +81,10 @@ function getSessionPatch(value: unknown): SessionPatch | null {
patch.updatedAt = value.updatedAt
hasKnownPatch = true
}
if (typeof value.model === 'string') {
patch.model = value.model
hasKnownPatch = true
}
if (typeof value.permissionMode === 'string') {
patch.permissionMode = value.permissionMode as Session['permissionMode']
hasKnownPatch = true
@@ -97,7 +101,7 @@ function hasUnknownSessionPatchKeys(value: unknown): boolean {
if (!hasRecordShape(value)) {
return false
}
const knownKeys = new Set(['active', 'thinking', 'activeAt', 'updatedAt', 'permissionMode', 'modelMode'])
const knownKeys = new Set(['active', 'thinking', 'activeAt', 'updatedAt', 'model', 'permissionMode', 'modelMode'])
return Object.keys(value).some((key) => !knownKeys.has(key))
}
@@ -382,6 +386,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
}
+1 -1
View File
@@ -46,7 +46,7 @@ export default {
'session.item.path': '路径',
'session.item.agent': '代理',
'session.item.model': '模型',
'session.item.modelMode': '模',
'session.item.modelMode': '模',
'session.item.worktree': '工作树',
'session.item.pending': '待处理',
'session.item.thinking': '思考中',
+22
View File
@@ -0,0 +1,22 @@
import { describe, expect, it } from 'vitest'
import { getSessionModelLabel } from './sessionModelLabel'
describe('getSessionModelLabel', () => {
it('prefers the explicit session model', () => {
expect(getSessionModelLabel({ model: 'gpt-5.4', modelMode: 'default' })).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',
value: 'Opus'
})
})
it('returns null when neither model nor mode is available', () => {
expect(getSessionModelLabel({})).toBeNull()
})
})
+28
View File
@@ -0,0 +1,28 @@
import { getModelModeLabel } from '@hapi/protocol'
import type { Session, SessionSummary } from '@/types/api'
type SessionModelSource = Pick<Session, 'model' | 'modelMode'> | Pick<SessionSummary, 'model' | 'modelMode'>
export type SessionModelLabel = {
key: 'session.item.model' | 'session.item.modelMode'
value: string
}
export function getSessionModelLabel(session: SessionModelSource): SessionModelLabel | null {
const explicitModel = typeof session.model === 'string' ? session.model.trim() : ''
if (explicitModel) {
return {
key: 'session.item.model',
value: explicitModel
}
}
if (session.modelMode) {
return {
key: 'session.item.modelMode',
value: getModelModeLabel(session.modelMode)
}
}
return null
}