mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
feat(gemini): support mid-session model change (#379)
This commit is contained in:
@@ -20,7 +20,7 @@ import { useActiveSuggestions } from '@/hooks/useActiveSuggestions'
|
||||
import { applySuggestion } from '@/utils/applySuggestion'
|
||||
import { usePlatform } from '@/hooks/usePlatform'
|
||||
import { usePWAInstall } from '@/hooks/usePWAInstall'
|
||||
import { isClaudeFlavor } from '@/lib/agentFlavorUtils'
|
||||
import { isClaudeFlavor, supportsModelChange } from '@/lib/agentFlavorUtils'
|
||||
import { markSkillUsed } from '@/lib/recent-skills'
|
||||
import { FloatingOverlay } from '@/components/ChatInput/FloatingOverlay'
|
||||
import { Autocomplete } from '@/components/ChatInput/Autocomplete'
|
||||
@@ -28,7 +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'
|
||||
import { getModelOptionsForFlavor, getNextModelForFlavor } from './modelOptions'
|
||||
import { getClaudeComposerEffortOptions } from './claudeEffortOptions'
|
||||
|
||||
export interface TextInputState {
|
||||
@@ -266,8 +266,8 @@ export function HappyComposer(props: {
|
||||
[agentFlavor]
|
||||
)
|
||||
const claudeModelOptions = useMemo(
|
||||
() => getClaudeComposerModelOptions(model),
|
||||
[model]
|
||||
() => getModelOptionsForFlavor(agentFlavor, model),
|
||||
[agentFlavor, model]
|
||||
)
|
||||
const claudeEffortOptions = useMemo(
|
||||
() => getClaudeComposerEffortOptions(effort),
|
||||
@@ -352,9 +352,9 @@ export function HappyComposer(props: {
|
||||
|
||||
useEffect(() => {
|
||||
const handleGlobalKeyDown = (e: globalThis.KeyboardEvent) => {
|
||||
if (e.key === 'm' && (e.metaKey || e.ctrlKey) && onModelChange && isClaudeFlavor(agentFlavor)) {
|
||||
if (e.key === 'm' && (e.metaKey || e.ctrlKey) && onModelChange && supportsModelChange(agentFlavor)) {
|
||||
e.preventDefault()
|
||||
onModelChange(getNextClaudeComposerModel(model))
|
||||
onModelChange(getNextModelForFlavor(agentFlavor, model))
|
||||
haptic('light')
|
||||
}
|
||||
}
|
||||
@@ -439,7 +439,7 @@ export function HappyComposer(props: {
|
||||
|
||||
const showCollaborationSettings = Boolean(onCollaborationModeChange && collaborationModeOptions.length > 0)
|
||||
const showPermissionSettings = Boolean(onPermissionModeChange && permissionModeOptions.length > 0)
|
||||
const showModelSettings = Boolean(onModelChange && isClaudeFlavor(agentFlavor))
|
||||
const showModelSettings = Boolean(onModelChange && supportsModelChange(agentFlavor))
|
||||
const showEffortSettings = Boolean(onEffortChange && isClaudeFlavor(agentFlavor))
|
||||
const showSettingsButton = Boolean(showCollaborationSettings || showPermissionSettings || showModelSettings || showEffortSettings)
|
||||
const showAbortButton = true
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { getModelOptionsForFlavor, getNextModelForFlavor } from './modelOptions'
|
||||
|
||||
describe('getModelOptionsForFlavor', () => {
|
||||
it('returns Gemini model options for gemini flavor', () => {
|
||||
const options = getModelOptionsForFlavor('gemini')
|
||||
expect(options[0]).toEqual({ value: null, label: 'Default' })
|
||||
expect(options.some((o) => o.value === 'gemini-3-flash-preview')).toBe(true)
|
||||
expect(options.some((o) => o.value === 'gemini-2.5-flash')).toBe(true)
|
||||
})
|
||||
|
||||
it('returns Claude model options for claude flavor', () => {
|
||||
const options = getModelOptionsForFlavor('claude')
|
||||
expect(options[0]).toEqual({ value: null, label: 'Auto' })
|
||||
expect(options.some((o) => o.value === 'sonnet')).toBe(true)
|
||||
expect(options.some((o) => o.value === 'opus')).toBe(true)
|
||||
})
|
||||
|
||||
it('includes custom Gemini model from env/config in options', () => {
|
||||
const options = getModelOptionsForFlavor('gemini', 'gemini-custom-experiment')
|
||||
expect(options.some((o) => o.value === 'gemini-custom-experiment')).toBe(true)
|
||||
})
|
||||
|
||||
it('does not duplicate a preset Gemini model', () => {
|
||||
const options = getModelOptionsForFlavor('gemini', 'gemini-2.5-flash')
|
||||
const flashCount = options.filter((o) => o.value === 'gemini-2.5-flash').length
|
||||
expect(flashCount).toBe(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('getNextModelForFlavor', () => {
|
||||
it('cycles Gemini models', () => {
|
||||
const next = getNextModelForFlavor('gemini', null)
|
||||
expect(next).not.toBeNull()
|
||||
})
|
||||
|
||||
it('cycles Claude models', () => {
|
||||
const next = getNextModelForFlavor('claude', null)
|
||||
expect(next).not.toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,40 @@
|
||||
import { MODEL_OPTIONS } from '@/components/NewSession/types'
|
||||
import { getClaudeComposerModelOptions, getNextClaudeComposerModel } from './claudeModelOptions'
|
||||
import type { ClaudeComposerModelOption } from './claudeModelOptions'
|
||||
|
||||
export type ModelOption = ClaudeComposerModelOption
|
||||
|
||||
function getGeminiModelOptions(currentModel?: string | null): ModelOption[] {
|
||||
const options = MODEL_OPTIONS.gemini.map((m) => ({
|
||||
value: m.value === 'auto' ? null : m.value,
|
||||
label: m.label
|
||||
}))
|
||||
const normalized = currentModel?.trim() || null
|
||||
if (normalized && !options.some((o) => o.value === normalized)) {
|
||||
options.splice(1, 0, { value: normalized, label: normalized })
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
function getNextGeminiModel(currentModel?: string | null): string | null {
|
||||
const options = getGeminiModelOptions(currentModel)
|
||||
const currentIndex = options.findIndex((o) => o.value === (currentModel ?? null))
|
||||
if (currentIndex === -1) {
|
||||
return options[0]?.value ?? null
|
||||
}
|
||||
return options[(currentIndex + 1) % options.length]?.value ?? null
|
||||
}
|
||||
|
||||
export function getModelOptionsForFlavor(flavor: string | undefined | null, currentModel?: string | null): ModelOption[] {
|
||||
if (flavor === 'gemini') {
|
||||
return getGeminiModelOptions(currentModel)
|
||||
}
|
||||
return getClaudeComposerModelOptions(currentModel)
|
||||
}
|
||||
|
||||
export function getNextModelForFlavor(flavor: string | undefined | null, currentModel?: string | null): string | null {
|
||||
if (flavor === 'gemini') {
|
||||
return getNextGeminiModel(currentModel)
|
||||
}
|
||||
return getNextClaudeComposerModel(currentModel)
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import { GEMINI_MODEL_PRESETS, GEMINI_MODEL_LABELS } from '@hapi/protocol'
|
||||
|
||||
export type AgentType = 'claude' | 'codex' | 'cursor' | 'gemini' | 'opencode'
|
||||
export type SessionType = 'simple' | 'worktree'
|
||||
export type CodexReasoningEffort = 'default' | 'low' | 'medium' | 'high' | 'xhigh'
|
||||
@@ -23,12 +25,8 @@ export const MODEL_OPTIONS: Record<AgentType, { value: string; label: string }[]
|
||||
],
|
||||
cursor: [],
|
||||
gemini: [
|
||||
{ value: 'auto', label: 'Auto' },
|
||||
{ value: 'gemini-3.1-pro-preview', label: 'Gemini 3.1 Pro Preview' },
|
||||
{ value: 'gemini-3-flash-preview', label: 'Gemini 3 Flash Preview' },
|
||||
{ value: 'gemini-2.5-pro', label: 'Gemini 2.5 Pro' },
|
||||
{ value: 'gemini-2.5-flash', label: 'Gemini 2.5 Flash' },
|
||||
{ value: 'gemini-2.5-flash-lite', label: 'Gemini 2.5 Flash Lite' },
|
||||
{ value: 'auto', label: 'Default' },
|
||||
...GEMINI_MODEL_PRESETS.map(m => ({ value: m, label: GEMINI_MODEL_LABELS[m] })),
|
||||
],
|
||||
opencode: [],
|
||||
}
|
||||
|
||||
@@ -13,3 +13,7 @@ export function isCursorFlavor(flavor?: string | null): boolean {
|
||||
export function isKnownFlavor(flavor?: string | null): boolean {
|
||||
return isClaudeFlavor(flavor) || isCodexFamilyFlavor(flavor) || isCursorFlavor(flavor)
|
||||
}
|
||||
|
||||
export function supportsModelChange(flavor?: string | null): boolean {
|
||||
return flavor === 'claude' || flavor === 'gemini'
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user