← back to Kickbacks Ai Upstream
src/statusbar.ts
152 lines
import * as vscode from "vscode";
import { buildLabel } from "./buildinfo";
export type SbState =
| { kind: "signed-out" }
| { kind: "active"; version: string; usd?: string; usdToday?: string }
| { kind: "incompatible"; version: string }
| { kind: "killed" }
| { kind: "offline" }
// `debug` renders exactly like `active` (the word "debug" is intentionally
// never surfaced in the status-bar text — operator-facing detail only).
| { kind: "debug"; on: boolean; version?: string; usd?: string; usdToday?: string }
| { kind: "ad"; adText: string }
// STICKY: the patch landed this session but the running Claude Code webview
// predates it — no ads (so no earnings) until a window reload. Once set,
// ROUTINE paints (ad / active / debug-ON earnings) are ignored so they
// can't bury the red "reload" call-to-action; safety/truth states (killed,
// offline, signed-out, incompatible, debug-OFF) always win and clear the
// lock — the bar must never hide a kill, an outage, or an auth loss behind
// the CTA (audit #29). The lock also clears with the reload itself (fresh
// activation).
| { kind: "needs-reload" };
// Darker than the previous #3fb950 (GitHub success-emphasis green) so it
// reads as confident-earning rather than a bright neon stripe.
const GREEN = "#2ea043";
// Used for states where the extension isn't earning (signed-out, injection
// toggled off, kill-switch engaged, backend offline). Sits at the same
// confidence level as GREEN — bright enough to flag the state at a glance
// in the status bar without being a "broken" red.
const RED = "#f85149";
export class StatusBar {
// Right-aligned (priority high so it sits toward the leading edge of the
// right cluster).
private item = vscode.window.createStatusBarItem(
vscode.StatusBarAlignment.Right, 1000);
text = "";
constructor() { this.item.command = "kickbacks.debugMenu"; this.item.show(); }
// Always render the two-figure form. A missing figure (fetch failed, or
// signed-in before the first /v1/earnings poll) degrades to "$0.00" — the
// signed-in/active bar never shows a bare label. Note: a fetch failure is
// therefore indistinguishable from genuine zero earnings (deliberate, per
// product call — prefer always showing a dollar figure).
private earned(usd?: string, today?: string): string {
return ` ($${today ?? "0.00"} today · $${usd ?? "0.00"})`;
}
/** True once `needs-reload` has been shown; masks ROUTINE repaints only
* (see the SbState docstring). */
private reloadLock = false;
/** Routine earning paints — the only kinds the reload lock may mask.
* Everything else (killed, offline, signed-out, incompatible, debug-OFF)
* is a safety/truth state the bar must never hide behind the CTA. */
private static isRoutine(s: SbState): boolean {
return s.kind === "ad" || s.kind === "active"
|| (s.kind === "debug" && s.on !== false);
}
/** Returns false when the paint was suppressed by the reload lock —
* statusBarAd checks this so an ad whose paint never landed is never
* billed (no impression/view_tick for an undisplayed ad, audit #29). */
set(s: SbState): boolean {
if (this.reloadLock && s.kind !== "needs-reload") {
if (StatusBar.isRoutine(s)) return false;
// Safety/truth state: always paint, and release the lock so the
// recovery paints that follow (e.g. back-online "active") aren't
// dropped against a stale safety label.
this.reloadLock = false;
}
let color: string | undefined;
let background: vscode.ThemeColor | undefined;
let command = "kickbacks.debugMenu";
let tooltip = "Kickbacks";
switch (s.kind) {
case "signed-out":
// No icon — the "K" codicon doesn't exist in VS Code's default set,
// and a generic glyph would misrepresent the brand. Text only.
// RED to flag the not-earning state at a glance.
this.text = "Kickbacks: Sign in";
color = RED;
tooltip = "Click to sign in to Kickbacks";
break;
case "active":
this.text = `Kickbacks${this.earned(s.usd, s.usdToday)}`;
color = GREEN;
tooltip = `Kickbacks active${s.version ? ` · Claude Code ${s.version}` : ""}`
+ ` · $${s.usdToday ?? "0.00"} today · $${s.usd ?? "0.00"} earned`
+ " (display-only credit, payout TBD)";
break;
case "debug":
// Signed-in/debug: green when ON, red when OFF (user has the menu
// master switch flipped off — earnings paused).
if (s.on === false) {
this.text = "Kickbacks: Off";
color = RED;
tooltip = "Kickbacks is currently OFF — click to re-enable";
} else {
this.text = `Kickbacks${this.earned(s.usd, s.usdToday)}`;
color = GREEN;
tooltip = `Kickbacks active${s.version ? ` · Claude Code ${s.version}` : ""}`
+ ` · $${s.usdToday ?? "0.00"} today · $${s.usd ?? "0.00"} earned`
+ " (display-only credit, payout TBD)";
}
break;
case "incompatible":
this.text = `Kickbacks incompatible (${s.version})`;
break;
case "killed":
this.text = "Kickbacks killed";
color = RED;
break;
case "offline":
this.text = "Kickbacks offline";
color = RED;
break;
case "ad":
// Brand the status-bar ad with a pipe separator: "Kickbacks.ai | <ad>".
this.text = `Kickbacks.ai | ${s.adText}`;
color = GREEN;
tooltip = "Kickbacks ad";
break;
case "needs-reload":
this.reloadLock = true;
this.text = "$(warning) Kickbacks: RELOAD to earn money";
// errorBackground is the only red background VS Code lets a status-bar
// item paint; the theme pairs it with white foreground. Explicit white
// on top so a custom theme can't dim the call-to-action.
background = new vscode.ThemeColor("statusBarItem.errorBackground");
color = "#ffffff";
// One click = the reload itself (not the menu) — the whole point of
// this state is removing every step between the user and the reload.
command = "workbench.action.reloadWindow";
tooltip = "Kickbacks won't earn money until you reload — click to reload now";
break;
}
// The click opens the menu (GET PAID OUT / sign in / sign out live there)
// — except needs-reload, where the click IS the reload.
this.item.command = command;
this.item.backgroundColor = background;
this.item.color = color;
// Live build age in the tooltip — the truthful "how long ago published"
// the VS Code Installation panel can't show (it's not author-extensible).
this.item.tooltip = `${tooltip} · ${buildLabel()}`;
this.item.text = this.text;
return true;
}
dispose(): void { this.item.dispose(); }
}