Skip to content

feat(opencode): colored, collapsible OpenCode 2 sidebar with real quota freshness - #42

Open
TodoniK wants to merge 1 commit into
cortexkit:mainfrom
TodoniK:feat/ga-sidebar-colors-and-freshness
Open

TodoniK wants to merge 1 commit into
cortexkit:mainfrom
TodoniK:feat/ga-sidebar-colors-and-freshness

Conversation

@TodoniK

@TodoniK TodoniK commented Oct 18, 2026 •

Copy link
Copy Markdown

Why

On OpenCode 2 the Antigravity sidebar is hard to read and partly wrong:

  • every row is plain text, although projectSidebar() already computes a tone per quota row and per status;
  • the footer always says "quota not checked yet", because both GA state sources hard-code status.checkedAt: null;
  • vault pools always read "no quota reading yet", because the vault state source drops each row's cachedQuotaUpdatedAt.

OpenCode 1 users get colored bars and a collapsible header; GA users get neither.

What changes

State freshness (plugin/ga-location-services.ts)

  • state answers report the newest cached quota reading as status.checkedAt (local and vault), or null when no row holds a reading. A checkedAt the location reports itself still wins.
  • Vault rows forward cachedQuotaUpdatedAt, so vault pools show their figure and age like local pools.
  • No protocol change: checkedAt was already a nullable number.

GA sidebar (ga/tui/host-ga.tsx)

  • Reads optional theme tokens from the host context (text.base, text.muted, text.feedback.{success,warning,error}.base, text.action.primary.base, background.base) and colors figures, bars and status by the existing projection tones; secondary text is muted, degraded-state notices use the warning color.
  • Each pool with a reading draws a remaining-quota bar sized from the measured sidebar width (up to 10 cells, dropped below 4), so narrow sidebars keep today's exact text.
  • The header is a clickable ▼/▶ ANTIGRAVITY badge; collapsed, each account takes one line (label, lowest remaining figure, status dot). The choice persists through context.storage.store when the host provides it.
  • Without a theme or storage the sidebar renders the same text uncolored and collapse lasts for the session.
 ▼ ANTIGRAVITY
Account 1                         active
Gm    ██████░░░░ 55% left
NG    no quota reading yet
  health 90
Route Account 1 · gemini · antigravity

The GA module graph is unchanged (still pinned by tui.test.tsx); @opentui/core is used as a type-only import.

Out of scope

Tests

  • ga-location-services.test.ts: newest-reading helper; local checkedAt from rows; reported checkedAt kept; vault rows forward reading times and report the newest; no identity in the answer.
  • tui.test.tsx: colors by tone with a fake theme (span colors), bar sizing and fill, collapsed figure, header click collapses and saves to storage, starts collapsed from storage; the existing width-32 reset-span frame is unchanged; one frame expectation updated for the new bar.
  • Gates run locally with Bun 1.4.2: build, typecheck, lint, format:check. bun test --isolate ./src ./scripts/build-tui.test.ts in packages/opencode: the 36 failures are the same on main without this change in this environment (Pi native path, setup CLI, GA test-case lifetime), none in touched areas. Existing mutations.toml anchors in touched files still match exactly once.

Specified with OpenSpec (proposal, two capability specs ga-sidebar and ga-state-status, design, tasks); the artifacts are kept out of the repository.


Summary by cubic

Makes the OpenCode 2 Antigravity sidebar readable and accurate: quota rows, bars, and status are now colored with the host theme, and state answers report the newest cached quota reading so the sidebar stops claiming "quota not checked yet."

State freshness

  • state answers report the newest cached quota reading as status.checkedAt for local and vault accounts, or null when no reading exists; a reported checkedAt still wins.
  • Vault pools forward cachedQuotaUpdatedAt, so they show their figures and ages instead of "no quota reading yet."

Sidebar UI

  • The header is a clickable ▼/▶ ANTIGRAVITY badge that collapses the panel to one line per account (label, lowest remaining figure, status dot); the choice persists in host plugin storage when available.
  • Quota rows draw a remaining-quota bar sized from the measured sidebar width, dropped when it does not fit; tones color figures, bars, and status, secondary text is muted, and degraded-state notices use the warning color.
  • Without a theme or storage, the sidebar renders the same uncolored text and collapses for the session only.

Written for commit ab7a9b1. Summary will update on new commits.

View guided diff

…ta freshness

The GA sidebar printed every row as plain text although the projection
already computes a tone per quota row and status, and it always said
"quota not checked yet" because both GA state sources hard-coded
status.checkedAt to null. Vault pools also always read "no quota reading
yet" because the vault source dropped each row's reading time.

- state answers report the newest cached quota reading as checkedAt, for
  local and vault custody; vault rows forward cachedQuotaUpdatedAt
- the GA renderer reads optional theme tokens from the host context and
  colors figures, bars, status, secondary text and notices by tone
- each pool with a reading draws a remaining-quota bar sized from the
  measured sidebar width (dropped when it does not fit, so narrow
  sidebars keep today's text)
- the header is a clickable ANTIGRAVITY badge that collapses the panel to
  one line per account; the choice persists in host plugin storage

Without a theme or storage the sidebar renders the same text uncolored and
collapse lasts for the session. The GA module graph is unchanged.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

5 issues found across 6 files

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/docs/OPENCODE-2.md">

<violation number="1" location="packages/opencode/docs/OPENCODE-2.md:51">
P2: This sentence presents theme coloring and collapse persistence as unconditional, but hosts without theme or storage support retain default colors and a session-only collapse choice. Document both fallbacks so users are not promised capabilities their host does not provide.</violation>
</file>

<file name="packages/opencode/src/plugin/ga-location-services.ts">

<violation number="1" location="packages/opencode/src/plugin/ga-location-services.ts:1763">
P2: The vault projection drops `cachedQuotaUpdatedAt` whenever `cachedQuota` is absent, even though vault state permits and hydrates those fields independently. Forward the timestamp independently so `status.checkedAt` remains accurate for timestamp-only readings.</violation>

<violation number="2" location="packages/opencode/src/plugin/ga-location-services.ts:1776">
P2: Over-limit state responses always report `checkedAt: null` because both sources discard all rows before calculating freshness. Compute the newest timestamp from the ready roster before the over-limit projection so clients do not lose quota freshness in this response mode.</violation>
</file>

<file name="packages/opencode/src/ga/tui/host-ga.tsx">

<violation number="1" location="packages/opencode/src/ga/tui/host-ga.tsx:1517">
P2: `quotaBarCells` reserves six cells even when a window label is longer, such as `Gm weekly`; `padEnd` cannot shorten it. Base the calculation on `Math.max(LABEL_CELLS, row.label.length)` so bars do not overflow rows with quota windows.</violation>

<violation number="2" location="packages/opencode/src/ga/tui/host-ga.tsx:1537">
P2: The sidebar reads a theme shape that OpenCode 2 does not provide, so all new color lookups are `undefined` in the real host and the advertised coloring never appears. Map tones to the host's flat `success`/`warning`/`error`/`textMuted`/`text` fields and use `accent`/`background` for the badge.</violation>
</file>

Reply to a comment to ask cubic a question or push back. It learns from your replies.

View guided diff | Re-trigger cubic

## UI and evidence limits

OpenCode 2's sidebar and menu use location-scoped snapshots through native host RPC, not OpenCode 1's sidebar/port-file configuration. Credentials, project IDs, and fingerprints do not belong in renderer snapshots or support reports. See [commands](COMMANDS.md) and [credential safety](CONFIGURATION.md#security-and-redaction).
OpenCode 2's sidebar and menu use location-scoped snapshots through native host RPC, not OpenCode 1's sidebar/port-file configuration. The sidebar colors quota with the host theme's feedback colors (remaining above 30 % success, 11–30 % warning, 10 % or less error) and collapses from its `ANTIGRAVITY` header; the collapse choice lives in the host's plugin storage, separate from OpenCode 1's `tui-preferences.jsonc`. Credentials, project IDs, and fingerprints do not belong in renderer snapshots or support reports. See [commands](COMMANDS.md) and [credential safety](CONFIGURATION.md#security-and-redaction).

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This sentence presents theme coloring and collapse persistence as unconditional, but hosts without theme or storage support retain default colors and a session-only collapse choice. Document both fallbacks so users are not promised capabilities their host does not provide.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. 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. At packages/opencode/docs/OPENCODE-2.md, line 51:

<comment>This sentence presents theme coloring and collapse persistence as unconditional, but hosts without theme or storage support retain default colors and a session-only collapse choice. Document both fallbacks so users are not promised capabilities their host does not provide.</comment>

<file context>
@@ -48,6 +48,6 @@ The host retry API identifies the session but not the exact HTTP attempt. The pl
 ## UI and evidence limits
 
-OpenCode 2's sidebar and menu use location-scoped snapshots through native host RPC, not OpenCode 1's sidebar/port-file configuration. Credentials, project IDs, and fingerprints do not belong in renderer snapshots or support reports. See [commands](COMMANDS.md) and [credential safety](CONFIGURATION.md#security-and-redaction).
+OpenCode 2's sidebar and menu use location-scoped snapshots through native host RPC, not OpenCode 1's sidebar/port-file configuration. The sidebar colors quota with the host theme's feedback colors (remaining above 30 % success, 11–30 % warning, 10 % or less error) and collapses from its `ANTIGRAVITY` header; the collapse choice lives in the host's plugin storage, separate from OpenCode 1's `tui-preferences.jsonc`. Credentials, project IDs, and fingerprints do not belong in renderer snapshots or support reports. See [commands](COMMANDS.md) and [credential safety](CONFIGURATION.md#security-and-redaction).
 
 The [released-host contract](opencode2-ga-2.0.22-contract.md) records pinned contracts and proof obligations. Source tests, a packed TUI smoke run, and a native-host run are distinct evidence. A native host using simulated OAuth responses does not prove Google's live OAuth acceptance or account entitlement. Live model verification means sending requests to the actual Antigravity service, not a mock server. This guide does not claim that native gates or live-service verification have run: [Argon MEDIUM/HIGH remain unverified live](MODEL-VARIANTS.md#gemini).
</file context>
Suggested change
OpenCode 2's sidebar and menu use location-scoped snapshots through native host RPC, not OpenCode 1's sidebar/port-file configuration. The sidebar colors quota with the host theme's feedback colors (remaining above 30 % success, 11–30 % warning, 10 % or less error) and collapses from its `ANTIGRAVITY` header; the collapse choice lives in the host's plugin storage, separate from OpenCode 1's `tui-preferences.jsonc`. Credentials, project IDs, and fingerprints do not belong in renderer snapshots or support reports. See [commands](COMMANDS.md) and [credential safety](CONFIGURATION.md#security-and-redaction).
OpenCode 2's sidebar and menu use location-scoped snapshots through native host RPC, not OpenCode 1's sidebar/port-file configuration. When available, the host theme colors quota (remaining above 30 % success, 11–30 % warning, 10 % or less error); without those theme tokens, the sidebar remains uncolored. The sidebar collapses from its `ANTIGRAVITY` header, and the collapse choice is saved in host plugin storage when available; otherwise it lasts only for the session, separate from OpenCode 1's `tui-preferences.jsonc`. Credentials, project IDs, and fingerprints do not belong in renderer snapshots or support reports. See [commands](COMMANDS.md) and [credential safety](CONFIGURATION.md#security-and-redaction).

route,
status: {
checkedAt: null,
checkedAt: newestQuotaReadingAt(projected.map(({ row }) => row)),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Over-limit state responses always report checkedAt: null because both sources discard all rows before calculating freshness. Compute the newest timestamp from the ready roster before the over-limit projection so clients do not lose quota freshness in this response mode.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. 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. At packages/opencode/src/plugin/ga-location-services.ts, line 1776:

<comment>Over-limit state responses always report `checkedAt: null` because both sources discard all rows before calculating freshness. Compute the newest timestamp from the ready roster before the over-limit projection so clients do not lose quota freshness in this response mode.</comment>

<file context>
@@ -1727,30 +1746,34 @@ function createGaVaultStateSource(input: {
       route,
       status: {
-        checkedAt: null,
+        checkedAt: newestQuotaReadingAt(projected.map(({ row }) => row)),
         quotaBackoffUntil: null,
         routingAuthoritative: true,
</file context>

Comment on lines +1763 to +1766
...(row.cachedQuota !== undefined &&
row.cachedQuotaUpdatedAt !== undefined
? { cachedQuotaUpdatedAt: row.cachedQuotaUpdatedAt }
: {}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The vault projection drops cachedQuotaUpdatedAt whenever cachedQuota is absent, even though vault state permits and hydrates those fields independently. Forward the timestamp independently so status.checkedAt remains accurate for timestamp-only readings.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. 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. At packages/opencode/src/plugin/ga-location-services.ts, line 1763:

<comment>The vault projection drops `cachedQuotaUpdatedAt` whenever `cachedQuota` is absent, even though vault state permits and hydrates those fields independently. Forward the timestamp independently so `status.checkedAt` remains accurate for timestamp-only readings.</comment>

<file context>
@@ -1727,30 +1746,34 @@ function createGaVaultStateSource(input: {
+            ...(row.cachedQuota !== undefined
+              ? { cachedQuota: row.cachedQuota }
+              : {}),
+            ...(row.cachedQuota !== undefined &&
+            row.cachedQuotaUpdatedAt !== undefined
+              ? { cachedQuotaUpdatedAt: row.cachedQuotaUpdatedAt }
</file context>
Suggested change
...(row.cachedQuota !== undefined &&
row.cachedQuotaUpdatedAt !== undefined
? { cachedQuotaUpdatedAt: row.cachedQuotaUpdatedAt }
: {}),
...(row.cachedQuotaUpdatedAt !== undefined
? { cachedQuotaUpdatedAt: row.cachedQuotaUpdatedAt }
: {}),

const reset = row.reset === '' ? 0 : row.reset.length + 1
const cells = Math.min(
BAR_CELLS,
width - LABEL_CELLS - 1 - row.figure.length - reset,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: quotaBarCells reserves six cells even when a window label is longer, such as Gm weekly; padEnd cannot shorten it. Base the calculation on Math.max(LABEL_CELLS, row.label.length) so bars do not overflow rows with quota windows.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. 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. At packages/opencode/src/ga/tui/host-ga.tsx, line 1517:

<comment>`quotaBarCells` reserves six cells even when a window label is longer, such as `Gm weekly`; `padEnd` cannot shorten it. Base the calculation on `Math.max(LABEL_CELLS, row.label.length)` so bars do not overflow rows with quota windows.</comment>

<file context>
@@ -1469,9 +1501,100 @@ interface SidebarView {
+  const reset = row.reset === '' ? 0 : row.reset.length + 1
+  const cells = Math.min(
+    BAR_CELLS,
+    width - LABEL_CELLS - 1 - row.figure.length - reset,
+  )
+  return cells >= MIN_BAR_CELLS ? cells : 0
</file context>

): GaColor | undefined {
switch (tone) {
case 'ok':
return theme?.text?.feedback?.success?.base

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The sidebar reads a theme shape that OpenCode 2 does not provide, so all new color lookups are undefined in the real host and the advertised coloring never appears. Map tones to the host's flat success/warning/error/textMuted/text fields and use accent/background for the badge.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. 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. At packages/opencode/src/ga/tui/host-ga.tsx, line 1537:

<comment>The sidebar reads a theme shape that OpenCode 2 does not provide, so all new color lookups are `undefined` in the real host and the advertised coloring never appears. Map tones to the host's flat `success`/`warning`/`error`/`textMuted`/`text` fields and use `accent`/`background` for the badge.</comment>

<file context>
@@ -1469,9 +1501,100 @@ interface SidebarView {
+): GaColor | undefined {
+  switch (tone) {
+    case 'ok':
+      return theme?.text?.feedback?.success?.base
+    case 'warn':
+      return theme?.text?.feedback?.warning?.base
</file context>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant