clauddy 1.21.0 → 1.21.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/auth.js +25 -3
- package/main.js +46 -3
- package/package.json +1 -1
- package/renderer/pet.js +23 -9
package/auth.js
CHANGED
|
@@ -35,12 +35,25 @@ function setDataDir(dir) {
|
|
|
35
35
|
tokens = null
|
|
36
36
|
profile = null
|
|
37
37
|
pending = null
|
|
38
|
+
unreadable = false
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
// Set when the file itself could not be read for a reason other than "it is not
|
|
42
|
+
// there" — a disk still waking up, say. That must not read as a logout, or the
|
|
43
|
+
// widget asks for a login it doesn't need and every account looks gone. A file
|
|
44
|
+
// we did read but cannot parse is a different matter: that one really is dead.
|
|
45
|
+
let unreadable = false
|
|
40
46
|
function load() {
|
|
41
47
|
if (tokens) return tokens
|
|
48
|
+
let raw = null
|
|
42
49
|
try {
|
|
43
|
-
|
|
50
|
+
raw = fs.readFileSync(tokenPath, 'utf8')
|
|
51
|
+
unreadable = false
|
|
52
|
+
} catch (e) {
|
|
53
|
+
unreadable = e?.code !== 'ENOENT'
|
|
54
|
+
}
|
|
55
|
+
try {
|
|
56
|
+
tokens = raw == null ? null : JSON.parse(raw)
|
|
44
57
|
} catch {
|
|
45
58
|
tokens = null
|
|
46
59
|
}
|
|
@@ -56,12 +69,15 @@ function save(t) {
|
|
|
56
69
|
function clear() {
|
|
57
70
|
tokens = null
|
|
58
71
|
profile = null
|
|
72
|
+
unreadable = false
|
|
59
73
|
try {
|
|
60
74
|
fs.unlinkSync(tokenPath)
|
|
61
75
|
} catch {}
|
|
62
76
|
}
|
|
63
77
|
function isConnected() {
|
|
64
|
-
|
|
78
|
+
// a token we merely failed to read still counts: the account is connected,
|
|
79
|
+
// we just couldn't see it this instant
|
|
80
|
+
return !!load() || unreadable
|
|
65
81
|
}
|
|
66
82
|
|
|
67
83
|
// Step 1: build the authorize URL (opens in the browser)
|
|
@@ -152,7 +168,13 @@ async function refresh() {
|
|
|
152
168
|
|
|
153
169
|
async function validToken() {
|
|
154
170
|
const t = load()
|
|
155
|
-
|
|
171
|
+
// status 0 is "try again later", not "log in again": only a token that is
|
|
172
|
+
// genuinely absent should send the user back through the browser
|
|
173
|
+
if (!t) {
|
|
174
|
+
throw Object.assign(new Error(unreadable ? 'token unreadable' : 'not connected'), {
|
|
175
|
+
status: unreadable ? 0 : 401,
|
|
176
|
+
})
|
|
177
|
+
}
|
|
156
178
|
if (!t.expires_at || t.expires_at - Date.now() < 60000) await refresh()
|
|
157
179
|
return load().access_token
|
|
158
180
|
}
|
package/main.js
CHANGED
|
@@ -8,6 +8,7 @@ const {
|
|
|
8
8
|
Tray,
|
|
9
9
|
Menu,
|
|
10
10
|
nativeImage,
|
|
11
|
+
powerMonitor,
|
|
11
12
|
} = require('electron')
|
|
12
13
|
const path = require('node:path')
|
|
13
14
|
const fs = require('node:fs')
|
|
@@ -336,6 +337,7 @@ function createWindow() {
|
|
|
336
337
|
for (const a of accounts.list()) pruneEmpty(a.id)
|
|
337
338
|
accounts.pruneOrphans() // folders those slots left behind
|
|
338
339
|
applyAccount(accounts.active())
|
|
340
|
+
powerMonitor?.on?.('resume', onResume)
|
|
339
341
|
const { workAreaSize } = screen.getPrimaryDisplay()
|
|
340
342
|
const H = 480
|
|
341
343
|
|
|
@@ -648,6 +650,7 @@ function watchDebug() {
|
|
|
648
650
|
// ---- real usage via OAuth (authoritative %), polled slowly with 429 backoff ----
|
|
649
651
|
let usageTimer = null
|
|
650
652
|
let usageBackoff = 5 * 60 * 1000
|
|
653
|
+
let authFails = 0 // consecutive 401s — see pollUsage
|
|
651
654
|
function scheduleUsagePoll() {
|
|
652
655
|
clearTimeout(usageTimer)
|
|
653
656
|
if (auth.isConnected()) usageTimer = setTimeout(pollUsage, usageBackoff)
|
|
@@ -656,11 +659,21 @@ async function pollUsage() {
|
|
|
656
659
|
try {
|
|
657
660
|
const u = await auth.fetchUsage()
|
|
658
661
|
usageBackoff = 5 * 60 * 1000
|
|
662
|
+
authFails = 0
|
|
659
663
|
pushRealUsage(u)
|
|
660
664
|
} catch (e) {
|
|
661
665
|
if (e && e.status === 429) {
|
|
662
666
|
usageBackoff = Math.min(usageBackoff * 2, 30 * 60 * 1000)
|
|
663
667
|
} else if (e && e.status === 401) {
|
|
668
|
+
// Refreshing rotates the refresh token, so a request that raced the one
|
|
669
|
+
// that rotated it comes back rejected while the session is perfectly
|
|
670
|
+
// alive. Clearing on that first answer throws away a good login, so the
|
|
671
|
+
// second rejection in a row is what counts.
|
|
672
|
+
if (++authFails < 2) {
|
|
673
|
+
usageBackoff = 60 * 1000
|
|
674
|
+
scheduleUsagePoll()
|
|
675
|
+
return
|
|
676
|
+
}
|
|
664
677
|
auth.clear()
|
|
665
678
|
pushRealUsage(null)
|
|
666
679
|
sendAccounts()
|
|
@@ -674,20 +687,50 @@ async function pollUsage() {
|
|
|
674
687
|
scheduleUsagePoll()
|
|
675
688
|
}
|
|
676
689
|
function startUsagePoll() {
|
|
690
|
+
authFails = 0
|
|
677
691
|
if (auth.isConnected()) pollUsage()
|
|
678
692
|
}
|
|
679
693
|
|
|
694
|
+
// Waking up: the poll timer was frozen through the sleep, so the numbers on
|
|
695
|
+
// screen are as old as the nap. Re-state what we know — a read that failed on
|
|
696
|
+
// the way down would otherwise leave a stale "log in" panel up — and poll
|
|
697
|
+
// again, after a beat, since the network is rarely back the instant we are.
|
|
698
|
+
function onResume() {
|
|
699
|
+
if (!auth.isConnected()) return
|
|
700
|
+
if (win && !win.isDestroyed()) win.webContents.send('auth-state', { connected: true })
|
|
701
|
+
clearTimeout(usageTimer)
|
|
702
|
+
usageBackoff = 5 * 60 * 1000
|
|
703
|
+
usageTimer = setTimeout(pollUsage, 5000)
|
|
704
|
+
sendProfile()
|
|
705
|
+
}
|
|
706
|
+
|
|
680
707
|
// push the logged-in account's identity (email + plan) to the renderer
|
|
681
|
-
|
|
708
|
+
let profileTimer = null
|
|
709
|
+
async function sendProfile(tries = 0) {
|
|
710
|
+
clearTimeout(profileTimer)
|
|
682
711
|
if (!auth.isConnected()) return
|
|
712
|
+
// whose profile this is, decided *before* the await: a switch during the
|
|
713
|
+
// fetch would otherwise label the new account with the old one's email
|
|
714
|
+
const id = accounts.activeId()
|
|
683
715
|
try {
|
|
684
716
|
const p = await auth.fetchProfile()
|
|
717
|
+
if (id !== accounts.activeId()) return // switched under us — this is stale
|
|
685
718
|
if (p?.email) {
|
|
686
|
-
accounts.label(
|
|
719
|
+
accounts.label(id, p.email) // a better name than "acct-xyz"
|
|
687
720
|
sendAccounts()
|
|
688
721
|
}
|
|
689
722
|
if (win && !win.isDestroyed()) win.webContents.send('profile', p)
|
|
690
|
-
} catch {
|
|
723
|
+
} catch {
|
|
724
|
+
// a rate limit or a blip would otherwise hide the chip — and with it the
|
|
725
|
+
// account switcher — until the app is restarted, so keep trying for a while
|
|
726
|
+
if (tries >= 4) return
|
|
727
|
+
profileTimer = setTimeout(
|
|
728
|
+
() => {
|
|
729
|
+
if (id === accounts.activeId()) sendProfile(tries + 1)
|
|
730
|
+
},
|
|
731
|
+
30 * 1000 * 2 ** tries,
|
|
732
|
+
)
|
|
733
|
+
}
|
|
691
734
|
}
|
|
692
735
|
|
|
693
736
|
// begin() has to run *after* any account switch: switching resets the pending
|
package/package.json
CHANGED
package/renderer/pet.js
CHANGED
|
@@ -700,26 +700,38 @@ window.api.onAuthState((s) => {
|
|
|
700
700
|
})
|
|
701
701
|
|
|
702
702
|
// logged-in account chip (email + plan) shown top-left when connected
|
|
703
|
+
let lastProfile = null
|
|
703
704
|
function showProfile(p) {
|
|
705
|
+
lastProfile = p
|
|
706
|
+
paintChip()
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// The chip is also the account switcher, so it must not vanish just because the
|
|
710
|
+
// profile fetch failed — that would strand the user on one account until a
|
|
711
|
+
// restart. The label main stored for the active account is the same email, so
|
|
712
|
+
// it stands in until the profile lands.
|
|
713
|
+
function paintChip() {
|
|
714
|
+
const p = lastProfile
|
|
715
|
+
const a = lastAccounts
|
|
716
|
+
const active = (a?.accounts || []).find((x) => x.id === a?.active)
|
|
717
|
+
const email = p?.email || (active?.connected ? active.label : null)
|
|
704
718
|
// the settings line says who is connected, not just that someone is
|
|
705
|
-
el('acc-ok').textContent = p?.
|
|
706
|
-
? `● ${p.email}${p.plan ? ` · ${p.plan}` : ''}`
|
|
707
|
-
: '● Connected'
|
|
719
|
+
el('acc-ok').textContent = email ? `● ${email}${p?.plan ? ` · ${p.plan}` : ''}` : '● Connected'
|
|
708
720
|
const chip = el('account-chip')
|
|
709
721
|
const mini = el('mini-acct')
|
|
710
|
-
if (!
|
|
722
|
+
if (!email) {
|
|
711
723
|
chip.hidden = true
|
|
712
724
|
mini.hidden = true
|
|
713
725
|
return
|
|
714
726
|
}
|
|
715
727
|
// expanded: full email chip in the top bar
|
|
716
|
-
el('ac-email').textContent =
|
|
717
|
-
chip.title = p
|
|
718
|
-
setPlan(el('ac-plan'), p
|
|
728
|
+
el('ac-email').textContent = email
|
|
729
|
+
chip.title = p?.name ? `${p.name} · ${email}` : email
|
|
730
|
+
setPlan(el('ac-plan'), p?.plan)
|
|
719
731
|
chip.hidden = false
|
|
720
732
|
// collapsed: short name + plan in the mini block (email won't fit at 116px)
|
|
721
|
-
el('mini-acct-name').textContent = p
|
|
722
|
-
setPlan(el('mini-acct-plan'), p
|
|
733
|
+
el('mini-acct-name').textContent = p?.name || email.split('@')[0]
|
|
734
|
+
setPlan(el('mini-acct-plan'), p?.plan)
|
|
723
735
|
mini.hidden = false
|
|
724
736
|
}
|
|
725
737
|
function setPlan(node, plan) {
|
|
@@ -765,6 +777,8 @@ function accountRow(acc, a) {
|
|
|
765
777
|
function renderAccounts(a) {
|
|
766
778
|
lastAccounts = a
|
|
767
779
|
document.body.classList.toggle('one-account', (a?.accounts || []).length < 2)
|
|
780
|
+
paintChip() // the label may be all the chip has to go on
|
|
781
|
+
|
|
768
782
|
if (el('acc-menu').hidden) return
|
|
769
783
|
// the switch is done: the chip now names the account the menu was pointing at
|
|
770
784
|
if (switching && !a?.busy) closeAccountMenu()
|