Repository navigation
feat(opencode): quota sidebar on OpenCode 2 - #395
Conversation
There was a problem hiding this comment.
7 issues found across 19 files
Confidence score: 3/5
- In
claude-sidebar.tsx, strippinglastUpdatedbefore change detection can freeze dynamic quota values after an unchanged response. Compare the full response or trigger a render tick. - In
v2/sidebar.ts, disabled vault accounts can disappear from the sidebar even when they need sign-in. Retain vault rows and populate their custody state, as OpenCode 1 does. - In
claude-sidebar.tsx, a failed slot registration can leave the preference watcher running because cleanup is only returned after registration succeeds. Dispose the controller in the registration failure path. - The sidebar tests miss important coverage:
opencode2-sidebar-render.test.tsdoes not check that rendered quota state survives a later polling error, andopencode2-sidebar.test.tsdoes not exercise enabled hybrid cache-keep. Add those cases so regressions in either path are caught.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. When an issue isn't valid or won't be fixed in this PR, reply in its thread with the reason and then resolve the thread. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/opencode/src/tui/claude-sidebar.tsx">
<violation number="1" location="packages/opencode/src/tui/claude-sidebar.tsx:954">
P2: The OpenCode 2 sidebar freezes dynamic quota values after the first unchanged response because it strips `lastUpdated` before change detection. Compare the full response, or add a render tick, so reset countdowns and pacing refresh at the configured poll interval.</violation>
<violation number="2" location="packages/opencode/src/tui/claude-sidebar.tsx:989">
P3: A failed OpenCode 2 slot registration leaks the preference watcher created by `createSidebarController`, because cleanup is returned only after registration succeeds. Dispose the controller in a catch around `host.ui.slot` before rethrowing the mount error.</violation>
</file>
<file name="packages/opencode/src/v2/sidebar.ts">
<violation number="1" location="packages/opencode/src/v2/sidebar.ts:69">
P2: This filter drops disabled vault accounts, so a vault credential needing sign-in can disappear from the OpenCode 2 sidebar. Retain vault rows using the OpenCode 1 rule and populate the corresponding custody state so the shared renderer can display them.</violation>
</file>
<file name="packages/opencode/src/tests/opencode2-sidebar-render.test.ts">
<violation number="1" location="packages/opencode/src/tests/opencode2-sidebar-render.test.ts:167">
P2: This fallback test only verifies the initial empty state when `sidebar` is unavailable, not preservation of an already-rendered quota state after a later polling error. Add a probe response sequence that returns valid state first, then rejects subsequent calls, and assert the quota remains visible.</violation>
</file>
<file name="packages/opencode/src/tests/opencode2-sidebar.test.ts">
<violation number="1" location="packages/opencode/src/tests/opencode2-sidebar.test.ts:147">
P2: This test does not exercise the active cache-keep path or verify its activation flag. Add `claudeCache: { enabled: true, mode: 'hybrid' }` to the fixture and assert `state.cacheKeep?.enabled` is `true`, otherwise a regression that disables cache keepalive can pass unnoticed.</violation>
</file>
<file name="STRUCTURE.md">
<violation number="1" location="STRUCTURE.md:80">
P3: This description omits the raw TSX fallback used by older hosts and bare Bun. Document the compiled path as conditional so the structure guide accurately reflects supported loading behavior.</violation>
</file>
<file name="docs/sidebar.md">
<violation number="1" location="docs/sidebar.md:7">
P2: This overstates the RPC privacy guarantee: the sidebar response includes local account IDs and labels (`fallbacks[].id`, `fallbacks[].label`, and `activeId`) so the renderer can identify accounts. Say that it excludes provider account identities and credential material instead of claiming it contains no account identities.</violation>
</file>
Architecture diagram
sequenceDiagram
participant UI as OpenCode 2 TUI
participant Entry as tui/entry.mjs
participant Setup as setupAnthropicTui
participant Mount as mountClaudeSidebar
participant RPC as Native RPC
participant Server as v2/menu.ts
participant Source as Native Source
participant Snapshot as Account Snapshot
participant Builder as v2/sidebar.ts
participant Renderer as ClaudeSidebar
Note over UI,Entry: PR: OpenCode 2 quota sidebar
UI->>Entry: setup(context)
Entry->>Setup: setupAnthropicTui(ctx, {mountSidebar})
Setup->>Setup: Register /claude menu
Setup->>Mount: mountSidebar(ctx, read)
Note over Mount,Builder: NEW: Sidebar polling loop
loop pollMs per session
Mount->>RPC: rpc.sidebar({sessionId}, {signal})
RPC->>Server: registered sidebar handler
Server->>Server: readSidebar(sessionID)
Server->>Source: servedAccount(sessionID)
Source-->>Server: account ID or undefined
Server->>Snapshot: runtime.read()
Server->>Builder: buildNativeSidebarState(snapshot, extras)
Builder->>Builder: Filter API keys, project OAuth accounts
Builder-->>Server: SidebarState (no secrets)
Server-->>Mount: RPC response
Mount->>Renderer: Draw with theme colors
alt Valid state
Renderer-->>Mount: Render quota bars, routing, cache
else RPC method not found
Mount->>Renderer: Show "Waiting for quota…"
end
end
Note over Source: NEW: servedBy Map
Source->>Source: Track accountId per session on authorize
Source->>Source: Delete on forgetSession
Note over UI,Setup: Session lifecycle
UI->>Setup: Cleanup call
Setup->>Mount: releaseSidebar()
Mount-->>Setup: Release sidebar.content slot
Setup-->>UI: Return dispose
Reply with feedback, questions, or to request a fix.
View guided diff | Re-trigger cubic
There was a problem hiding this comment.
Thanks for this, the direction is right: one renderer for both OpenCode generations, an additive sidebar method on the existing menu RPC that fails closed to the empty state, and a TUI bundle that stays JSX-free. The build, typecheck, lint and your new tests pass on our side.
A few things need changing before we can merge:
- Countdowns and reading ages freeze.
HostSidebardropslastUpdatedbefore comparing answers, so an answer with the same values never updates, and reset countdowns, the reading age and stale dimming stop moving. Comparing the full answer instead would remount every row on each poll, which is the flicker in #379. We're fixing #379 on main first (collapsed rows keyed by position instead of by object); after that, please add a clock signal (anowthat ticks) that the time-based values read, rather than redrawing on every poll. A test that a countdown advances without a state change would cover it. - Turned-off vault accounts disappear.
v2/sidebar.tsfilters fallbacks onaccount.enabled. OpenCode 1 keeps vault rows when they're turned off and setscustodyState, which the shared renderer uses to show "turned off". Please keepsource === 'vault'rows and derivecustodyStatethe same way, with a test for a turned-off vault account. - Watcher leak. If
host.ui.slotthrows, the controller's watcher keeps running. Wrapping the call and disposing the controller on failure fixes it. - Docs wording.
docs/sidebar.mdsays the answer carries no account identities, but it does include local route ids and labels. "No tokens, keys, provider account identities or credential material" would be accurate.
Smaller points, optional:
refreshBackoffActivegets an undefined identity and fingerprint, so a stale backoff from a replaced login could still show; passingaccount.accountIdentityavoids that.- API-key routes set
servedBy, which marks main as active while an API key is serving (OpenCode 1 does the same today). - STRUCTURE.md could mention the TSX fallback.
Because the moved renderer also carries the #379 fix and the credit amounts we're restoring for #390, please rebase once those land on main; I'll comment here when they do. One test, opencode2-native-source.test.ts > inactive native vault admission bounds a stalled refresh at two seconds, timed out once when we ran it alongside other files; it's probably load, but please run that file alone on your branch.
|
The sidebar fixes this PR builds on are now on main as de983cd (#405): collapsed rows keyed by position (#379), credit amounts after the shared credit figure (#390), and a signed-out vault default shown once as main (#392). Please rebase onto main when you pick up the review changes; |
OpenCode 2 had no quota sidebar: the generation-2 TUI only registered /claude and toasts, so 5h/7d bars, pacing, routing and health were only visible on OpenCode 1. - the OpenCode 2 server answers a new `sidebar` method on the existing native RPC with the OpenCode 1 SidebarState, projected from the native account snapshot (v2/sidebar.ts); the output is normalized fail-closed and carries no token, key, receipt or account identity - the active account is per session: the native source records the account behind each session's latest authorization and forgets it with the session - the sidebar rendering moves unchanged from tui.tsx to tui/claude-sidebar.tsx, shared by both generations with the same tui-preferences.jsonc settings; tui.tsx keeps file polling and dialogs - on OpenCode 2, entry.mjs loads the precompiled renderer and setupAnthropicTui mounts it on sidebar.content with the host theme mapped to the flat tokens; it polls per session and shows "Waiting for quota" against servers without the method Docs and changelog updated. cortexkit#379 is unchanged and a fix in the shared renderer will cover both generations.
- Countdowns, reading ages, pacing and backoff times read a sidebar
clock that ticks at the poll interval and on each state change, so
they advance while the server answers the same state. Quota bar
segments are keyed by position so the tick does not remount them.
- Turned-off vault accounts stay listed with OpenCode 1's custody
state ("turned off"); vaultServed and vaultReauth follow it.
- A local account with an unsupported saved credential shows why it
cannot serve instead of looking ready.
- Refresh backoffs are matched to the account identity, so one left by
a replaced login no longer shows.
- The collapsed rows mark no account active before one has served the
session, matching the expanded view, in both generations.
- A failing slot claim disposes the preference watcher.
- Docs: the RPC answer carries route ids and labels but no tokens,
keys, provider account identities or credential material; STRUCTURE
names the raw TSX fallback. Changelog entry moved to Unreleased.
Tests: countdown advances without a state change, last quota kept
after failed polls, turned-off/cold/served vault rows, unsupported
credential, replaced-login backoff, cache keepalive enabled flag,
no active collapsed row before the first request.
816ef2e to
1f1b92b
Compare
|
@ualtinok rebased onto main (2.1.1) with the review changes in a separate commit, 1f1b92b, so the delta is easy to read. Ready for another look and, if it suits you, a merge. From the review:
Optional points: The #379/#390/#392 changes from de983cd are applied to the moved renderer ( Also handled: the bots' unsupported-credential state line, the collapsed view marking main active before the first request, the cache keepalive assertion and a "keeps the last quota after failed polls" test. OpenCode 2.1.0 (out today): nothing this PR uses changed. With the dev pin moved to |
# Conflicts: # CHANGELOG.md
…es watcher Setup keeps the /claude menu when the sidebar cannot mount. The test checks that the controller created for the refused slot is disposed: the child process must exit on its own, which a leaked preferences directory watcher prevents. Removing the dispose in mountClaudeSidebar makes it fail.
|
Merged, thank you. All four review changes were in, plus two of the optional ones, and the moved renderer kept the #379, #390 and #392 fixes. Two small things I did on top, so you know what changed in your branch:
It will go out in the next release. |
Why
On OpenCode 2 there is no quota sidebar:
docs/sidebar.mdsays the live sidebar is OpenCode 1 only, and the generation-2 TUI only registers/claudeand toasts. Users moving to OpenCode 2 lose the per-account 5h/7d bars, pacing, routing and health at a glance.What changes
Server (OpenCode 2)
sidebarmethod on the existingcortexkit-anthropic-nativeRPC (id and other methods unchanged). Output is validated withnormalizeSidebarState, so the boundary fails closed to the empty state.v2/sidebar.tsprojects theNativeAccountSnapshotto the sameSidebarStateOpenCode 1 reads, with OpenCode 1's field rules: state lines, tiers, quota/refresh backoffs (matched to the account identity), permanent-refresh re-sign-in, routing mode, fast mode, effective relay, cache keepalive and Prime. Vault rows stay listed when turned off and carry OpenCode 1'scustodyState, so the shared renderer shows "turned off". A local account whose saved credential is not a Claude OAuth token shows why it cannot serve. API-key accounts are not listed. The answer carries local route ids and labels, but no tokens, keys, provider account identities or credential material.source.tsrecords the account behind each session's latest authorization (servedAccount) and drops it inforgetSession, which already bounds sessions to 512.Shared renderer
tui.tsxtosrc/tui/claude-sidebar.tsx, with main's Collapsed sidebar flickers: account rows remount on every state update and draw at a 40-column fallback width first #379, 2.0.3 no longer shows the dollar amounts for a credit allowance (extra usage); only 'credits N% left' #390 and Vault default account appears twice in the sidebar while its sign-in has expired (primary pin deleted on needs_reauth) #392 changes carried over (collapsed rows keyed by position, credit amounts after the shared figure).tui.tsxkeeps file polling and the/claude-quotadialog and rendersClaudeSidebar. The preference controller now returns the watcher's dispose (OpenCode 1 still keeps it for the process).prefs.pollMsand on each state change, so they advance while the server answers the same state. Rows are not rebuilt: the clock only updates their text, and bar segments are keyed by position.anthropic-authentry oftui-preferences.jsonc.TUI (OpenCode 2)
mountClaudeSidebarclaimssidebar.content, maps OpenCode 2'sResolvedTheme(text.base/muted,text.feedback.*.base,text.action.primary.base,background.base,border.base) to the flat tokens, pollsrpc.sidebarper session atprefs.pollMswith an abort signal, redraws only when content changes, and keeps the last state on errors (an older server shows "Waiting for quota…"). If the slot claim throws, the preference watcher is disposed.build-tui.tsprecompilestui/claude-sidebar.tsx;entry.mjsloads it with the same runtime-registry probe as the OpenCode 1 loader (raw TSX fallback on older hosts and bare Bun) and passes it tosetupAnthropicTui, which stays JSX-free and does not load the OpenCode 1 TUI. A renderer that fails to load leaves/claudeworking.Rendered by the test through
entry.mjssetup()with an OpenCode 2-shaped context:OpenCode 2.1.0
Checked against
@opencode/pluginand@opencode/theme2.1.0 (released 2026-10-20). The plugin API changes since 2.0.22 (SessionCompactionResult.providerStaterenamednative,session.list, dialog selectsearch/actions) are not used here, and the TUI context still exposes the nestedResolvedThemeandui.slotthis PR reads. With the dev pin moved to 2.1.0 locally,typecheckpasses, and the packed real-host suite (opencode2-e2e.test.tswith@opencode/cli@2.1.0) passes its six OpenCode 2 cases. The pin itself stays at 2.0.22 in this PR; bumping it belongs with the dependency updates.Out of scope
mainactive while a key serves, as OpenCode 1 does today./claudedoes.Tests
opencode2-sidebar.test.ts: projection (accounts, API keys excluded, active per session, backoffs, permanent vs expired refresh failure, vault re-sign-in line), turned-off / cold / served vault rows with their custody state, unsupported saved credential, backoff from a replaced login ignored, policy fields including the cache keepalive enabled flag, RPC round trip and no secret, fail-closed output schema,registerNativeMenusidebar handler.opencode2-native-source.test.ts:servedAccountper session and afterforgetSession. The file passes alone (36 pass), including the two-second stalled-refresh bound.opencode2-sidebar-render.test.ts: loadssrc/tui/entry.mjssetup()in a Bun subprocess with OpenTUI's test renderer, a fake OpenCode 2 context and RPC: slot claimed, server quota drawn with theme colors asserted per span, slot released on cleanup; a server withoutsidebarshows "Waiting for quota…"; a countdown advances from2h59mto1h59mwhile the server keeps answering the same state (fails without the clock); the last quota stays drawn, and keeps counting down, after later polls fail.sidebar-state.test.ts: no collapsed row active before the first request.lint(one existing warning inlocal-routing-migration.test.ts),format:check,typecheck,buildandsmoke:tui(npm 11; npm 12 blocks@opencode/cli's postinstall and changednpm pack --json).bun test src/testsinpackages/opencode: 2282 pass, 8 fail. Five fail identically onmainhere (OC2 setup acceptance, S2 rerun, vault generation 2 x2, Pi auth path), the OpenCode 1 e2e case times out in this environment, and two timed out under load and pass alone (custody tombstone, first render case; the render cases now allow 20 s for the subprocess).Specified with OpenSpec (proposal, capability specs
sidebar-state-rpcandopencode2-sidebar, design, tasks); the artifacts are kept out of the repository.

Confidence Score: 5/5
The PR appears safe to merge; no new actionable defects were established.Summary
Adds the Claude quota sidebar to OpenCode 2 through the existing native RPC.
Diagram
Reviews (3) · Last reviewed commit: "test(opencode): a refused OpenCode 2 sid..." · Reviewed by Greptile