Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added
### Changed
- **Consumo do Claude agora é _lazy_ (coleta sob demanda).** O loop de fundo (60s)
deixou de bater na API `/api/oauth/usage` do Claude — a chamada só acontece quando
você **vai olhar o uso**: ao **abrir/expandir** o overlay, ao **revelar** a janela
(tray/atalho), no botão **⟳** e no **boot** (1 chamada). Essa API divide um limite
**agregado** com o `/status` do próprio Claude Code, então consultá-la a cada 60 s
alimentava o **429** e a barra de % do Claude sumia. Agora o app fica fora desse
balde na maior parte do tempo; entre gatilhos, mostra o último % conhecido (ou o
plano-só, sem inventar número). O cache de 5 min evita repetir a chamada a cada
abrir/fechar. GLM/Codex/Antigravity seguem no loop normal (não sofrem esse limite).
### Fixed

## [0.6.8] - 2026-07-10
Expand Down
27 changes: 21 additions & 6 deletions main.js
Original file line number Diff line number Diff line change
Expand Up @@ -596,7 +596,12 @@ function createWindow() {
function toggleWin() {
if (!win || win.isDestroyed()) return;
if (win.isVisible()) win.hide();
else { win.show(); try { win.setSkipTaskbar(true); } catch {} try { win.moveTop(); } catch {} }
else {
win.show(); try { win.setSkipTaskbar(true); } catch {} try { win.moveTop(); } catch {}
// Revelou o overlay (tray/atalho) → o usuário vai olhar; busca o % do Claude
// agora (lazy). Cache de 5 min evita repetir a cada mostra/esconde.
collectAndSendUsage({ claudeFetch: true });
}
}

// Traz o overlay de volta à tela se ele estiver OCULTO (hide). Não rouba o foco
Expand Down Expand Up @@ -791,6 +796,10 @@ ipcMain.on('set-expanded', (_e, { expanded, h } = {}) => {
// que o autosize deixou no estado expandido (janela não reduzia ao recolher).
win.setMinimumSize(MIN_W, height);
win.setSize(w, height, false);
} else {
// Expandiu: o usuário quer VER o uso → busca o % do Claude agora (lazy). O
// cache de 5 min evita spam de abrir/fechar; fora daqui o loop não bate.
collectAndSendUsage({ claudeFetch: true });
}
});

Expand Down Expand Up @@ -944,10 +953,12 @@ app.whenReady().then(() => {
.on('all', () => sendSessions());
reapDead();
setInterval(() => { _discAt = 0; reapDead(); sendSessions(); saveBounds(); }, 5000); // descobre novos + limpa mortos + captura posição (drag externo p/ ex.)
// Consumo/reset dos agentes: GLM (rede, cache 30s) + Claude (arquivo local).
// Consumo/reset dos agentes: GLM (rede, cache 30s) + Codex/Antigravity (disco).
// Cadência própria (60s) — desacoplada das sessões (que refrescam a cada 5s).
collectAndSendUsage();
setInterval(collectAndSendUsage, 60 * 1000);
// O Claude é LAZY: o loop de fundo NÃO bate na API dele (limite agregado do
// 429); só o boot e os gatilhos de UI (abrir/revelar overlay, ⟳) buscam o %.
collectAndSendUsage({ claudeFetch: true }); // boot: 1 chamada p/ já ter o %
setInterval(collectAndSendUsage, 60 * 1000); // fundo: claudeFetch=false (não bate)
setupAutoUpdater(); // update checker (boot + 1h) — AppImage auto-update
});

Expand Down Expand Up @@ -1122,7 +1133,7 @@ function codexCwdsFromSessions() {
return [...cwds];
}

async function collectAndSendUsage() {
async function collectAndSendUsage({ claudeFetch = false } = {}) {
try {
let glmCreds = glmCredsFromSessions();
// Fallback 1: o próprio app foi lançado de um terminal GLM (vars já no env).
Expand All @@ -1140,6 +1151,10 @@ async function collectAndSendUsage() {
const codexCwds = codexCwdsFromSessions();
const entries = await usage.collectUsage({
glmCreds, codexCwds, home: app.getPath('home'),
// LAZY: o loop de fundo (claudeFetch=false) NÃO bate na API do Claude — só
// os gatilhos de UI (abrir/revelar overlay, ⟳) e o boot passam true. Tira o
// app do limite agregado do 429 (compartilhado com o /status do Claude Code).
claudeAllowFetch: claudeFetch,
// cooldown do 429 persistido: não rebate na API enquanto vigente; o coletor
// chama de volta setCooldown quando leva um 429 novo (grava {until, fails}).
claudeCooldownUntil: claudeCooldownUntil,
Expand Down Expand Up @@ -1197,7 +1212,7 @@ ipcMain.on('force-usage', () => {
usage._clearGlmCache();
usage._clearCodexCache();
} catch { /* ignore */ }
collectAndSendUsage();
collectAndSendUsage({ claudeFetch: true }); // ⟳: gatilho de UI → busca o % agora
});

// ---- update checker (versão + release mais nova do GitHub) ----
Expand Down
14 changes: 13 additions & 1 deletion src/usage.js
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,7 @@ function claudePlanFromCreds({ subscriptionType, rateLimitTier } = {}) {
// na API: devolvemos o último valor bom conhecido, ou o plano-só. Assim o tile
// não some nem pisca ⚠ e a janela de rate limit expira sozinha.
const _claudeCacheByToken = new Map(); // token → { at, entries, cooldownUntil }
async function readClaudeUsage({ home, now, fetcher, cooldownUntil, cooldownFails, setCooldown } = {}) {
async function readClaudeUsage({ home, now, fetcher, cooldownUntil, cooldownFails, setCooldown, allowFetch = true } = {}) {
const pc = readClaudeConfig({ home, now });
const creds = readClaudeCreds({ home });
// Plano: credenciais primeiro (tier/subscription REAIS — ex.: 'default_claude_max_5x'),
Expand Down Expand Up @@ -317,6 +317,14 @@ async function readClaudeUsage({ home, now, fetcher, cooldownUntil, cooldownFail
const cd = Math.max(cached && cached.cooldownUntil || 0, cooldownUntil || 0);
if (cd && nowMs < cd) return (cached && cached.entries) || planOnly;

// LAZY (coleta sob demanda): o loop de fundo passa allowFetch=false e NÃO bate
// na API — a chamada só acontece quando o usuário VAI OLHAR o uso (abrir/revelar
// o overlay, botão ⟳) ou no boot. A /api/oauth/usage divide um limite AGREGADO
// com o /status do próprio Claude Code; consultá-la em loop de 60s alimentava o
// 429. Sem gatilho, devolvemos o último valor bom conhecido (ou o plano-só) —
// mesmo comportamento do cooldown, porém sem tocar na rede.
if (!allowFetch) return (cached && cached.entries) || planOnly;

const headers = {
Authorization: 'Bearer ' + token,
'anthropic-beta': 'oauth-2025-04-20',
Expand Down Expand Up @@ -754,6 +762,10 @@ async function collectUsage(opts = {}) {
home: opts.home, now: opts.now, fetcher: opts.claudeFetcher,
cooldownUntil: opts.claudeCooldownUntil, cooldownFails: opts.claudeCooldownFails,
setCooldown: opts.claudeSetCooldown,
// Lazy: só bate na API do Claude quando o caller pede (gatilho de UI). O
// main.js passa true ao abrir/revelar o overlay e no ⟳; o loop de fundo
// omite → false. Default true preserva o contrato dos testes/uso direto.
allowFetch: opts.claudeAllowFetch !== false,
}).catch(() => null),
Promise.resolve().then(() => readAntigravityUsage({ home: opts.home })).catch(() => null),
...creds.map((c) => readGlmUsage({
Expand Down
28 changes: 28 additions & 0 deletions test/usage.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,34 @@ test('readClaudeUsage: cooldownUntil injetado (persistido) bloqueia a chamada me
fs.rmSync(tmp, { recursive: true, force: true });
});

test('readClaudeUsage: allowFetch=false (lazy) NÃO bate na API — cai no plano-só sem chamar o fetcher', async () => {
_clearClaudeCache(); // sem valor bom prévio
const tmp = claudeHome({ token: 'tok' });
const f = mockFetcher({ five_hour: { utilization: 50, resets_at: '2026-07-07T17:00:00Z' } });
// loop de fundo (lazy): sem cache e sem gatilho de UI → não toca a rede.
const r = await readClaudeUsage({ home: tmp, now: NOW, fetcher: f, allowFetch: false });
assert.equal(f.calls.length, 0, 'loop de fundo não bate na API do Claude');
assert.equal(r.length, 1);
assert.equal(r[0].id, 'claude-plan'); // plano-só honesto (sem %)
fs.rmSync(tmp, { recursive: true, force: true });
});

test('readClaudeUsage: allowFetch=false devolve o ÚLTIMO valor bom do cache (não rebate nem regride)', async () => {
_clearClaudeCache();
const tmp = claudeHome({ token: 'tok' });
const f = mockFetcher({ five_hour: { utilization: 50, resets_at: '2026-07-07T17:00:00Z' } });
// gatilho de UI (abrir overlay) → busca 1x e popula o cache com o % real.
const r1 = await readClaudeUsage({ home: tmp, now: NOW, fetcher: f, allowFetch: true });
assert.equal(r1[0].id, 'claude-5h');
assert.equal(f.calls.length, 1);
// +6min (cache de 5min já expirou) MAS loop de fundo (lazy) → não rebate; mantém o %.
const r2 = await readClaudeUsage({ home: tmp, now: NOW + 6 * 60_000, fetcher: f, allowFetch: false });
assert.equal(f.calls.length, 1, 'lazy não gasta chamada no loop de fundo');
assert.equal(r2[0].id, 'claude-5h');
assert.equal(r2[0].usedPct, 50, 'segura o último valor bom em vez de regredir pro plano-só');
fs.rmSync(tmp, { recursive: true, force: true });
});

test('readClaudeUsage: 429 chama setCooldown com {until, fails} p/ persistir', async () => {
_clearClaudeCache();
const tmp = claudeHome({ token: 'tok' });
Expand Down
Loading