fix(opencode): use ACP-reported reasoning effort options (#853)

* fix(opencode): use ACP-reported reasoning effort options

Expose thought_level options from OpenCode ACP to the web UI via RPC/API
instead of hardcoded presets, and validate effort values before setConfigOption.

Fixes #852

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(opencode): sync hub effort after coerced setConfigOption

When resolveThoughtLevelEffort falls back to a different supported value,
roll back session state after a successful ACP update so keepalive and the
web UI do not keep advertising the rejected effort.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
SSU-WEI HUANG
2026-06-10 13:57:03 +08:00
committed by GitHub
co-authored by Cursor
parent 3473a88d67
commit cad58cfa0b
18 changed files with 486 additions and 35 deletions
@@ -0,0 +1,24 @@
import { describe, expect, it } from 'vitest'
import { getOpencodeReasoningEffortRefetchInterval, shouldRetryOpencodeReasoningEffortQuery } from './useOpencodeReasoningEffortOptions'
describe('useOpencodeReasoningEffortOptions retry policy', () => {
it('retries transient failures up to three times', () => {
expect(shouldRetryOpencodeReasoningEffortQuery(0)).toBe(true)
expect(shouldRetryOpencodeReasoningEffortQuery(2)).toBe(true)
expect(shouldRetryOpencodeReasoningEffortQuery(3)).toBe(false)
})
it('polls until options are available', () => {
expect(getOpencodeReasoningEffortRefetchInterval(true, undefined, 0)).toBe(1000)
expect(getOpencodeReasoningEffortRefetchInterval(true, { success: false, error: 'not ready' }, 2)).toBe(1000)
expect(getOpencodeReasoningEffortRefetchInterval(true, {
success: true,
options: [{ value: 'low', name: 'Low' }]
}, 1)).toBe(false)
})
it('stops polling when disabled or after the max poll count', () => {
expect(getOpencodeReasoningEffortRefetchInterval(false, undefined, 0)).toBe(false)
expect(getOpencodeReasoningEffortRefetchInterval(true, undefined, 10)).toBe(false)
})
})
@@ -0,0 +1,77 @@
import { useQuery } from '@tanstack/react-query'
import type { OpencodeReasoningEffortResponse } from '@hapi/protocol/apiTypes'
import type { ApiClient } from '@/api/client'
import { queryKeys } from '@/lib/query-keys'
export function shouldRetryOpencodeReasoningEffortQuery(failureCount: number): boolean {
return failureCount < 3
}
const MAX_OPENCODE_REASONING_EFFORT_DISCOVERY_POLLS = 10
export function getOpencodeReasoningEffortRefetchInterval(
enabled: boolean,
data: OpencodeReasoningEffortResponse | undefined,
pollCount: number
): 1000 | false {
if (!enabled || pollCount >= MAX_OPENCODE_REASONING_EFFORT_DISCOVERY_POLLS) {
return false
}
if (!data) {
return 1000
}
if (data.success === false) {
return 1000
}
return (data.options?.length ?? 0) > 0 ? false : 1000
}
export function useOpencodeReasoningEffortOptions(args: {
api: ApiClient | null
sessionId?: string | null
enabled?: boolean
}): {
options: Array<{ value: string; name?: string }>
currentValue: string | null
isLoading: boolean
error: string | null
} {
const { api, sessionId } = args
const enabled = Boolean(args.enabled && api && sessionId)
const query = useQuery({
queryKey: sessionId
? queryKeys.sessionOpencodeReasoningEffortOptions(sessionId)
: ['session-opencode-reasoning-effort-options', 'unknown'] as const,
queryFn: async () => {
if (!api) {
throw new Error('API unavailable')
}
if (!sessionId) {
throw new Error('OpenCode reasoning effort target unavailable')
}
return await api.getSessionOpencodeReasoningEffortOptions(sessionId)
},
enabled,
staleTime: 30_000,
retry: (failureCount) => shouldRetryOpencodeReasoningEffortQuery(failureCount),
refetchInterval: (query) => getOpencodeReasoningEffortRefetchInterval(
enabled,
query.state.data as OpencodeReasoningEffortResponse | undefined,
query.state.dataUpdateCount + query.state.errorUpdateCount
),
})
return {
options: query.data?.options ?? [],
currentValue: query.data?.currentValue ?? null,
isLoading: query.isLoading,
error: query.data?.success === false
? (query.data.error ?? 'Failed to load OpenCode reasoning effort options')
: query.error instanceof Error
? query.error.message
: query.error
? 'Failed to load OpenCode reasoning effort options'
: null,
}
}