clauddy 1.20.0 → 1.21.1
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/README.md +1 -1
- package/accounts.js +27 -0
- package/main.js +20 -3
- package/package.json +1 -1
- package/renderer/index.html +6 -0
- package/renderer/pet.js +33 -10
- package/renderer/style.css +95 -6
package/README.md
CHANGED
|
@@ -171,7 +171,7 @@ update-desktop-database ~/.local/share/applications 2>/dev/null
|
|
|
171
171
|
## Controls
|
|
172
172
|
|
|
173
173
|
- **Drag** the widget anywhere on screen
|
|
174
|
-
- **–** minimizes to just the pet
|
|
174
|
+
- **–** minimizes to just the pet, ringed by the live session % (the number sits inside the ring); the **⤢** button or a double-click on the pet expands it back
|
|
175
175
|
- **⚙** opens settings (log in, toggle alerts, set thresholds, pick the display mode)
|
|
176
176
|
- **↗** opens the official Usage page
|
|
177
177
|
- **×** quits
|
package/accounts.js
CHANGED
|
@@ -139,6 +139,32 @@ function remove(id) {
|
|
|
139
139
|
return true
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
// Folders left behind under `accounts/` by slots that are no longer listed —
|
|
143
|
+
// an add abandoned mid-login, mostly, since saving the alert state recreates
|
|
144
|
+
// the folder on its way out. Only ever drops a folder with no auth.json in it,
|
|
145
|
+
// so a token is never destroyed by a bad read of accounts.json.
|
|
146
|
+
function pruneOrphans() {
|
|
147
|
+
const root = path.join(BASE_DIR, 'accounts')
|
|
148
|
+
let dirs = []
|
|
149
|
+
try {
|
|
150
|
+
dirs = fs.readdirSync(root, { withFileTypes: true })
|
|
151
|
+
} catch {
|
|
152
|
+
return 0 // no extra accounts were ever added
|
|
153
|
+
}
|
|
154
|
+
const known = new Set(list().map((a) => a.id))
|
|
155
|
+
let n = 0
|
|
156
|
+
for (const d of dirs) {
|
|
157
|
+
if (!d.isDirectory() || known.has(d.name)) continue
|
|
158
|
+
const dir = path.join(root, d.name)
|
|
159
|
+
if (fs.existsSync(path.join(dir, 'auth.json'))) continue
|
|
160
|
+
try {
|
|
161
|
+
fs.rmSync(dir, { recursive: true, force: true })
|
|
162
|
+
n++
|
|
163
|
+
} catch {}
|
|
164
|
+
}
|
|
165
|
+
return n
|
|
166
|
+
}
|
|
167
|
+
|
|
142
168
|
module.exports = {
|
|
143
169
|
BASE_DIR,
|
|
144
170
|
DEFAULT_ID,
|
|
@@ -151,4 +177,5 @@ module.exports = {
|
|
|
151
177
|
label,
|
|
152
178
|
setClaudeDir,
|
|
153
179
|
remove,
|
|
180
|
+
pruneOrphans,
|
|
154
181
|
}
|
package/main.js
CHANGED
|
@@ -334,6 +334,7 @@ function createWindow() {
|
|
|
334
334
|
if (fallback) accounts.setActive(fallback.id)
|
|
335
335
|
}
|
|
336
336
|
for (const a of accounts.list()) pruneEmpty(a.id)
|
|
337
|
+
accounts.pruneOrphans() // folders those slots left behind
|
|
337
338
|
applyAccount(accounts.active())
|
|
338
339
|
const { workAreaSize } = screen.getPrimaryDisplay()
|
|
339
340
|
const H = 480
|
|
@@ -677,16 +678,32 @@ function startUsagePoll() {
|
|
|
677
678
|
}
|
|
678
679
|
|
|
679
680
|
// push the logged-in account's identity (email + plan) to the renderer
|
|
680
|
-
|
|
681
|
+
let profileTimer = null
|
|
682
|
+
async function sendProfile(tries = 0) {
|
|
683
|
+
clearTimeout(profileTimer)
|
|
681
684
|
if (!auth.isConnected()) return
|
|
685
|
+
// whose profile this is, decided *before* the await: a switch during the
|
|
686
|
+
// fetch would otherwise label the new account with the old one's email
|
|
687
|
+
const id = accounts.activeId()
|
|
682
688
|
try {
|
|
683
689
|
const p = await auth.fetchProfile()
|
|
690
|
+
if (id !== accounts.activeId()) return // switched under us — this is stale
|
|
684
691
|
if (p?.email) {
|
|
685
|
-
accounts.label(
|
|
692
|
+
accounts.label(id, p.email) // a better name than "acct-xyz"
|
|
686
693
|
sendAccounts()
|
|
687
694
|
}
|
|
688
695
|
if (win && !win.isDestroyed()) win.webContents.send('profile', p)
|
|
689
|
-
} catch {
|
|
696
|
+
} catch {
|
|
697
|
+
// a rate limit or a blip would otherwise hide the chip — and with it the
|
|
698
|
+
// account switcher — until the app is restarted, so keep trying for a while
|
|
699
|
+
if (tries >= 4) return
|
|
700
|
+
profileTimer = setTimeout(
|
|
701
|
+
() => {
|
|
702
|
+
if (id === accounts.activeId()) sendProfile(tries + 1)
|
|
703
|
+
},
|
|
704
|
+
30 * 1000 * 2 ** tries,
|
|
705
|
+
)
|
|
706
|
+
}
|
|
690
707
|
}
|
|
691
708
|
|
|
692
709
|
// begin() has to run *after* any account switch: switching resets the pending
|
package/package.json
CHANGED
package/renderer/index.html
CHANGED
|
@@ -112,6 +112,12 @@
|
|
|
112
112
|
<path class="dk-leaf" d="M198 103 q5 -12 3 -25 q-7 9 -3 25" />
|
|
113
113
|
<path class="dk-leaf-c" d="M198 103 q0 -14 0 -27 q3 13 0 27" />
|
|
114
114
|
</svg>
|
|
115
|
+
<!-- collapsed: a ring around the pet with the session % inside -->
|
|
116
|
+
<svg id="ring" viewBox="0 0 100 100" aria-hidden="true">
|
|
117
|
+
<circle class="ring-track" cx="50" cy="50" r="45" />
|
|
118
|
+
<circle id="ring-fill" cx="50" cy="50" r="45" />
|
|
119
|
+
</svg>
|
|
120
|
+
<span id="ring-pct">0%</span>
|
|
115
121
|
<div id="shadow"></div>
|
|
116
122
|
<div id="pet">
|
|
117
123
|
<svg id="claude" viewBox="0 0 100 90" width="86" height="77">
|
package/renderer/pet.js
CHANGED
|
@@ -455,6 +455,8 @@ function oneShot(cls, ms) {
|
|
|
455
455
|
setTimeout(() => document.body.classList.remove(cls), ms)
|
|
456
456
|
}
|
|
457
457
|
|
|
458
|
+
const RING_LEN = 2 * Math.PI * 45 // the collapsed ring's circumference (r=45)
|
|
459
|
+
|
|
458
460
|
// session-reset celebration: jump + a colorful confetti burst
|
|
459
461
|
const CONFETTI_COLORS = ['#ffd23f', '#ff5d86', '#7ec77d', '#6db3f2', '#e0805a', '#c89bff']
|
|
460
462
|
function spawnConfetti(i) {
|
|
@@ -577,6 +579,13 @@ function render(d) {
|
|
|
577
579
|
const mini = el('mini-pct')
|
|
578
580
|
mini.textContent = liveOn ? `${Math.round(sessPct)}%` : '—'
|
|
579
581
|
mini.classList.toggle('high', liveOn && sessPct >= 80)
|
|
582
|
+
// the collapsed ring: how much of the session is already spent
|
|
583
|
+
const rf = el('ring-fill')
|
|
584
|
+
rf.style.strokeDashoffset = `${RING_LEN * (1 - Math.min(sessPct, 100) / 100)}`
|
|
585
|
+
rf.classList.toggle('high', sessPct >= 80)
|
|
586
|
+
const rp = el('ring-pct')
|
|
587
|
+
rp.textContent = mini.textContent
|
|
588
|
+
rp.classList.toggle('high', liveOn && sessPct >= 80)
|
|
580
589
|
const sf = el('session-fill')
|
|
581
590
|
sf.style.width = `${sessPct}%`
|
|
582
591
|
sf.classList.toggle('high', sessPct >= 80)
|
|
@@ -632,7 +641,7 @@ function fitSize() {
|
|
|
632
641
|
requestAnimationFrame(() => {
|
|
633
642
|
const collapsed = document.body.classList.contains('collapsed')
|
|
634
643
|
const zoom = Number.parseFloat(document.body.style.zoom) || 1
|
|
635
|
-
const w = (collapsed ?
|
|
644
|
+
const w = (collapsed ? 168 : 276) * zoom
|
|
636
645
|
// offsetHeight never reflects a CSS zoom applied to an ancestor (confirmed
|
|
637
646
|
// empirically against this Electron build) — so measure the unzoomed content
|
|
638
647
|
// height, then scale the whole thing (content + margin) ourselves
|
|
@@ -691,26 +700,38 @@ window.api.onAuthState((s) => {
|
|
|
691
700
|
})
|
|
692
701
|
|
|
693
702
|
// logged-in account chip (email + plan) shown top-left when connected
|
|
703
|
+
let lastProfile = null
|
|
694
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)
|
|
695
718
|
// the settings line says who is connected, not just that someone is
|
|
696
|
-
el('acc-ok').textContent = p?.
|
|
697
|
-
? `● ${p.email}${p.plan ? ` · ${p.plan}` : ''}`
|
|
698
|
-
: '● Connected'
|
|
719
|
+
el('acc-ok').textContent = email ? `● ${email}${p?.plan ? ` · ${p.plan}` : ''}` : '● Connected'
|
|
699
720
|
const chip = el('account-chip')
|
|
700
721
|
const mini = el('mini-acct')
|
|
701
|
-
if (!
|
|
722
|
+
if (!email) {
|
|
702
723
|
chip.hidden = true
|
|
703
724
|
mini.hidden = true
|
|
704
725
|
return
|
|
705
726
|
}
|
|
706
727
|
// expanded: full email chip in the top bar
|
|
707
|
-
el('ac-email').textContent =
|
|
708
|
-
chip.title = p
|
|
709
|
-
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)
|
|
710
731
|
chip.hidden = false
|
|
711
732
|
// collapsed: short name + plan in the mini block (email won't fit at 116px)
|
|
712
|
-
el('mini-acct-name').textContent = p
|
|
713
|
-
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)
|
|
714
735
|
mini.hidden = false
|
|
715
736
|
}
|
|
716
737
|
function setPlan(node, plan) {
|
|
@@ -756,6 +777,8 @@ function accountRow(acc, a) {
|
|
|
756
777
|
function renderAccounts(a) {
|
|
757
778
|
lastAccounts = a
|
|
758
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
|
+
|
|
759
782
|
if (el('acc-menu').hidden) return
|
|
760
783
|
// the switch is done: the chip now names the account the menu was pointing at
|
|
761
784
|
if (switching && !a?.busy) closeAccountMenu()
|
package/renderer/style.css
CHANGED
|
@@ -314,6 +314,19 @@ body.one-account .ac-caret {
|
|
|
314
314
|
body.collapsed #account-chip {
|
|
315
315
|
display: none;
|
|
316
316
|
}
|
|
317
|
+
/* collapsed: the account rides up on the button line, like the chip does */
|
|
318
|
+
body.collapsed #mini-acct {
|
|
319
|
+
position: absolute;
|
|
320
|
+
top: 6px;
|
|
321
|
+
left: 10px;
|
|
322
|
+
z-index: 10;
|
|
323
|
+
height: 19px;
|
|
324
|
+
margin-bottom: 0;
|
|
325
|
+
justify-content: flex-start;
|
|
326
|
+
}
|
|
327
|
+
body.collapsed #mini-acct-name {
|
|
328
|
+
max-width: 62px;
|
|
329
|
+
}
|
|
317
330
|
|
|
318
331
|
/* settings panel */
|
|
319
332
|
#settings {
|
|
@@ -1076,7 +1089,7 @@ body.collapsed #mini {
|
|
|
1076
1089
|
display: block;
|
|
1077
1090
|
}
|
|
1078
1091
|
body.collapsed #card {
|
|
1079
|
-
width:
|
|
1092
|
+
width: 144px;
|
|
1080
1093
|
padding: 12px 12px 13px;
|
|
1081
1094
|
}
|
|
1082
1095
|
body.collapsed #stage {
|
|
@@ -1148,7 +1161,7 @@ body.settings-open #pet {
|
|
|
1148
1161
|
|
|
1149
1162
|
/* collapsed: just the face, on the warm card (like before) */
|
|
1150
1163
|
body.collapsed #stage {
|
|
1151
|
-
height:
|
|
1164
|
+
height: 120px;
|
|
1152
1165
|
margin-top: 9px;
|
|
1153
1166
|
background: transparent;
|
|
1154
1167
|
box-shadow: none;
|
|
@@ -1171,19 +1184,95 @@ body.collapsed #gears,
|
|
|
1171
1184
|
body.collapsed #glasses {
|
|
1172
1185
|
display: none;
|
|
1173
1186
|
}
|
|
1187
|
+
/* collapsed: the pet sits a bit high, so the % fits under it inside the ring */
|
|
1174
1188
|
body.collapsed #pet {
|
|
1175
1189
|
position: static;
|
|
1176
1190
|
left: auto;
|
|
1177
1191
|
bottom: auto;
|
|
1178
|
-
|
|
1192
|
+
transform: translateY(-8px);
|
|
1193
|
+
zoom: 0.86;
|
|
1194
|
+
}
|
|
1195
|
+
/* the ring is a fixed frame, so the usual bob reads as jumping in there */
|
|
1196
|
+
body.collapsed #claude {
|
|
1197
|
+
animation: bob-mini 3.6s ease-in-out infinite;
|
|
1198
|
+
}
|
|
1199
|
+
@keyframes bob-mini {
|
|
1200
|
+
0%,
|
|
1201
|
+
100% {
|
|
1202
|
+
transform: translateY(0);
|
|
1203
|
+
}
|
|
1204
|
+
50% {
|
|
1205
|
+
transform: translateY(-1.5px);
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
/* the ring: session % drawn around the pet, only when there's a real % to show */
|
|
1209
|
+
#ring {
|
|
1210
|
+
display: none;
|
|
1211
|
+
}
|
|
1212
|
+
body.collapsed #ring {
|
|
1213
|
+
display: block;
|
|
1214
|
+
position: absolute;
|
|
1215
|
+
left: 50%;
|
|
1216
|
+
top: 50%;
|
|
1217
|
+
width: 118px;
|
|
1218
|
+
height: 118px;
|
|
1219
|
+
transform: translate(-50%, -50%);
|
|
1220
|
+
pointer-events: none;
|
|
1221
|
+
z-index: 1;
|
|
1222
|
+
}
|
|
1223
|
+
.ring-track {
|
|
1224
|
+
fill: none;
|
|
1225
|
+
stroke: var(--track);
|
|
1226
|
+
stroke-width: 3.5;
|
|
1227
|
+
}
|
|
1228
|
+
#ring-fill {
|
|
1229
|
+
fill: none;
|
|
1230
|
+
stroke: var(--green);
|
|
1231
|
+
stroke-width: 3.5;
|
|
1232
|
+
stroke-linecap: round;
|
|
1233
|
+
transform: rotate(-90deg);
|
|
1234
|
+
transform-origin: 50% 50%;
|
|
1235
|
+
stroke-dasharray: 282.7;
|
|
1236
|
+
stroke-dashoffset: 282.7; /* set from JS */
|
|
1237
|
+
transition:
|
|
1238
|
+
stroke-dashoffset 0.5s ease,
|
|
1239
|
+
stroke 0.3s ease;
|
|
1240
|
+
}
|
|
1241
|
+
#ring-fill.high {
|
|
1242
|
+
stroke: #e0553f;
|
|
1243
|
+
}
|
|
1244
|
+
#ring-pct {
|
|
1245
|
+
display: none;
|
|
1246
|
+
}
|
|
1247
|
+
body.collapsed #ring-pct {
|
|
1248
|
+
display: block;
|
|
1249
|
+
position: absolute;
|
|
1250
|
+
left: 0;
|
|
1251
|
+
right: 0;
|
|
1252
|
+
bottom: 12px;
|
|
1253
|
+
z-index: 3;
|
|
1254
|
+
text-align: center;
|
|
1255
|
+
font-size: 13px;
|
|
1256
|
+
font-weight: 700;
|
|
1257
|
+
color: var(--text);
|
|
1258
|
+
font-variant-numeric: tabular-nums;
|
|
1259
|
+
pointer-events: none;
|
|
1260
|
+
}
|
|
1261
|
+
#ring-pct.high {
|
|
1262
|
+
color: #e0553f;
|
|
1263
|
+
}
|
|
1264
|
+
/* the ring already says it — no need to repeat it under the pet */
|
|
1265
|
+
body.collapsed #mini-pct-row {
|
|
1266
|
+
display: none;
|
|
1179
1267
|
}
|
|
1180
1268
|
body.collapsed #shadow {
|
|
1181
1269
|
display: block;
|
|
1182
1270
|
position: absolute;
|
|
1183
|
-
|
|
1271
|
+
/* pinned under the pet's feet, which the ring lifted off the stage floor */
|
|
1272
|
+
top: calc(50% + 23px);
|
|
1184
1273
|
left: 50%;
|
|
1185
|
-
width:
|
|
1186
|
-
height:
|
|
1274
|
+
width: 48px;
|
|
1275
|
+
height: 8px;
|
|
1187
1276
|
background: radial-gradient(ellipse, rgba(0, 0, 0, 0.45), rgba(0, 0, 0, 0));
|
|
1188
1277
|
transform: translateX(-50%);
|
|
1189
1278
|
}
|