mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
feat(opencode): add plan mode, reasoning effort, and status telemetry (#688)
* feat(opencode): support plan mode * feat(opencode): support reasoning effort * feat(opencode): surface context usage in web Bridge OpenCode ACP usage updates into the existing token-count pipeline so the web status bar can show live context and cache information without a separate UI path. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(opencode): restrict plan mode to remote, rollback reasoning effort on failure - Block local OpenCode plan startup (tools not enforced in local path) - Allow remote OpenCode plan only (ACP permission handler denies tools) - Guard web /permission-mode endpoint for local OpenCode plan sessions - Rollback session reasoning effort when OpenCode rejects set_config_option - Wire rollback callback through opencodeLoop to runOpencode closure - Add tests: local plan rejected, remote plan allowed, web guard, effort rollback * fix(web): auto-retry OpenCode models query to populate model selector without refresh - Retry early failures (RPC may still be registering on new sessions) - Poll briefly until availableModels is non-empty - Stop polling once model options are discovered - Add tests for retry/poll/stop policy * fix(opencode): cap model discovery polling --------- Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -74,16 +74,32 @@ describe('listOpencodeModelsForCwd', () => {
|
||||
expect(closeMock).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('returns empty availableModels when session/new omits the models block', async () => {
|
||||
it('reads availableModels from configOptions when session/new omits the models block', async () => {
|
||||
sendRequestMock
|
||||
.mockResolvedValueOnce({ protocolVersion: 1 })
|
||||
.mockResolvedValueOnce({ sessionId: 'sess-2' })
|
||||
.mockResolvedValueOnce({
|
||||
sessionId: 'sess-2',
|
||||
configOptions: [
|
||||
{
|
||||
id: 'model',
|
||||
category: 'model',
|
||||
currentValue: 'opencode/big-pickle',
|
||||
options: [
|
||||
{ value: 'opencode/big-pickle', name: 'OpenCode Zen/Big Pickle' },
|
||||
{ value: 'deepseek/deepseek-chat', name: 'DeepSeek/DeepSeek Chat' }
|
||||
]
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const result = await listOpencodeModelsForCwd('/tmp/proj')
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.availableModels).toEqual([])
|
||||
expect(result.currentModelId).toBeNull()
|
||||
expect(result.availableModels).toEqual([
|
||||
{ modelId: 'opencode/big-pickle', name: 'OpenCode Zen/Big Pickle' },
|
||||
{ modelId: 'deepseek/deepseek-chat', name: 'DeepSeek/DeepSeek Chat' }
|
||||
])
|
||||
expect(result.currentModelId).toBe('opencode/big-pickle')
|
||||
})
|
||||
|
||||
it('reads availableModels from top-level fields too (alternate response shape)', async () => {
|
||||
|
||||
@@ -25,7 +25,7 @@ function normalizeAvailableModels(rawModels: unknown): OpencodeModelSummary[] {
|
||||
const out: OpencodeModelSummary[] = [];
|
||||
for (const entry of rawModels) {
|
||||
if (!isObject(entry)) continue;
|
||||
const modelId = asString(entry.modelId);
|
||||
const modelId = asString(entry.modelId) ?? asString(entry.value);
|
||||
if (!modelId) continue;
|
||||
const name = asString(entry.name) ?? undefined;
|
||||
out.push(name ? { modelId, name } : { modelId });
|
||||
@@ -33,6 +33,24 @@ function normalizeAvailableModels(rawModels: unknown): OpencodeModelSummary[] {
|
||||
return out;
|
||||
}
|
||||
|
||||
function extractModelConfigOption(response: Record<string, unknown>): {
|
||||
currentValue: string | null;
|
||||
options: unknown[];
|
||||
} | null {
|
||||
if (!Array.isArray(response.configOptions)) return null;
|
||||
|
||||
for (const entry of response.configOptions) {
|
||||
if (!isObject(entry)) continue;
|
||||
if (asString(entry.category) !== 'model') continue;
|
||||
return {
|
||||
currentValue: asString(entry.currentValue),
|
||||
options: Array.isArray(entry.options) ? entry.options : []
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function extractModelsFromResponse(response: unknown): {
|
||||
availableModels: OpencodeModelSummary[];
|
||||
currentModelId: string | null;
|
||||
@@ -47,16 +65,17 @@ function extractModelsFromResponse(response: unknown): {
|
||||
const nestedList = nested?.availableModels;
|
||||
const nestedCurrent = nested?.currentModelId;
|
||||
|
||||
const configModelOption = extractModelConfigOption(response);
|
||||
const rawModels = Array.isArray(directList)
|
||||
? directList
|
||||
: Array.isArray(nestedList)
|
||||
? nestedList
|
||||
: null;
|
||||
: configModelOption?.options ?? null;
|
||||
const rawCurrent = typeof directCurrent === 'string'
|
||||
? directCurrent
|
||||
: typeof nestedCurrent === 'string'
|
||||
? nestedCurrent
|
||||
: null;
|
||||
: configModelOption?.currentValue ?? null;
|
||||
|
||||
return {
|
||||
availableModels: normalizeAvailableModels(rawModels),
|
||||
|
||||
Reference in New Issue
Block a user