mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
unify model selection
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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': '思考中',
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user