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:
SSU-WEI HUANG
2026-05-27 11:17:24 +08:00
committed by GitHub
co-authored by Cursor
parent d5a67b717c
commit 6f2bb7d32b
43 changed files with 1263 additions and 68 deletions
+36
View File
@@ -641,6 +641,42 @@ describe('normalizeDecryptedMessage', () => {
})
})
it('normalizes token_count payloads with explicit contextTokens', () => {
const message = makeMessage({
role: 'agent',
content: {
type: 'codex',
data: {
type: 'token_count',
info: {
total: {
inputTokens: 8_119,
outputTokens: 2,
cachedInputTokens: 5_760,
thoughtTokens: 11,
totalTokens: 13_892
},
contextTokens: 13_879,
modelContextWindow: 65_536
}
}
}
})
const normalized = normalizeDecryptedMessage(message)
expect(normalized).toMatchObject({
role: 'event',
usage: {
input_tokens: 8119,
output_tokens: 2,
cache_read_input_tokens: 5760,
context_tokens: 13879,
context_window: 65536
}
})
})
it('normalizes Codex context_compacted as a compact event', () => {
const message = makeMessage({
role: 'agent',
+6 -1
View File
@@ -88,7 +88,12 @@ function normalizeCodexTokenUsage(value: unknown, data?: Record<string, unknown>
?? usageSource.cacheReadInputTokens
?? usageSource.cache_read_input_tokens
) ?? undefined,
context_tokens: inputTokens,
context_tokens: asNumber(
info.contextTokens
?? info.context_tokens
?? usageSource.contextTokens
?? usageSource.context_tokens
) ?? inputTokens,
context_window: asNumber(info.modelContextWindow ?? info.model_context_window) ?? undefined,
thread_id: asString(
data?.thread_id