mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-09 07:29:51 +00:00
feat: add Grok Build support (#1030)
* test: define Grok Build integration behavior * feat: add Grok Build agent integration * test: cover Grok permissions and resume paths * docs: add Grok Build setup guide * fix: scope Grok ACP discovery to session cwd * fix: align Grok permission UI semantics * docs: clarify Grok runner setup * test: require Grok create model and effort options * feat: add Grok create model and effort pickers * test: define Grok runtime parity behavior * feat: add Grok runtime ACP controls and discovery * fix: tighten Grok runtime controls * fix: suppress nonfatal Grok title quota errors * feat: support Grok Auto permission mode * feat: forward ACP native session titles for Grok * fix: guard Grok Windows shell arguments
This commit is contained in:
+33
-2
@@ -31,6 +31,8 @@ import type {
|
||||
DeleteUploadResponse,
|
||||
FileReadResponse,
|
||||
GitCommandResponse,
|
||||
GrokModelsResponse,
|
||||
GrokReasoningEffortResponse,
|
||||
ListDirectoryResponse,
|
||||
MachineListDirectoryResponse,
|
||||
MachinePathsExistsResponse,
|
||||
@@ -596,11 +598,22 @@ export class ApiClient {
|
||||
yolo?: boolean,
|
||||
sessionType?: 'simple' | 'worktree',
|
||||
worktreeName?: string,
|
||||
effort?: string
|
||||
effort?: string,
|
||||
permissionMode?: PermissionMode
|
||||
): Promise<SpawnResponse> {
|
||||
return await this.request<SpawnResponse>(`/api/machines/${encodeURIComponent(machineId)}/spawn`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ directory, agent, model, modelReasoningEffort, yolo, sessionType, worktreeName, effort })
|
||||
body: JSON.stringify({
|
||||
directory,
|
||||
agent,
|
||||
model,
|
||||
modelReasoningEffort,
|
||||
yolo,
|
||||
sessionType,
|
||||
worktreeName,
|
||||
effort,
|
||||
permissionMode
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -654,6 +667,24 @@ export class ApiClient {
|
||||
)
|
||||
}
|
||||
|
||||
async getMachineGrokModelsForCwd(machineId: string, cwd: string): Promise<GrokModelsResponse> {
|
||||
return await this.request<GrokModelsResponse>(
|
||||
`/api/machines/${encodeURIComponent(machineId)}/grok-models?cwd=${encodeURIComponent(cwd)}`
|
||||
)
|
||||
}
|
||||
|
||||
async getSessionGrokModels(sessionId: string): Promise<GrokModelsResponse> {
|
||||
return await this.request<GrokModelsResponse>(
|
||||
`/api/sessions/${encodeURIComponent(sessionId)}/grok-models`
|
||||
)
|
||||
}
|
||||
|
||||
async getSessionGrokReasoningEffortOptions(sessionId: string): Promise<GrokReasoningEffortResponse> {
|
||||
return await this.request<GrokReasoningEffortResponse>(
|
||||
`/api/sessions/${encodeURIComponent(sessionId)}/grok-reasoning-effort-options`
|
||||
)
|
||||
}
|
||||
|
||||
async getSlashCommands(sessionId: string): Promise<SlashCommandsResponse> {
|
||||
return await this.request<SlashCommandsResponse>(
|
||||
`/api/sessions/${encodeURIComponent(sessionId)}/slash-commands`
|
||||
|
||||
@@ -19,6 +19,10 @@ const FLAVOR_BADGES: Record<string, { label: string; colors: string }> = {
|
||||
label: 'Km',
|
||||
colors: 'bg-[#7c3aed] text-white',
|
||||
},
|
||||
grok: {
|
||||
label: 'Gr',
|
||||
colors: 'bg-[#111111] text-white',
|
||||
},
|
||||
pi: {
|
||||
label: 'Pi',
|
||||
colors: 'bg-[#5b21b6] text-white',
|
||||
|
||||
@@ -159,6 +159,7 @@ export function HappyComposer(props: {
|
||||
* disambiguates when two providers share a modelId). */
|
||||
piSelectedModel?: { provider: string; modelId: string } | null
|
||||
availableModelReasoningEffortOptions?: Array<{ value: string; name?: string }>
|
||||
availableEffortOptions?: Array<{ value: string; name?: string }>
|
||||
/** Cursor: selected base model key (not wire id). */
|
||||
selectedModelBase?: string | null
|
||||
/** Cursor: selected variant sku/wire for highlight when session stores an ACP wire id. */
|
||||
@@ -226,6 +227,7 @@ export function HappyComposer(props: {
|
||||
piModels,
|
||||
piSelectedModel,
|
||||
availableModelReasoningEffortOptions,
|
||||
availableEffortOptions,
|
||||
selectedModelBase,
|
||||
selectedModelVariant,
|
||||
modelEffortOptions,
|
||||
@@ -508,8 +510,16 @@ export function HappyComposer(props: {
|
||||
const claudeEffortOptions = useMemo(
|
||||
() => agentFlavor === 'pi'
|
||||
? getPiThinkingLevelOptions(effort, selectedPiModel?.thinkingLevelMap)
|
||||
: agentFlavor === 'grok' && availableEffortOptions && availableEffortOptions.length > 0
|
||||
? [
|
||||
{ value: null, label: 'Default' },
|
||||
...availableEffortOptions.map((option) => ({
|
||||
value: option.value,
|
||||
label: option.name ?? option.value
|
||||
}))
|
||||
]
|
||||
: getClaudeComposerEffortOptions(effort),
|
||||
[agentFlavor, effort, selectedPiModel]
|
||||
[agentFlavor, effort, selectedPiModel, availableEffortOptions]
|
||||
)
|
||||
const permissionModes = useMemo(
|
||||
() => permissionModeOptions.map((option) => option.mode),
|
||||
|
||||
@@ -154,6 +154,16 @@ describe('getModelOptionsForFlavor', () => {
|
||||
{ value: 'claude-sonnet-4-5', label: 'claude-sonnet-4-5' }
|
||||
])
|
||||
})
|
||||
|
||||
it('returns only default/current for grok without falling back to Claude models', () => {
|
||||
expect(getModelOptionsForFlavor('grok')).toEqual([
|
||||
{ value: null, label: 'Default' }
|
||||
])
|
||||
expect(getModelOptionsForFlavor('grok', 'grok-4.5')).toEqual([
|
||||
{ value: null, label: 'Default' },
|
||||
{ value: 'grok-4.5', label: 'grok-4.5' }
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('getNextModelForFlavor', () => {
|
||||
@@ -219,6 +229,10 @@ describe('getNextModelForFlavor', () => {
|
||||
expect(next).toBe('claude-sonnet-4-5')
|
||||
})
|
||||
|
||||
it('keeps the current grok model on cycle (no Claude fallback)', () => {
|
||||
expect(getNextModelForFlavor('grok', 'grok-4.5')).toBe('grok-4.5')
|
||||
})
|
||||
|
||||
it('returns null for pi without a current model (no Claude fallback)', () => {
|
||||
const next = getNextModelForFlavor('pi', null)
|
||||
expect(next).toBeNull()
|
||||
|
||||
@@ -126,6 +126,9 @@ export function getModelOptionsForFlavor(
|
||||
if (flavor === 'kimi') {
|
||||
return withCurrentModelOption([{ value: null, label: 'Default' }], currentModel)
|
||||
}
|
||||
if (flavor === 'grok') {
|
||||
return withCurrentModelOption([{ value: null, label: 'Default' }], currentModel)
|
||||
}
|
||||
// Pi model list is provided dynamically via piModels prop in SessionChat,
|
||||
// not through this function. Show just the auto/default option here to
|
||||
// prevent falling through to the Claude preset cycler (which would
|
||||
@@ -175,6 +178,9 @@ export function getNextModelForFlavor(
|
||||
if (flavor === 'kimi') {
|
||||
return normalizeCurrentModel(currentModel)
|
||||
}
|
||||
if (flavor === 'grok') {
|
||||
return normalizeCurrentModel(currentModel)
|
||||
}
|
||||
// Pi model list is provided dynamically via piModels prop — pressing
|
||||
// Ctrl/Cmd+M must not fall through to the Claude preset cycler.
|
||||
if (flavor === 'pi') {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { CREATABLE_AGENT_FLAVORS } from '@hapi/protocol'
|
||||
import { CREATABLE_AGENT_FLAVORS, getFlavorLabel } from '@hapi/protocol'
|
||||
import type { AgentType } from './types'
|
||||
import { useTranslation } from '@/lib/use-translation'
|
||||
|
||||
@@ -29,7 +29,7 @@ export function AgentSelector(props: {
|
||||
disabled={props.isDisabled}
|
||||
className="accent-[var(--app-link)]"
|
||||
/>
|
||||
<span className="text-sm capitalize">{agentType}</span>
|
||||
<span className="text-sm">{getFlavorLabel(agentType)}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { I18nProvider } from '@/lib/i18n-context'
|
||||
import { GrokPermissionModeSelector } from './GrokPermissionModeSelector'
|
||||
|
||||
describe('GrokPermissionModeSelector', () => {
|
||||
it('offers Auto when Grok advertises the account feature', () => {
|
||||
render(<I18nProvider>
|
||||
<GrokPermissionModeSelector
|
||||
agent="grok"
|
||||
value="default"
|
||||
autoPermissionModeSupported={true}
|
||||
isDisabled={false}
|
||||
onChange={vi.fn()}
|
||||
/>
|
||||
</I18nProvider>)
|
||||
|
||||
expect(screen.getByRole('option', { name: 'Auto' })).not.toBeDisabled()
|
||||
})
|
||||
|
||||
it('shows Auto as unavailable when Grok does not advertise it', () => {
|
||||
render(<I18nProvider>
|
||||
<GrokPermissionModeSelector
|
||||
agent="grok"
|
||||
value="default"
|
||||
autoPermissionModeSupported={false}
|
||||
isDisabled={false}
|
||||
onChange={vi.fn()}
|
||||
/>
|
||||
</I18nProvider>)
|
||||
|
||||
expect(screen.getByRole('option', { name: 'Auto (unavailable)' })).toBeDisabled()
|
||||
expect(screen.getByText(/did not enable Auto permissions/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,47 @@
|
||||
import {
|
||||
getPermissionModeOptionsForFlavor,
|
||||
type GrokPermissionMode
|
||||
} from '@hapi/protocol'
|
||||
import { useTranslation } from '@/lib/use-translation'
|
||||
import type { AgentType } from './types'
|
||||
|
||||
export function GrokPermissionModeSelector(props: {
|
||||
agent: AgentType
|
||||
value: GrokPermissionMode
|
||||
autoPermissionModeSupported: boolean | null
|
||||
isDisabled: boolean
|
||||
onChange: (value: GrokPermissionMode) => void
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (props.agent !== 'grok') return null
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1.5 px-3 py-3">
|
||||
<label className="text-xs font-medium text-[var(--app-hint)]">
|
||||
{t('misc.permissionMode')}
|
||||
</label>
|
||||
<select
|
||||
value={props.value}
|
||||
onChange={(event) => props.onChange(event.target.value as GrokPermissionMode)}
|
||||
disabled={props.isDisabled}
|
||||
className="w-full px-3 py-2 text-sm rounded-lg border border-[var(--app-divider)] bg-[var(--app-bg)] text-[var(--app-text)] focus:outline-none focus:ring-2 focus:ring-[var(--app-link)] disabled:opacity-50"
|
||||
>
|
||||
{getPermissionModeOptionsForFlavor('grok').map((option) => {
|
||||
const unavailable = option.mode === 'auto'
|
||||
&& props.autoPermissionModeSupported === false
|
||||
return (
|
||||
<option key={option.mode} value={option.mode} disabled={unavailable}>
|
||||
{option.label}{unavailable ? ` (${t('newSession.grokAutoUnavailable')})` : ''}
|
||||
</option>
|
||||
)
|
||||
})}
|
||||
</select>
|
||||
{props.autoPermissionModeSupported === false ? (
|
||||
<span className="text-xs text-[var(--app-hint)]">
|
||||
{t('newSession.grokAutoUnavailableDesc')}
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { fireEvent, render } from '@testing-library/react'
|
||||
|
||||
vi.mock('@/lib/use-translation', () => ({
|
||||
useTranslation: () => ({ t: (key: string) => key }),
|
||||
}))
|
||||
|
||||
import { LaunchEffortSelector } from './LaunchEffortSelector'
|
||||
|
||||
describe('LaunchEffortSelector', () => {
|
||||
it('renders Grok low/medium/high effort and forwards the selection', () => {
|
||||
const onChange = vi.fn()
|
||||
const { container } = render(
|
||||
<LaunchEffortSelector
|
||||
agent="grok"
|
||||
effort="auto"
|
||||
isDisabled={false}
|
||||
onEffortChange={onChange}
|
||||
/>
|
||||
)
|
||||
const select = container.querySelector('select') as HTMLSelectElement
|
||||
|
||||
expect(Array.from(select.options).map((option) => option.value)).toEqual([
|
||||
'auto', 'low', 'medium', 'high'
|
||||
])
|
||||
fireEvent.change(select, { target: { value: 'low' } })
|
||||
expect(onChange).toHaveBeenCalledWith('low')
|
||||
})
|
||||
})
|
||||
+13
-8
@@ -1,19 +1,24 @@
|
||||
import type { AgentType, ClaudeEffort } from './types'
|
||||
import { CLAUDE_EFFORT_OPTIONS } from './types'
|
||||
import type { AgentType, LaunchEffort } from './types'
|
||||
import { CLAUDE_EFFORT_OPTIONS, GROK_EFFORT_OPTIONS } from './types'
|
||||
import { useTranslation } from '@/lib/use-translation'
|
||||
|
||||
export function ClaudeEffortSelector(props: {
|
||||
export function LaunchEffortSelector(props: {
|
||||
agent: AgentType
|
||||
effort: ClaudeEffort
|
||||
effort: LaunchEffort
|
||||
isDisabled: boolean
|
||||
onEffortChange: (value: ClaudeEffort) => void
|
||||
onEffortChange: (value: LaunchEffort) => void
|
||||
grokOptions?: Array<{ value: string; label: string }>
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
|
||||
if (props.agent !== 'claude') {
|
||||
if (props.agent !== 'claude' && props.agent !== 'grok') {
|
||||
return null
|
||||
}
|
||||
|
||||
const options = props.agent === 'grok'
|
||||
? (props.grokOptions ?? GROK_EFFORT_OPTIONS)
|
||||
: CLAUDE_EFFORT_OPTIONS
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1.5 px-3 py-3">
|
||||
<label className="text-xs font-medium text-[var(--app-hint)]">
|
||||
@@ -22,11 +27,11 @@ export function ClaudeEffortSelector(props: {
|
||||
</label>
|
||||
<select
|
||||
value={props.effort}
|
||||
onChange={(e) => props.onEffortChange(e.target.value as ClaudeEffort)}
|
||||
onChange={(e) => props.onEffortChange(e.target.value)}
|
||||
disabled={props.isDisabled}
|
||||
className="w-full px-3 py-2 text-sm rounded-lg border border-[var(--app-divider)] bg-[var(--app-bg)] text-[var(--app-text)] focus:outline-none focus:ring-2 focus:ring-[var(--app-link)] disabled:opacity-50"
|
||||
>
|
||||
{CLAUDE_EFFORT_OPTIONS.map((option) => (
|
||||
{options.map((option) => (
|
||||
<option key={option.value} value={option.value}>
|
||||
{option.label}
|
||||
</option>
|
||||
@@ -0,0 +1,42 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { buildGrokEffortOptions, buildGrokModelOptions, shouldEnableGrokModelDiscovery } from './grokModels'
|
||||
|
||||
describe('Grok Create-session options', () => {
|
||||
it('enables model discovery only for an existing cwd on the target machine', () => {
|
||||
const args = {
|
||||
agent: 'grok' as const,
|
||||
machineId: 'machine-1',
|
||||
cwd: '/home/user/project',
|
||||
cwdExists: true,
|
||||
}
|
||||
|
||||
expect(shouldEnableGrokModelDiscovery(args)).toBe(true)
|
||||
expect(shouldEnableGrokModelDiscovery({ ...args, cwdExists: undefined })).toBe(false)
|
||||
expect(shouldEnableGrokModelDiscovery({ ...args, agent: 'claude' })).toBe(false)
|
||||
})
|
||||
|
||||
it('shows Default plus every discovered Grok model', () => {
|
||||
expect(buildGrokModelOptions([
|
||||
{ modelId: 'grok-4.5' },
|
||||
{ modelId: 'custom-fast', name: 'Custom Fast' }
|
||||
])).toEqual([
|
||||
{ value: 'auto', label: 'Default' },
|
||||
{ value: 'grok-4.5', label: 'grok-4.5' },
|
||||
{ value: 'custom-fast', label: 'Custom Fast' }
|
||||
])
|
||||
})
|
||||
|
||||
it('uses the selected model ACP effort catalog', () => {
|
||||
expect(buildGrokEffortOptions([{
|
||||
modelId: 'grok-4.5',
|
||||
reasoningEfforts: [
|
||||
{ value: 'high', name: 'High Effort', isDefault: true },
|
||||
{ value: 'low', name: 'Low Effort' }
|
||||
]
|
||||
}], 'auto', 'grok-4.5')).toEqual([
|
||||
{ value: 'auto', label: 'Default' },
|
||||
{ value: 'high', label: 'High Effort' },
|
||||
{ value: 'low', label: 'Low Effort' }
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,50 @@
|
||||
import type { GrokModelSummary } from '@/types/api'
|
||||
import type { AgentType } from './types'
|
||||
|
||||
export function shouldEnableGrokModelDiscovery(args: {
|
||||
agent: AgentType
|
||||
machineId: string | null
|
||||
cwd: string
|
||||
cwdExists: boolean | undefined
|
||||
}): boolean {
|
||||
return args.agent === 'grok'
|
||||
&& Boolean(args.machineId)
|
||||
&& args.cwd.length > 0
|
||||
&& args.cwdExists === true
|
||||
}
|
||||
|
||||
export function buildGrokModelOptions(
|
||||
availableModels: GrokModelSummary[]
|
||||
): Array<{ value: string; label: string }> {
|
||||
return [
|
||||
{ value: 'auto', label: 'Default' },
|
||||
...availableModels.map((model) => ({
|
||||
value: model.modelId,
|
||||
label: model.name ?? model.modelId
|
||||
}))
|
||||
]
|
||||
}
|
||||
|
||||
export function buildGrokEffortOptions(
|
||||
availableModels: GrokModelSummary[],
|
||||
selectedModel: string,
|
||||
currentModelId: string | null
|
||||
): Array<{ value: string; label: string }> {
|
||||
const effectiveModel = selectedModel === 'auto' ? currentModelId : selectedModel
|
||||
const efforts = availableModels.find((model) => model.modelId === effectiveModel)?.reasoningEfforts
|
||||
if (!efforts || efforts.length === 0) {
|
||||
return [
|
||||
{ value: 'auto', label: 'Default' },
|
||||
{ value: 'low', label: 'Low' },
|
||||
{ value: 'medium', label: 'Medium' },
|
||||
{ value: 'high', label: 'High' }
|
||||
]
|
||||
}
|
||||
return [
|
||||
{ value: 'auto', label: 'Default' },
|
||||
...efforts.map((effort) => ({
|
||||
value: effort.value,
|
||||
label: effort.name ?? effort.value
|
||||
}))
|
||||
]
|
||||
}
|
||||
@@ -1,12 +1,14 @@
|
||||
import { useCallback, useDeferredValue, useEffect, useMemo, useRef, useState, type KeyboardEvent as ReactKeyboardEvent } from 'react'
|
||||
import type { ApiClient } from '@/api/client'
|
||||
import type { Machine } from '@/types/api'
|
||||
import type { GrokPermissionMode } from '@hapi/protocol'
|
||||
import { usePlatform } from '@/hooks/usePlatform'
|
||||
import { useMachinePathsExists } from '@/hooks/useMachinePathsExists'
|
||||
import { useSpawnSession } from '@/hooks/mutations/useSpawnSession'
|
||||
import { useCodexModels } from '@/hooks/queries/useCodexModels'
|
||||
import { useCursorModelsForMachine } from '@/hooks/queries/useCursorModelsForMachine'
|
||||
import { useOpencodeModelsForCwd } from '@/hooks/queries/useOpencodeModelsForCwd'
|
||||
import { useGrokModelsForCwd } from '@/hooks/queries/useGrokModelsForCwd'
|
||||
import { useSessions } from '@/hooks/queries/useSessions'
|
||||
import { useActiveSuggestions, type Suggestion } from '@/hooks/useActiveSuggestions'
|
||||
import { useDirectorySuggestions } from '@/hooks/useDirectorySuggestions'
|
||||
@@ -30,15 +32,17 @@ import {
|
||||
saveNewSessionFormDraft,
|
||||
shouldRestoreNewSessionFormDraft
|
||||
} from './newSessionFormDraft'
|
||||
import type { AgentType, ClaudeEffort, CodexReasoningEffort, SessionType } from './types'
|
||||
import type { AgentType, LaunchEffort, CodexReasoningEffort, SessionType } from './types'
|
||||
import { ActionButtons } from './ActionButtons'
|
||||
import { AgentSelector } from './AgentSelector'
|
||||
import { DirectorySection } from './DirectorySection'
|
||||
import { GrokPermissionModeSelector } from './GrokPermissionModeSelector'
|
||||
import { MachineSelector } from './MachineSelector'
|
||||
import { ModelSelector } from './ModelSelector'
|
||||
import { OpencodeModelSelector } from './OpencodeModelSelector'
|
||||
import { ClaudeEffortSelector } from './ClaudeEffortSelector'
|
||||
import { LaunchEffortSelector } from './LaunchEffortSelector'
|
||||
import { shouldEnableOpencodeModelDiscovery } from './opencodeModelsGate'
|
||||
import { buildGrokEffortOptions, buildGrokModelOptions, shouldEnableGrokModelDiscovery } from './grokModels'
|
||||
import { ReasoningEffortSelector } from './ReasoningEffortSelector'
|
||||
import {
|
||||
loadPreferredAgent,
|
||||
@@ -75,9 +79,10 @@ export function NewSession(props: {
|
||||
const [model, setModel] = useState('auto')
|
||||
const [cursorSelectedBase, setCursorSelectedBase] = useState('auto')
|
||||
const pendingCursorBaseRef = useRef<string | null>(null)
|
||||
const [effort, setEffort] = useState<ClaudeEffort>('auto')
|
||||
const [effort, setEffort] = useState<LaunchEffort>('auto')
|
||||
const [modelReasoningEffort, setModelReasoningEffort] = useState<CodexReasoningEffort>('default')
|
||||
const [yoloMode, setYoloMode] = useState(loadPreferredYoloMode)
|
||||
const [grokPermissionMode, setGrokPermissionMode] = useState<GrokPermissionMode>('default')
|
||||
const [sessionType, setSessionType] = useState<SessionType>('simple')
|
||||
const [worktreeName, setWorktreeName] = useState('')
|
||||
const [directoryCreationConfirmed, setDirectoryCreationConfirmed] = useState(false)
|
||||
@@ -93,6 +98,7 @@ export function NewSession(props: {
|
||||
useEffect(() => {
|
||||
setEffort('auto')
|
||||
setModelReasoningEffort('default')
|
||||
setGrokPermissionMode('default')
|
||||
if (agent !== 'cursor') {
|
||||
setModel('auto')
|
||||
setCursorSelectedBase('auto')
|
||||
@@ -146,6 +152,7 @@ export function NewSession(props: {
|
||||
setEffort(draft.effort)
|
||||
setModelReasoningEffort(draft.modelReasoningEffort)
|
||||
setYoloMode(draft.yoloMode)
|
||||
setGrokPermissionMode(draft.grokPermissionMode)
|
||||
setSessionType(draft.sessionType)
|
||||
setWorktreeName(draft.worktreeName)
|
||||
clearNewSessionFormDraft()
|
||||
@@ -364,6 +371,38 @@ export function NewSession(props: {
|
||||
cwdExists: deferredDirectoryExists,
|
||||
})
|
||||
})
|
||||
const grokModelsState = useGrokModelsForCwd({
|
||||
api: props.api,
|
||||
machineId,
|
||||
cwd: deferredDirectory,
|
||||
enabled: shouldEnableGrokModelDiscovery({
|
||||
agent,
|
||||
machineId,
|
||||
cwd: deferredDirectory,
|
||||
cwdExists: deferredDirectoryExists,
|
||||
})
|
||||
})
|
||||
const grokModelOptions = useMemo(
|
||||
() => buildGrokModelOptions(grokModelsState.availableModels),
|
||||
[grokModelsState.availableModels]
|
||||
)
|
||||
const grokEffortOptions = useMemo(
|
||||
() => buildGrokEffortOptions(
|
||||
grokModelsState.availableModels,
|
||||
model,
|
||||
grokModelsState.currentModelId
|
||||
),
|
||||
[grokModelsState.availableModels, grokModelsState.currentModelId, model]
|
||||
)
|
||||
useEffect(() => {
|
||||
if (
|
||||
agent === 'grok'
|
||||
&& grokPermissionMode === 'auto'
|
||||
&& grokModelsState.autoPermissionModeSupported === false
|
||||
) {
|
||||
setGrokPermissionMode('default')
|
||||
}
|
||||
}, [agent, grokPermissionMode, grokModelsState.autoPermissionModeSupported])
|
||||
useEffect(() => {
|
||||
// Auto-pick the OpenCode default model when discovery finishes, so the
|
||||
// form has a sensible value if the user hits Enter without scrolling.
|
||||
@@ -476,6 +515,7 @@ export function NewSession(props: {
|
||||
effort,
|
||||
modelReasoningEffort,
|
||||
yoloMode,
|
||||
grokPermissionMode,
|
||||
sessionType,
|
||||
worktreeName
|
||||
})
|
||||
@@ -489,6 +529,7 @@ export function NewSession(props: {
|
||||
effort,
|
||||
modelReasoningEffort,
|
||||
yoloMode,
|
||||
grokPermissionMode,
|
||||
sessionType,
|
||||
worktreeName,
|
||||
trimmedDirectory
|
||||
@@ -580,7 +621,9 @@ export function NewSession(props: {
|
||||
const resolvedModel = agent === 'opencode'
|
||||
? (opencodeSelectedModel ?? undefined)
|
||||
: (model !== 'auto' ? model : undefined)
|
||||
const resolvedEffort = agent === 'claude' && effort !== 'auto' ? effort : undefined
|
||||
const resolvedEffort = (agent === 'claude' || agent === 'grok') && effort !== 'auto'
|
||||
? effort
|
||||
: undefined
|
||||
const resolvedModelReasoningEffort = (agent === 'codex' || agent === 'opencode') && modelReasoningEffort !== 'default'
|
||||
? modelReasoningEffort
|
||||
: undefined
|
||||
@@ -591,7 +634,8 @@ export function NewSession(props: {
|
||||
model: resolvedModel,
|
||||
effort: resolvedEffort,
|
||||
modelReasoningEffort: resolvedModelReasoningEffort,
|
||||
yolo: yoloMode,
|
||||
yolo: agent === 'grok' ? undefined : yoloMode,
|
||||
permissionMode: agent === 'grok' ? grokPermissionMode : undefined,
|
||||
sessionType,
|
||||
worktreeName: sessionType === 'worktree' ? (worktreeName.trim() || undefined) : undefined
|
||||
})
|
||||
@@ -717,25 +761,32 @@ export function NewSession(props: {
|
||||
options={
|
||||
agent === 'codex'
|
||||
? codexModelOptions
|
||||
: agent === 'grok'
|
||||
? grokModelOptions
|
||||
: undefined
|
||||
}
|
||||
isDisabled={
|
||||
isFormDisabled
|
||||
|| (agent === 'codex' && Boolean(codexModelsState.error))
|
||||
|| (agent === 'grok' && Boolean(grokModelsState.error))
|
||||
}
|
||||
isLoading={agent === 'codex' && codexModelsState.isLoading}
|
||||
isLoading={(agent === 'codex' && codexModelsState.isLoading)
|
||||
|| (agent === 'grok' && grokModelsState.isLoading)}
|
||||
error={agent === 'codex' && codexModelsState.error
|
||||
? `${t('newSession.model.loadFailed')}: ${codexModelsState.error}`
|
||||
: null}
|
||||
: agent === 'grok' && grokModelsState.error
|
||||
? `${t('newSession.model.loadFailed')}: ${grokModelsState.error}`
|
||||
: null}
|
||||
onModelChange={setModel}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
<ClaudeEffortSelector
|
||||
<LaunchEffortSelector
|
||||
agent={agent}
|
||||
effort={effort}
|
||||
isDisabled={isFormDisabled}
|
||||
onEffortChange={setEffort}
|
||||
grokOptions={agent === 'grok' ? grokEffortOptions : undefined}
|
||||
/>
|
||||
<ReasoningEffortSelector
|
||||
agent={agent}
|
||||
@@ -744,11 +795,20 @@ export function NewSession(props: {
|
||||
isDisabled={isFormDisabled || (agent === 'codex' && codexModelsState.isLoading)}
|
||||
onChange={setModelReasoningEffort}
|
||||
/>
|
||||
<YoloToggle
|
||||
yoloMode={yoloMode}
|
||||
<GrokPermissionModeSelector
|
||||
agent={agent}
|
||||
value={grokPermissionMode}
|
||||
autoPermissionModeSupported={grokModelsState.autoPermissionModeSupported}
|
||||
isDisabled={isFormDisabled}
|
||||
onToggle={setYoloMode}
|
||||
onChange={setGrokPermissionMode}
|
||||
/>
|
||||
{agent !== 'grok' ? (
|
||||
<YoloToggle
|
||||
yoloMode={yoloMode}
|
||||
isDisabled={isFormDisabled}
|
||||
onToggle={setYoloMode}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{(error ?? spawnError) ? (
|
||||
<div className="px-3 py-2 text-sm text-red-600">
|
||||
|
||||
@@ -21,6 +21,7 @@ describe('newSessionFormDraft', () => {
|
||||
effort: 'auto',
|
||||
modelReasoningEffort: 'default',
|
||||
yoloMode: false,
|
||||
grokPermissionMode: 'default',
|
||||
sessionType: 'simple',
|
||||
worktreeName: ''
|
||||
})
|
||||
@@ -33,6 +34,7 @@ describe('newSessionFormDraft', () => {
|
||||
effort: 'auto',
|
||||
modelReasoningEffort: 'default',
|
||||
yoloMode: false,
|
||||
grokPermissionMode: 'default',
|
||||
sessionType: 'simple',
|
||||
worktreeName: ''
|
||||
})
|
||||
@@ -58,6 +60,7 @@ describe('newSessionFormDraft', () => {
|
||||
effort: 'auto',
|
||||
modelReasoningEffort: 'default',
|
||||
yoloMode: false,
|
||||
grokPermissionMode: 'default',
|
||||
sessionType: 'simple',
|
||||
worktreeName: ''
|
||||
})
|
||||
@@ -74,6 +77,7 @@ describe('newSessionFormDraft', () => {
|
||||
effort: 'auto',
|
||||
modelReasoningEffort: 'default',
|
||||
yoloMode: false,
|
||||
grokPermissionMode: 'default',
|
||||
sessionType: 'simple',
|
||||
worktreeName: ''
|
||||
})
|
||||
@@ -90,6 +94,7 @@ describe('newSessionFormDraft', () => {
|
||||
effort: 'high',
|
||||
modelReasoningEffort: 'high',
|
||||
yoloMode: true,
|
||||
grokPermissionMode: 'default',
|
||||
sessionType: 'simple',
|
||||
worktreeName: ''
|
||||
})
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { CREATABLE_AGENT_FLAVORS } from '@hapi/protocol'
|
||||
import type { AgentType, ClaudeEffort, CodexReasoningEffort, SessionType } from './types'
|
||||
import {
|
||||
CREATABLE_AGENT_FLAVORS,
|
||||
GROK_PERMISSION_MODES,
|
||||
type GrokPermissionMode
|
||||
} from '@hapi/protocol'
|
||||
import type { AgentType, LaunchEffort, CodexReasoningEffort, SessionType } from './types'
|
||||
|
||||
const DRAFT_STORAGE_KEY = 'hapi:new-session-form-draft'
|
||||
|
||||
@@ -8,9 +12,10 @@ export type NewSessionFormDraft = {
|
||||
model: string
|
||||
cursorSelectedBase: string
|
||||
machineId: string | null
|
||||
effort: ClaudeEffort
|
||||
effort: LaunchEffort
|
||||
modelReasoningEffort: CodexReasoningEffort
|
||||
yoloMode: boolean
|
||||
grokPermissionMode: GrokPermissionMode
|
||||
sessionType: SessionType
|
||||
worktreeName: string
|
||||
}
|
||||
@@ -48,11 +53,15 @@ export function loadNewSessionFormDraft(): NewSessionFormDraft | null {
|
||||
? parsed.cursorSelectedBase
|
||||
: 'auto',
|
||||
machineId: typeof parsed.machineId === 'string' ? parsed.machineId : null,
|
||||
effort: agentPreserved ? ((parsed.effort as ClaudeEffort | undefined) ?? 'auto') : 'auto',
|
||||
effort: agentPreserved ? ((parsed.effort as LaunchEffort | undefined) ?? 'auto') : 'auto',
|
||||
modelReasoningEffort: agentPreserved
|
||||
? ((parsed.modelReasoningEffort as CodexReasoningEffort | undefined) ?? 'default')
|
||||
: 'default',
|
||||
yoloMode: Boolean(parsed.yoloMode),
|
||||
grokPermissionMode: agentPreserved
|
||||
&& GROK_PERMISSION_MODES.includes(parsed.grokPermissionMode as GrokPermissionMode)
|
||||
? parsed.grokPermissionMode as GrokPermissionMode
|
||||
: 'default',
|
||||
sessionType: (parsed.sessionType as SessionType | undefined) ?? 'simple',
|
||||
worktreeName: typeof parsed.worktreeName === 'string' ? parsed.worktreeName : ''
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { CLAUDE_MODEL_PRESETS, getClaudeModelLabel } from '@hapi/protocol'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { CLAUDE_EFFORT_OPTIONS, MODEL_OPTIONS } from './types'
|
||||
import { CLAUDE_EFFORT_OPTIONS, GROK_EFFORT_OPTIONS, MODEL_OPTIONS } from './types'
|
||||
|
||||
describe('Claude model options', () => {
|
||||
it('derives options from shared Claude model presets', () => {
|
||||
@@ -33,3 +33,14 @@ describe('Claude effort options', () => {
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('Grok effort options', () => {
|
||||
it('offers only the effort levels supported by grok-4.5', () => {
|
||||
expect(GROK_EFFORT_OPTIONS).toEqual([
|
||||
{ value: 'auto', label: 'Default' },
|
||||
{ value: 'low', label: 'Low' },
|
||||
{ value: 'medium', label: 'Medium' },
|
||||
{ value: 'high', label: 'High' },
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -6,13 +6,15 @@ import {
|
||||
GEMINI_MODEL_LABELS,
|
||||
GEMINI_MODEL_PRESETS
|
||||
} from '@hapi/protocol'
|
||||
import type { AgentFlavor, ClaudeEffortLevel } from '@hapi/protocol'
|
||||
import type { AgentFlavor } from '@hapi/protocol'
|
||||
|
||||
export type AgentType = AgentFlavor
|
||||
export type SessionType = 'simple' | 'worktree'
|
||||
// Codex reports supported efforts dynamically; keep this open for new server values.
|
||||
export type CodexReasoningEffort = string
|
||||
export type ClaudeEffort = 'auto' | ClaudeEffortLevel
|
||||
// Grok reports effort values dynamically through ACP, while Claude uses the
|
||||
// fixed ClaudeEffortLevel catalog.
|
||||
export type LaunchEffort = string
|
||||
|
||||
function modelPresetOptions<TModel extends string>(
|
||||
presets: readonly TModel[],
|
||||
@@ -38,6 +40,7 @@ export const MODEL_OPTIONS: Record<AgentType, { value: string; label: string }[]
|
||||
...modelPresetOptions(GEMINI_MODEL_PRESETS, GEMINI_MODEL_LABELS),
|
||||
],
|
||||
opencode: [],
|
||||
grok: [],
|
||||
pi: [],
|
||||
}
|
||||
|
||||
@@ -50,7 +53,14 @@ export const CODEX_REASONING_EFFORT_OPTIONS: { value: CodexReasoningEffort; labe
|
||||
{ value: 'max', label: 'Max' },
|
||||
]
|
||||
|
||||
export const CLAUDE_EFFORT_OPTIONS: { value: ClaudeEffort; label: string }[] = [
|
||||
export const CLAUDE_EFFORT_OPTIONS: { value: LaunchEffort; label: string }[] = [
|
||||
{ value: 'auto', label: 'Auto' },
|
||||
...CLAUDE_EFFORT_LEVELS.map((value) => ({ value, label: CLAUDE_EFFORT_LABELS[value] })),
|
||||
]
|
||||
|
||||
export const GROK_EFFORT_OPTIONS: { value: LaunchEffort; label: string }[] = [
|
||||
{ value: 'auto', label: 'Default' },
|
||||
{ value: 'low', label: 'Low' },
|
||||
{ value: 'medium', label: 'Medium' },
|
||||
{ value: 'high', label: 'High' },
|
||||
]
|
||||
|
||||
@@ -62,6 +62,8 @@ import {
|
||||
} from '@/lib/sessionChatCursorModel'
|
||||
import { buildCursorEffortPickerOptions, resolveCursorVariantOptions } from '@/lib/cursorModelOptions'
|
||||
import { useOpencodeModels } from '@/hooks/queries/useOpencodeModels'
|
||||
import { useGrokModels } from '@/hooks/queries/useGrokModels'
|
||||
import { useGrokReasoningEffortOptions } from '@/hooks/queries/useGrokReasoningEffortOptions'
|
||||
import { usePiModels } from '@/hooks/queries/usePiModels'
|
||||
import { useOpencodeReasoningEffortOptions } from '@/hooks/queries/useOpencodeReasoningEffortOptions'
|
||||
import { useVoiceOptional } from '@/lib/voice-context'
|
||||
@@ -577,6 +579,27 @@ function SessionChatInner(props: SessionChatProps) {
|
||||
label: opencodeModel.name ?? opencodeModel.modelId
|
||||
}))
|
||||
}, [agentFlavor, opencodeModelsState.availableModels])
|
||||
const grokModelsState = useGrokModels({
|
||||
api: props.api,
|
||||
sessionId: props.session.id,
|
||||
enabled: agentFlavor === 'grok' && props.session.active && !controlledByUser
|
||||
})
|
||||
const grokEffortState = useGrokReasoningEffortOptions({
|
||||
api: props.api,
|
||||
sessionId: props.session.id,
|
||||
enabled: agentFlavor === 'grok' && props.session.active && !controlledByUser
|
||||
})
|
||||
const grokModelOptions = useMemo(() => (
|
||||
agentFlavor === 'grok'
|
||||
? [
|
||||
{ value: null, label: 'Default' },
|
||||
...grokModelsState.availableModels.map((model) => ({
|
||||
value: model.modelId,
|
||||
label: model.name ?? model.modelId
|
||||
}))
|
||||
]
|
||||
: undefined
|
||||
), [agentFlavor, grokModelsState.availableModels])
|
||||
const cursorModelsState = useCursorModels({
|
||||
api: props.api,
|
||||
sessionId: props.session.id,
|
||||
@@ -1296,6 +1319,8 @@ function SessionChatInner(props: SessionChatProps) {
|
||||
)
|
||||
: agentFlavor === 'opencode'
|
||||
? opencodeModelOptions
|
||||
: agentFlavor === 'grok'
|
||||
? grokModelOptions
|
||||
// Pi uses its own provider-qualified picker (piModels prop).
|
||||
// Feeding piModelOptions here would make the generic Ctrl/Cmd+M
|
||||
// cycler (getNextModelForFlavor) post a bare modelId string,
|
||||
@@ -1313,6 +1338,11 @@ function SessionChatInner(props: SessionChatProps) {
|
||||
? opencodeReasoningEffortState.options
|
||||
: undefined
|
||||
}
|
||||
availableEffortOptions={
|
||||
agentFlavor === 'grok' && grokEffortState.options.length > 0
|
||||
? grokEffortState.options
|
||||
: undefined
|
||||
}
|
||||
active={props.session.active}
|
||||
allowSendWhenInactive
|
||||
thinking={props.session.thinking}
|
||||
@@ -1361,6 +1391,10 @@ function SessionChatInner(props: SessionChatProps) {
|
||||
: undefined)
|
||||
: agentFlavor === 'pi'
|
||||
? (props.session.active && !piModelsState.error ? handleModelChange : undefined)
|
||||
: agentFlavor === 'grok'
|
||||
? (props.session.active && !controlledByUser && !grokModelsState.error
|
||||
? handleModelChange
|
||||
: undefined)
|
||||
: handleModelChange
|
||||
}
|
||||
onModelEffortChange={
|
||||
@@ -1380,7 +1414,13 @@ function SessionChatInner(props: SessionChatProps) {
|
||||
? handleModelReasoningEffortChange
|
||||
: undefined
|
||||
}
|
||||
onEffortChange={handleEffortChange}
|
||||
onEffortChange={
|
||||
agentFlavor === 'grok'
|
||||
? (props.session.active && !controlledByUser && grokEffortState.options.length > 0
|
||||
? handleEffortChange
|
||||
: undefined)
|
||||
: handleEffortChange
|
||||
}
|
||||
serviceTier={agentFlavor === 'codex' ? props.session.serviceTier : undefined}
|
||||
onServiceTierChange={
|
||||
agentFlavor === 'codex'
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import type { AgentFlavor } from '@hapi/protocol'
|
||||
import type { AgentFlavor, PermissionMode } from '@hapi/protocol'
|
||||
import type { ApiClient } from '@/api/client'
|
||||
import type { SpawnResponse } from '@/types/api'
|
||||
import { queryKeys } from '@/lib/query-keys'
|
||||
@@ -12,6 +12,7 @@ type SpawnInput = {
|
||||
effort?: string
|
||||
modelReasoningEffort?: string
|
||||
yolo?: boolean
|
||||
permissionMode?: PermissionMode
|
||||
sessionType?: 'simple' | 'worktree'
|
||||
worktreeName?: string
|
||||
}
|
||||
@@ -37,7 +38,8 @@ export function useSpawnSession(api: ApiClient | null): {
|
||||
input.yolo,
|
||||
input.sessionType,
|
||||
input.worktreeName,
|
||||
input.effort
|
||||
input.effort,
|
||||
input.permissionMode
|
||||
)
|
||||
},
|
||||
onSuccess: () => {
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import type { ApiClient } from '@/api/client'
|
||||
import type { GrokModelSummary } from '@/types/api'
|
||||
import { queryKeys } from '@/lib/query-keys'
|
||||
|
||||
export function useGrokModels(args: {
|
||||
api: ApiClient | null
|
||||
sessionId?: string | null
|
||||
enabled?: boolean
|
||||
}): {
|
||||
availableModels: GrokModelSummary[]
|
||||
currentModelId: string | null
|
||||
isLoading: boolean
|
||||
error: string | null
|
||||
} {
|
||||
const enabled = Boolean(args.enabled && args.api && args.sessionId)
|
||||
const query = useQuery({
|
||||
queryKey: args.sessionId
|
||||
? queryKeys.sessionGrokModels(args.sessionId)
|
||||
: ['session-grok-models', 'unknown'] as const,
|
||||
queryFn: async () => {
|
||||
if (!args.api || !args.sessionId) throw new Error('Grok session unavailable')
|
||||
return await args.api.getSessionGrokModels(args.sessionId)
|
||||
},
|
||||
enabled,
|
||||
staleTime: 30_000,
|
||||
retry: false,
|
||||
})
|
||||
|
||||
return {
|
||||
availableModels: query.data?.availableModels ?? [],
|
||||
currentModelId: query.data?.currentModelId ?? null,
|
||||
isLoading: query.isLoading,
|
||||
error: query.data?.success === false
|
||||
? (query.data.error ?? 'Failed to load Grok models')
|
||||
: query.error instanceof Error ? query.error.message : null,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import type { ApiClient } from '@/api/client'
|
||||
import type { GrokModelSummary } from '@/types/api'
|
||||
import { queryKeys } from '@/lib/query-keys'
|
||||
|
||||
export function useGrokModelsForCwd(args: {
|
||||
api: ApiClient | null
|
||||
machineId?: string | null
|
||||
cwd?: string | null
|
||||
enabled?: boolean
|
||||
}): {
|
||||
availableModels: GrokModelSummary[]
|
||||
currentModelId: string | null
|
||||
autoPermissionModeSupported: boolean | null
|
||||
isLoading: boolean
|
||||
error: string | null
|
||||
} {
|
||||
const { api, machineId, cwd } = args
|
||||
const trimmedCwd = typeof cwd === 'string' ? cwd.trim() : ''
|
||||
const enabled = Boolean(args.enabled && api && machineId && trimmedCwd)
|
||||
|
||||
const query = useQuery({
|
||||
queryKey: machineId && trimmedCwd
|
||||
? queryKeys.machineGrokModelsForCwd(machineId, trimmedCwd)
|
||||
: ['machine-grok-models', 'unknown', 'unknown'] as const,
|
||||
queryFn: async () => {
|
||||
if (!api || !machineId || !trimmedCwd) {
|
||||
throw new Error('Grok models target unavailable')
|
||||
}
|
||||
return await api.getMachineGrokModelsForCwd(machineId, trimmedCwd)
|
||||
},
|
||||
enabled,
|
||||
staleTime: 60_000,
|
||||
retry: false,
|
||||
})
|
||||
|
||||
return {
|
||||
availableModels: query.data?.availableModels ?? [],
|
||||
currentModelId: query.data?.currentModelId ?? null,
|
||||
autoPermissionModeSupported: query.data?.autoPermissionModeSupported ?? null,
|
||||
isLoading: query.isLoading,
|
||||
error: query.data?.success === false
|
||||
? (query.data.error ?? 'Failed to load Grok models')
|
||||
: query.error instanceof Error
|
||||
? query.error.message
|
||||
: query.error
|
||||
? 'Failed to load Grok models'
|
||||
: null,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import type { ApiClient } from '@/api/client'
|
||||
import type { GrokReasoningEffortOption } from '@/types/api'
|
||||
import { queryKeys } from '@/lib/query-keys'
|
||||
|
||||
export function useGrokReasoningEffortOptions(args: {
|
||||
api: ApiClient | null
|
||||
sessionId?: string | null
|
||||
enabled?: boolean
|
||||
}): {
|
||||
options: GrokReasoningEffortOption[]
|
||||
currentValue: string | null
|
||||
isLoading: boolean
|
||||
error: string | null
|
||||
} {
|
||||
const enabled = Boolean(args.enabled && args.api && args.sessionId)
|
||||
const query = useQuery({
|
||||
queryKey: args.sessionId
|
||||
? queryKeys.sessionGrokReasoningEffortOptions(args.sessionId)
|
||||
: ['session-grok-reasoning-effort-options', 'unknown'] as const,
|
||||
queryFn: async () => {
|
||||
if (!args.api || !args.sessionId) throw new Error('Grok session unavailable')
|
||||
return await args.api.getSessionGrokReasoningEffortOptions(args.sessionId)
|
||||
},
|
||||
enabled,
|
||||
staleTime: 30_000,
|
||||
retry: false,
|
||||
})
|
||||
|
||||
return {
|
||||
options: query.data?.options ?? [],
|
||||
currentValue: query.data?.currentValue ?? null,
|
||||
isLoading: query.isLoading,
|
||||
error: query.data?.success === false
|
||||
? (query.data.error ?? 'Failed to load Grok effort options')
|
||||
: query.error instanceof Error ? query.error.message : null,
|
||||
}
|
||||
}
|
||||
@@ -243,6 +243,8 @@ export default {
|
||||
'newSession.yolo': 'YOLO mode',
|
||||
'newSession.yolo.title': 'Bypass approvals and sandbox',
|
||||
'newSession.yolo.desc': 'Uses dangerous agent flags when spawning.',
|
||||
'newSession.grokAutoUnavailable': 'unavailable',
|
||||
'newSession.grokAutoUnavailableDesc': 'Grok did not enable Auto permissions for this account or CLI build.',
|
||||
'newSession.create': 'Create',
|
||||
'newSession.creating': 'Creating…',
|
||||
|
||||
|
||||
@@ -247,6 +247,8 @@ export default {
|
||||
'newSession.yolo': 'YOLO 模式',
|
||||
'newSession.yolo.title': '跳过审批和沙箱',
|
||||
'newSession.yolo.desc': '启动时使用危险的代理标志。',
|
||||
'newSession.grokAutoUnavailable': '不可用',
|
||||
'newSession.grokAutoUnavailableDesc': 'Grok 尚未为此账号或 CLI 版本启用自动权限模式。',
|
||||
'newSession.create': '创建',
|
||||
'newSession.creating': '创建中…',
|
||||
|
||||
|
||||
@@ -22,5 +22,8 @@ export const queryKeys = {
|
||||
sessionOpencodeModels: (sessionId: string) => ['session-opencode-models', sessionId] as const,
|
||||
sessionOpencodeReasoningEffortOptions: (sessionId: string) => ['session-opencode-reasoning-effort-options', sessionId] as const,
|
||||
machineOpencodeModelsForCwd: (machineId: string, cwd: string) => ['machine-opencode-models', machineId, cwd] as const,
|
||||
machineGrokModelsForCwd: (machineId: string, cwd: string) => ['machine-grok-models', machineId, cwd] as const,
|
||||
sessionGrokModels: (sessionId: string) => ['session-grok-models', sessionId] as const,
|
||||
sessionGrokReasoningEffortOptions: (sessionId: string) => ['session-grok-reasoning-effort-options', sessionId] as const,
|
||||
skills: (sessionId: string) => ['skills', sessionId] as const,
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ export function resolveAgentSessionIdFromMetadata(
|
||||
case 'codex': return metadata.codexSessionId ?? undefined
|
||||
case 'gemini': return metadata.geminiSessionId ?? undefined
|
||||
case 'opencode': return metadata.opencodeSessionId ?? undefined
|
||||
case 'grok': return metadata.grokSessionId ?? undefined
|
||||
case 'cursor': return metadata.cursorSessionId ?? undefined
|
||||
case 'kimi': return metadata.kimiSessionId ?? undefined
|
||||
case 'pi': return metadata.piSessionId ?? undefined
|
||||
|
||||
@@ -18,6 +18,10 @@ export type {
|
||||
DirectoryEntry,
|
||||
FileReadResponse,
|
||||
GitCommandResponse,
|
||||
GrokModelsResponse,
|
||||
GrokModelSummary,
|
||||
GrokReasoningEffortResponse,
|
||||
GrokReasoningEffortOption,
|
||||
ListDirectoryResponse,
|
||||
MachineDirectoryEntry,
|
||||
MachineListDirectoryResponse,
|
||||
|
||||
Reference in New Issue
Block a user