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:
@@ -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 })
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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': '思考中',
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import type {
|
||||
export type {
|
||||
AgentState,
|
||||
AttachmentMetadata,
|
||||
ModelMode,
|
||||
PermissionMode,
|
||||
Session,
|
||||
SessionSummary,
|
||||
|
||||
Reference in New Issue
Block a user