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