Skip to content

Commit aa3d12a

Browse files
committed
feat(ui): remove sidebar usage meters and add context tooltip
1 parent 2c87f2a commit aa3d12a

File tree

7 files changed

+312
-40
lines changed

7 files changed

+312
-40
lines changed
Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
---
2+
shaping: true
3+
---
4+
5+
# Multi-Provider Sessions — Shaping
6+
7+
Handle session usage tracking when users can switch between API providers (Anthropic, OpenAI, etc.) and account types (API keys vs OAuth plans).
8+
9+
---
10+
11+
## Frame
12+
13+
### Source
14+
15+
> how should we handle the session feature with opencode? with codex it is a single provider. with opencode users could be using api or various different providers.
16+
17+
Screenshot shows "Session · Resets 2 hours" and "Weekly · Resets 4 days" usage meters at the bottom of the UI.
18+
19+
### Problem
20+
21+
Codex assumes a single provider (OpenAI) with a fixed rate limit structure (session + weekly windows). OpenCode supports multiple providers (Anthropic, OpenAI, OpenRouter, etc.) with different billing models:
22+
23+
1. **OAuth-based plans** (e.g., Anthropic Max, OpenAI ChatGPT) — have rate limit windows (session/weekly)
24+
2. **API keys** — pay-per-use, no rate limits, just credit balance
25+
3. **Self-hosted** — no limits at all
26+
27+
The current UI shows "Session" and "Weekly" meters assuming everyone has time-based rate limits. This is incorrect for API key users and multi-provider setups.
28+
29+
### Outcome
30+
31+
Usage display that accurately reflects the user's actual billing model per provider. API key users see spend/credits. OAuth users see rate limits. Multi-provider users see usage for their active provider.
32+
33+
---
34+
35+
## Requirements (R)
36+
37+
| ID | Requirement | Status |
38+
|----|-------------|--------|
39+
| R0 | Show usage information relevant to the user's current billing model | Core goal |
40+
| R1 | API key users see credit balance or spend, not rate limit windows | Must-have |
41+
| R2 | OAuth/plan users see rate limit windows (session/weekly) when available | Must-have |
42+
| R3 | Usage display updates when user switches providers or models | Must-have |
43+
| R4 | No misleading UI — don't show "Session: 0%" to API key users | Must-have |
44+
| R5 | Support providers that don't expose usage data at all (graceful degradation) | Must-have |
45+
| R6 | Usage is scoped to the active provider, not aggregated across all providers | Undecided |
46+
| R7 | Token usage per-turn/per-thread still works regardless of billing model | Must-have |
47+
| R8 | Local usage tracking (JSONL scanning) continues to work for historical view | Nice-to-have |
48+
49+
---
50+
51+
## Shapes
52+
53+
### CURRENT: Single-provider rate limits
54+
55+
| Part | Mechanism |
56+
|------|-----------|
57+
| **CUR1** | `RateLimitSnapshot` has `primary` (session) and `secondary` (weekly) windows |
58+
| **CUR2** | `usageLabels.ts` computes percentages assuming both windows exist |
59+
| **CUR3** | UI always shows "Session" and "Weekly" meters |
60+
| **CUR4** | `account/rateLimits/updated` event pushes updates |
61+
| **CUR5** | `AccountSnapshot.type` distinguishes `chatgpt` vs `apikey` but UI treats them the same |
62+
| **CUR6** | `CreditsSnapshot` exists but is secondary to rate limits |
63+
64+
### A: Provider-aware usage display
65+
66+
The usage display adapts based on the active provider's billing model. Rate limit windows shown for OAuth plans, credit balance for API keys, nothing for self-hosted.
67+
68+
| Part | Mechanism |
69+
|------|-----------|
70+
| **A1** | `AccountSnapshot` gains `billingModel: "rate_limited" | "pay_per_use" | "unlimited"` derived from provider auth method |
71+
| **A2** | `usageLabels.ts` returns different label shapes based on billing model |
72+
| **A3** | UI conditionally renders rate limit meters OR credit balance OR nothing |
73+
| **A4** | When user switches provider/model, re-fetch usage info for new provider |
74+
| **A5** | `RateLimitSnapshot` becomes optional — null for pay-per-use and unlimited |
75+
| **A6** | `CreditsSnapshot` expanded to show balance, spend-this-session, and cost-per-token hints |
76+
77+
### B: Unified usage abstraction
78+
79+
Abstract all billing models into a single "usage" concept that the UI displays uniformly.
80+
81+
| Part | Mechanism |
82+
|------|-----------|
83+
| **B1** | Normalize all billing models into `UsageSnapshot { percent?: number, label: string, sublabel?: string }` |
84+
| **B2** | Rate limits → percent; API credits → "X credits remaining"; Unlimited → "Unlimited" |
85+
| **B3** | Single meter component that displays whatever the backend provides |
86+
| **B4** | Backend computes the normalized snapshot, frontend just renders |
87+
88+
---
89+
90+
## Open Questions
91+
92+
| # | Question | Status |
93+
|---|----------|--------|
94+
| Q1 | Does OpenCode's REST API expose rate limits and/or credit balance per provider? | Needs spike |
95+
| Q2 | Which providers have rate limits vs pay-per-use vs neither? | Needs spike |
96+
| Q3 | Should we show usage for all connected providers or just the active one? | Undecided |
97+
| Q4 | How does OpenCode report usage for API key providers? | Needs spike |
98+
99+
---
100+
101+
## Spike: OpenCode Usage API
102+
103+
### Context
104+
105+
We need to understand what usage information OpenCode exposes per provider before we can design the UI.
106+
107+
### Questions and Answers
108+
109+
| # | Question | Answer |
110+
|---|----------|--------|
111+
| **S1-Q1** | What does `GET /provider` return? Does it include rate limits or credit info? | No. Returns `{ all: Provider[], default: {...}, connected: string[] }`. Provider includes models and auth methods but no usage/rate data. |
112+
| **S1-Q2** | What does `AccountSnapshot` look like in OpenCode's type system? | Not exposed. The OpenCode REST API has no account/usage endpoint. |
113+
| **S1-Q3** | Is there a per-provider or per-auth-method usage endpoint? | No. The OpenCode server API has no rate limit, credits, or billing endpoints. |
114+
| **S1-Q4** | What events does OpenCode emit for usage updates? | Only per-message token counts via `message.updated` events. No rate limit or account-level usage events. |
115+
116+
### Finding
117+
118+
**OpenCode's REST API does not expose rate limit or credit information.** This is fundamentally different from Codex, which had account-level rate limit data.
119+
120+
What OpenCode DOES provide:
121+
- Per-message token usage (`message.updated``tokens: { input, output, cache, reasoning }`)
122+
- Provider/model list with auth method types (`GET /provider/auth`)
123+
- Local session logs that can be scanned for historical token usage
124+
125+
What OpenCode does NOT provide:
126+
- Rate limit windows (session/weekly)
127+
- Credit balances
128+
- Account billing status
129+
- Provider-specific usage quotas
130+
131+
### Implication
132+
133+
The "Session: X%" / "Weekly: X%" UI as designed for Codex **cannot be implemented with OpenCode** because the underlying data doesn't exist. We need a different approach.
134+
135+
---
136+
137+
## Revised Requirements
138+
139+
Based on spike findings, requirements need updating:
140+
141+
| ID | Requirement | Status |
142+
|----|-------------|--------|
143+
| R0 | Show usage information relevant to the user's current session | Core goal |
144+
| R1 | ~~API key users see credit balance~~ **Dropped** — OpenCode doesn't expose this | Out |
145+
| R2 | ~~OAuth/plan users see rate limit windows~~ **Dropped** — OpenCode doesn't expose this | Out |
146+
| R3 | 🟡 Usage display reflects token consumption this session | Must-have |
147+
| R4 | No misleading UI — don't show rate limit meters that have no backing data | Must-have |
148+
| R5 | Support providers that don't expose usage data (graceful degradation) | Must-have |
149+
| R6 | 🟡 Per-thread token usage continues to work (already implemented) | Must-have |
150+
| R7 | 🟡 Local usage tracking shows historical token consumption | Nice-to-have |
151+
| R8 | 🟡 Model context window shown relative to current token usage | Nice-to-have |
152+
153+
---
154+
155+
## Revised Shapes
156+
157+
### C: Token-based usage display (no rate limits)
158+
159+
Since OpenCode doesn't expose rate limits, replace the Session/Weekly meters with token-based metrics derived from what we actually have.
160+
161+
| Part | Mechanism |
162+
|------|-----------|
163+
| **C1** | Remove `RateLimitSnapshot` fetching — it's always empty |
164+
| **C2** | Primary metric: tokens used this thread (already have via `thread/tokenUsage/updated`) |
165+
| **C3** | Secondary metric: context window usage percent (tokens / model.limit.context) |
166+
| **C4** | Show "X / Y tokens" or "X% context" instead of "Session: X%" |
167+
| **C5** | Remove "Resets in X hours" — no reset concept for API usage |
168+
| **C6** | Local usage view shows historical token consumption (existing `LocalUsageSnapshot`) |
169+
170+
### D: Hybrid — preserve UI if provider exposes limits
171+
172+
If OpenCode adds rate limit support in the future (or if we detect specific providers that have it), conditionally show the old UI.
173+
174+
| Part | Mechanism | Flag |
175+
|------|-----------|:----:|
176+
| **D1** | Attempt to fetch rate limits via hypothetical endpoint | ⚠️ |
177+
| **D2** | If rate limits exist, show Session/Weekly meters (CURRENT behavior) | |
178+
| **D3** | If no rate limits, fall back to Shape C token display | |
179+
| **D4** | Provider detection: check auth method type to predict billing model | ⚠️ |
180+
181+
---
182+
183+
## Fit Check
184+
185+
| Req | Requirement | Status | CURRENT | C | D |
186+
|-----|-------------|--------|:-------:|:-:|:-:|
187+
| R0 | Show usage information relevant to the user's current session | Core goal ||||
188+
| R3 | Usage display reflects token consumption this session | Must-have ||||
189+
| R4 | No misleading UI — don't show empty rate limit meters | Must-have ||||
190+
| R5 | Support providers with no usage data (graceful degradation) | Must-have ||||
191+
| R6 | Per-thread token usage continues to work | Must-have ||||
192+
| R7 | Local usage tracking shows historical token consumption | Nice-to-have ||||
193+
| R8 | Model context window shown relative to current token usage | Nice-to-have ||||
194+
195+
**Notes:**
196+
- CURRENT fails R0/R4: Shows "Session: 0%" and "Weekly: 0%" with "Resets X hours" which is misleading — there's no rate limit data backing it
197+
- D has flagged unknowns (D1, D4) — depends on OpenCode adding rate limit endpoints in the future
198+
- C is fully implementable with current OpenCode capabilities
199+
200+
**Recommendation:** Shape C is the pragmatic choice. It uses data we actually have and provides meaningful usage feedback.
201+
202+
---
203+
204+
## Shape C Detail
205+
206+
### Reference: OpenCode TUI
207+
208+
Screenshot shows OpenCode's native UI displays:
209+
```
210+
Context
211+
68,107 tokens
212+
17% used
213+
$0.00 spent
214+
```
215+
216+
### What our UI would show
217+
218+
**Current (misleading):**
219+
```
220+
Session · Resets 2 hours 0%
221+
Weekly · Resets 4 days 14%
222+
```
223+
224+
**Proposed (Shape C) — match OpenCode UI:**
225+
```
226+
Context
227+
68,107 tokens · 17% used
228+
```
229+
230+
Or compact version for status bar:
231+
```
232+
68.1k tokens · 17%
233+
```
234+
235+
The "$0.00 spent" line is omitted — OpenCode calculates this client-side from token counts × model pricing, but this pricing data isn't exposed via REST API.
236+
237+
### Parts Breakdown
238+
239+
| Part | Mechanism |
240+
|------|-----------|
241+
| **C1** | `usageLabels.ts``getContextUsageLabels()` takes `ThreadTokenUsage` |
242+
| **C2** | Compute `contextPercent = (total.totalTokens / modelContextWindow) * 100` |
243+
| **C3** | Format tokens with locale separator: `68,107 tokens` |
244+
| **C4** | Show percent used when `modelContextWindow` is known |
245+
| **C5** | Remove rate limit meters, replace with context display |
246+
| **C6** | Keep `LocalUsageSnapshot` for historical view |
247+
248+
### Migration Path
249+
250+
1. Remove `RateLimitSnapshot` from thread state
251+
2. Remove `account_rate_limits_core` stub and related event handling
252+
3. Update `ComposerMetaBar` to show context usage instead of rate limits
253+
4. Add context window percentage computation
254+
5. Update `usageLabels.ts` with new formatting functions
255+
256+
---
257+
258+
## Open Questions
259+
260+
| # | Question | Status |
261+
|---|----------|--------|
262+
| Q1 | Should we show cumulative tokens across all threads or just active thread? | **Active thread only** — matches OpenCode TUI |
263+
| Q2 | How prominent should context window usage be? | **Primary display** — matches OpenCode TUI |
264+
| Q3 | Should we surface local historical usage (last 7 days) in the status bar? | **No** — keep footer simple, historical view accessible elsewhere |
265+
266+
---
267+
268+
## Slices
269+
270+
| # | Slice | Demo | Status |
271+
|---|-------|------|--------|
272+
| V1 | Remove SidebarFooter usage display, add token tooltip to ComposerMetaBar | Hover context ring → "68,107 of 200,000 tokens" | **Done** |
273+
| V2 | Remove rate limit infrastructure | N/A | **Skipped** |
274+
275+
### V1: Final Implementation
276+
277+
**Approach changed:** Instead of replacing the sidebar footer display, we removed it entirely. Context info is already displayed in the `ComposerMetaBar` ("Context free 30%"). Added token count tooltip on hover.
278+
279+
**Changes:**
280+
281+
| File | Change |
282+
|------|--------|
283+
| `src/features/app/components/Sidebar.tsx` | Removed SidebarFooter, removed `activeTokenUsage` prop |
284+
| `src/features/layout/hooks/layoutNodes/buildPrimaryNodes.tsx` | Removed `activeTokenUsage` prop |
285+
| `src/features/layout/hooks/layoutNodes/types.ts` | Moved `activeTokenUsage` to keep single definition |
286+
| `src/features/composer/components/ComposerMetaBar.tsx` | Added `contextTooltip` showing token counts on hover |
287+
| `src/App.tsx` | Cleaned up unused rate limit references |
288+
| `src/features/app/components/SidebarFooter.tsx` | Reverted to original (unused, kept for upstream compat) |
289+
| `src/features/app/utils/usageLabels.ts` | Reverted to original (kept for upstream compat) |
290+
291+
**UI Result:**
292+
- Sidebar footer: **removed** (no misleading rate limit display)
293+
- ComposerMetaBar: Shows "Context free 30%" with tooltip "68,107 of 200,000 tokens" on hover
294+
295+
### V2 Decision: Skipped
296+
297+
**Rationale:** Keep rate limit infrastructure dormant to minimize upstream merge conflicts.
298+
299+
- Upstream CodexMonitor uses rate limits (OpenAI/Codex has them)
300+
- Removing creates conflicts in reducer, types, hooks across many files
301+
- Dead code costs nothing at runtime
302+
- If OpenCode adds rate limit APIs later, infrastructure is ready
303+
304+
**Demo:** App still works, no rate limit fetching or state, cleaner codebase.

src/App.tsx

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -512,7 +512,6 @@ function MainApp() {
512512
threadListCursorByWorkspace,
513513
activeTurnIdByThread,
514514
tokenUsageByThread,
515-
rateLimitsByWorkspace,
516515
accountByWorkspace,
517516
planByThread,
518517
lastAgentMessageByThread,
@@ -1112,9 +1111,6 @@ function MainApp() {
11121111
getWorkspaceGroupName,
11131112
});
11141113

1115-
const activeRateLimits = activeWorkspaceId
1116-
? rateLimitsByWorkspace[activeWorkspaceId] ?? null
1117-
: null;
11181114
const activeTokenUsage = activeThreadId
11191115
? tokenUsageByThread[activeThreadId] ?? null
11201116
: null;
@@ -1735,8 +1731,7 @@ function MainApp() {
17351731
activeWorkspaceId,
17361732
activeThreadId,
17371733
activeItems,
1738-
activeRateLimits,
1739-
usageShowRemaining: appSettings.usageShowRemaining,
1734+
activeTokenUsage,
17401735
accountInfo: activeAccount,
17411736
onSwitchAccount: handleSwitchAccount,
17421737
onCancelSwitchAccount: handleCancelSwitchAccount,
@@ -2059,7 +2054,6 @@ function MainApp() {
20592054
onReviewPromptConfirmCommit: confirmCommit,
20602055
onReviewPromptUpdateCustomInstructions: updateCustomInstructions,
20612056
onReviewPromptConfirmCustom: confirmCustom,
2062-
activeTokenUsage,
20632057
activeQueue,
20642058
draftText: activeDraft,
20652059
onDraftChange: handleDraftChange,

src/features/app/components/Sidebar.test.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,6 @@ const baseProps = {
2929
onRefreshAllThreads: vi.fn(),
3030
activeWorkspaceId: null,
3131
activeThreadId: null,
32-
accountRateLimits: null,
33-
usageShowRemaining: false,
3432
accountInfo: null,
3533
onSwitchAccount: vi.fn(),
3634
onCancelSwitchAccount: vi.fn(),

0 commit comments

Comments
 (0)