clauddy 1.20.0 → 1.21.0

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 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's face (showing the live session %); the **⤢** button or a double-click on the pet expands it back
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
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "clauddy",
3
3
  "desktopName": "clauddy.desktop",
4
- "version": "1.20.0",
4
+ "version": "1.21.0",
5
5
  "description": "A cute desktop pet that tracks your Claude Code usage",
6
6
  "main": "main.js",
7
7
  "bin": {
@@ -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 ? 140 : 276) * zoom
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
@@ -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: 116px;
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: 98px;
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
- zoom: 1;
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
- bottom: 2px;
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: 60px;
1186
- height: 9px;
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
  }