clauddy 1.7.0 → 1.9.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 +34 -3
- package/bin/clauddy.js +8 -0
- package/config.json +2 -1
- package/main.js +150 -8
- package/package.json +4 -2
- package/renderer/index.html +121 -0
- package/renderer/pet.js +60 -3
- package/renderer/style.css +488 -11
- package/usage.js +75 -2
package/README.md
CHANGED
|
@@ -48,6 +48,27 @@ The token is saved locally (see [Data & privacy](#data--privacy)) and refreshed
|
|
|
48
48
|
|
|
49
49
|
Plus a welcome **wave** on launch. You can [poke the pet from the terminal](#play-with-the-pet) too.
|
|
50
50
|
|
|
51
|
+
### What Claude's up to
|
|
52
|
+
|
|
53
|
+
While Claude Code is actively working, the pet sets up a little desk scene that
|
|
54
|
+
mirrors **what it's doing right now** — inferred from your local logs (the last
|
|
55
|
+
tool it used). The status line names the activity, and three of them get their
|
|
56
|
+
own animated scene:
|
|
57
|
+
|
|
58
|
+
<table>
|
|
59
|
+
<tr>
|
|
60
|
+
<td align="center"><img src="https://raw.githubusercontent.com/renatoaug/claude-usage-monitor/main/docs/media/reading.gif" width="280" alt="reading" /><br /><b>reading</b><br /><sub>puts on glasses & flips through docs</sub></td>
|
|
61
|
+
<td align="center"><img src="https://raw.githubusercontent.com/renatoaug/claude-usage-monitor/main/docs/media/editing.gif" width="280" alt="editing" /><br /><b>editing</b><br /><sub>types at the laptop, coffee in reach</sub></td>
|
|
62
|
+
</tr>
|
|
63
|
+
<tr>
|
|
64
|
+
<td colspan="2" align="center"><img src="https://raw.githubusercontent.com/renatoaug/claude-usage-monitor/main/docs/media/running.gif" width="280" alt="running" /><br /><b>running</b><br /><sub>watches a task log tick through its checks</sub></td>
|
|
65
|
+
</tr>
|
|
66
|
+
</table>
|
|
67
|
+
|
|
68
|
+
Other activities — **planning**, **researching**, **delegating**, **waiting** —
|
|
69
|
+
show up in the status line as they happen. When Claude goes quiet, the pet drops
|
|
70
|
+
back to plain **working** / **idle**.
|
|
71
|
+
|
|
51
72
|
## Install
|
|
52
73
|
|
|
53
74
|
**macOS (Apple Silicon)** is the first-class build. **Windows (x64)** works too, and **Linux** is next. The `bunx`/`npx` route below runs on all of them today.
|
|
@@ -100,10 +121,19 @@ Coming soon — the build is already wired for `tar.gz` and AppImage. In the mea
|
|
|
100
121
|
|
|
101
122
|
- **Drag** the widget anywhere on screen
|
|
102
123
|
- **–** minimizes to just the pet's face (showing the live session %); the **⤢** button or a double-click on the pet expands it back
|
|
103
|
-
- **⚙** opens settings (log in, toggle alerts, set thresholds)
|
|
124
|
+
- **⚙** opens settings (log in, toggle alerts, set thresholds, pick the display mode)
|
|
104
125
|
- **↗** opens the official Usage page
|
|
105
126
|
- **×** quits
|
|
106
127
|
|
|
128
|
+
### Floating or menu bar
|
|
129
|
+
|
|
130
|
+
Under **⚙ Settings → Display** you can pick where Clauddy lives:
|
|
131
|
+
|
|
132
|
+
- **Floating pet** — the always-on widget in the corner (default).
|
|
133
|
+
- **Menu bar** — a small pet icon in the macOS menu bar showing your live session **%** (it turns 🔥 near your limit). Click it to pop open the full pet + usage panel; click away to dismiss. Right-click for a quick menu.
|
|
134
|
+
|
|
135
|
+
Switching is instant — no restart. (On Windows/Linux the icon lives in the system tray; the live % shows in its tooltip.)
|
|
136
|
+
|
|
107
137
|
## Alerts
|
|
108
138
|
|
|
109
139
|
Optional **macOS notifications** when your session or weekly usage crosses the thresholds you set (default **80%** and **95%**) — e.g. _"Your session is over 80% — now at 82%"_. They re-arm automatically once usage drops back below a threshold (after a reset). Toggle them and edit the thresholds in **⚙ Settings**.
|
|
@@ -114,11 +144,12 @@ Settings saved from the UI live in `~/.claude-usage-monitor/config.json`, so you
|
|
|
114
144
|
|
|
115
145
|
```jsonc
|
|
116
146
|
{
|
|
147
|
+
"mode": "floating", // "floating" pet in the corner, or "menubar" popover
|
|
117
148
|
"alerts": true, // macOS notifications on/off
|
|
118
149
|
"alertThresholds": [80, 95], // notify when session/week cross these % (two levels)
|
|
119
150
|
"fireThreshold": 90, // session % at which the pet catches fire (maxed out stays 100)
|
|
120
151
|
"pollIntervalMs": 4000, // how often local logs are re-read
|
|
121
|
-
"activeThresholdMs":
|
|
152
|
+
"activeThresholdMs": 20000, // "active" if Claude wrote to its logs within this window
|
|
122
153
|
"sleepThresholdMs": 300000, // "sleeping" after this much idle time (5 min)
|
|
123
154
|
}
|
|
124
155
|
```
|
|
@@ -145,7 +176,7 @@ live. (Installed globally? Drop the `bunx`: `clauddy poke`. Working on the repo?
|
|
|
145
176
|
## How it works
|
|
146
177
|
|
|
147
178
|
- **`main.js`** — Electron main process: frameless, transparent, always-on-top window; polls usage; fires macOS notifications; watches `config.json` and `debug.json`.
|
|
148
|
-
- **`usage.js`** — reads `~/.claude/projects/**/*.jsonl`, sums tokens per model/day, detects the rolling 5-hour session window,
|
|
179
|
+
- **`usage.js`** — reads `~/.claude/projects/**/*.jsonl`, sums tokens per model/day, detects the rolling 5-hour session window, the working/sleeping status, and which activity (reading/editing/running/…) Claude is on from its latest tool use.
|
|
149
180
|
- **`auth.js`** — OAuth login (PKCE, same public client as Claude Code) that fetches the authoritative usage %. Token stored locally, never committed.
|
|
150
181
|
- **`renderer/`** — the pet itself: an SVG pixel sprite, CSS animations, and the Web Animations API for particles.
|
|
151
182
|
- **`make-icon.js`** — generates the app icon from the pixel sprite (`build/icon.icns`).
|
package/bin/clauddy.js
CHANGED
|
@@ -22,6 +22,14 @@ const STATES = [
|
|
|
22
22
|
'celebrate',
|
|
23
23
|
'auto',
|
|
24
24
|
'clear',
|
|
25
|
+
// activity states (what Claude Code is doing)
|
|
26
|
+
'editing',
|
|
27
|
+
'reading',
|
|
28
|
+
'planning',
|
|
29
|
+
'running',
|
|
30
|
+
'researching',
|
|
31
|
+
'delegating',
|
|
32
|
+
'waiting',
|
|
25
33
|
]
|
|
26
34
|
const arg = process.argv[2]
|
|
27
35
|
|
package/config.json
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_note": "Defaults bundled with the app. User settings are saved to ~/.claude-usage-monitor/config.json. Session/weekly % come from your account (log in via Settings).",
|
|
3
|
+
"mode": "floating",
|
|
3
4
|
"alerts": true,
|
|
4
5
|
"alertThresholds": [80, 95],
|
|
5
6
|
"fireThreshold": 90,
|
|
6
7
|
"pollIntervalMs": 4000,
|
|
7
|
-
"activeThresholdMs":
|
|
8
|
+
"activeThresholdMs": 20000,
|
|
8
9
|
"sleepThresholdMs": 300000
|
|
9
10
|
}
|
package/main.js
CHANGED
|
@@ -1,4 +1,14 @@
|
|
|
1
|
-
const {
|
|
1
|
+
const {
|
|
2
|
+
app,
|
|
3
|
+
BrowserWindow,
|
|
4
|
+
ipcMain,
|
|
5
|
+
screen,
|
|
6
|
+
Notification,
|
|
7
|
+
shell,
|
|
8
|
+
Tray,
|
|
9
|
+
Menu,
|
|
10
|
+
nativeImage,
|
|
11
|
+
} = require('electron')
|
|
2
12
|
const path = require('node:path')
|
|
3
13
|
const fs = require('node:fs')
|
|
4
14
|
const os = require('node:os')
|
|
@@ -30,12 +40,20 @@ let config
|
|
|
30
40
|
let doTick = null
|
|
31
41
|
const W = 276
|
|
32
42
|
|
|
43
|
+
// menu-bar (tray) mode state
|
|
44
|
+
let tray = null
|
|
45
|
+
let currentMode = 'floating' // 'floating' widget | 'menubar' popover
|
|
46
|
+
let trayBounds = null // last known tray icon rect, to anchor the popover
|
|
47
|
+
let lastBlurHide = 0 // debounce: ignore the tray click that dismissed the popover
|
|
48
|
+
let sessionPct = null // authoritative session % shown in the tray title
|
|
49
|
+
|
|
33
50
|
function publicConfig(c) {
|
|
34
51
|
return {
|
|
35
52
|
plan: c.plan,
|
|
36
53
|
sessionTokenBudget: c.sessionTokenBudget,
|
|
37
54
|
weeklyTokenBudget: c.weeklyTokenBudget,
|
|
38
55
|
weeklyAnchorIso: c.weeklyAnchorIso,
|
|
56
|
+
mode: c.mode,
|
|
39
57
|
alerts: c.alerts,
|
|
40
58
|
alertThresholds: c.alertThresholds,
|
|
41
59
|
fireThreshold: c.fireThreshold,
|
|
@@ -48,11 +66,12 @@ function loadConfig() {
|
|
|
48
66
|
sessionTokenBudget: 630000000,
|
|
49
67
|
weeklyTokenBudget: 3450000000,
|
|
50
68
|
weeklyAnchorIso: null,
|
|
69
|
+
mode: 'floating', // 'floating' widget or 'menubar' popover
|
|
51
70
|
alerts: true,
|
|
52
71
|
alertThresholds: [80, 95],
|
|
53
72
|
fireThreshold: 90, // session % at which the pet catches fire (tired still fixed at 100)
|
|
54
73
|
pollIntervalMs: 4000,
|
|
55
|
-
activeThresholdMs:
|
|
74
|
+
activeThresholdMs: 20000,
|
|
56
75
|
sleepThresholdMs: 300000,
|
|
57
76
|
}
|
|
58
77
|
for (const p of [EXTERNAL_CONFIG, path.join(__dirname, 'config.json')]) {
|
|
@@ -105,6 +124,7 @@ function createWindow() {
|
|
|
105
124
|
transparent: true,
|
|
106
125
|
backgroundColor: '#00000000', // fully transparent — Windows needs this or the window paints black
|
|
107
126
|
resizable: false,
|
|
127
|
+
show: false, // applyMode() reveals it (floating) or keeps it a hidden popover (menubar)
|
|
108
128
|
alwaysOnTop: true,
|
|
109
129
|
skipTaskbar: true,
|
|
110
130
|
hasShadow: false,
|
|
@@ -120,6 +140,14 @@ function createWindow() {
|
|
|
120
140
|
win.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true })
|
|
121
141
|
win.loadFile(path.join(__dirname, 'renderer', 'index.html'))
|
|
122
142
|
|
|
143
|
+
// in menu-bar mode the popover dismisses when it loses focus (click elsewhere)
|
|
144
|
+
win.on('blur', () => {
|
|
145
|
+
if (currentMode === 'menubar' && win.isVisible()) {
|
|
146
|
+
win.hide()
|
|
147
|
+
lastBlurHide = Date.now()
|
|
148
|
+
}
|
|
149
|
+
})
|
|
150
|
+
|
|
123
151
|
const tick = () => {
|
|
124
152
|
if (!win || win.isDestroyed()) return
|
|
125
153
|
try {
|
|
@@ -140,7 +168,115 @@ function createWindow() {
|
|
|
140
168
|
startUsagePoll()
|
|
141
169
|
sendProfile()
|
|
142
170
|
watchDebug()
|
|
171
|
+
applyMode(config.mode) // reveal the widget, or set up the tray + popover
|
|
172
|
+
})
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ---- menu-bar (tray) mode ----------------------------------------------------
|
|
176
|
+
// Floating mode: the widget lives bottom-right, always visible. Menu-bar mode:
|
|
177
|
+
// the same window becomes a popover shown under a tray icon on click. Switching
|
|
178
|
+
// is live — no relaunch — so the Settings toggle applies immediately.
|
|
179
|
+
function applyMode(mode) {
|
|
180
|
+
const next = mode === 'menubar' ? 'menubar' : 'floating'
|
|
181
|
+
const changed = next !== currentMode
|
|
182
|
+
currentMode = next
|
|
183
|
+
if (currentMode === 'menubar') {
|
|
184
|
+
ensureTray()
|
|
185
|
+
win.setSkipTaskbar(true)
|
|
186
|
+
if (changed && win.isVisible()) win.hide() // hide only when switching in
|
|
187
|
+
} else {
|
|
188
|
+
destroyTray()
|
|
189
|
+
if (changed || !win.isVisible()) {
|
|
190
|
+
positionFloating()
|
|
191
|
+
win.show()
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
updateTray()
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function ensureTray() {
|
|
198
|
+
if (tray) return
|
|
199
|
+
const img = nativeImage.createFromPath(path.join(__dirname, 'build', 'trayTemplate.png'))
|
|
200
|
+
img.setTemplateImage(true) // let macOS recolor it for the light/dark menu bar
|
|
201
|
+
tray = new Tray(img)
|
|
202
|
+
tray.setToolTip('Clauddy')
|
|
203
|
+
tray.on('click', (_e, bounds) => {
|
|
204
|
+
trayBounds = bounds
|
|
205
|
+
togglePopover()
|
|
143
206
|
})
|
|
207
|
+
tray.on('right-click', () => tray.popUpContextMenu(trayMenu()))
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function destroyTray() {
|
|
211
|
+
if (tray) {
|
|
212
|
+
tray.destroy()
|
|
213
|
+
tray = null
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function trayMenu() {
|
|
218
|
+
return Menu.buildFromTemplate([
|
|
219
|
+
{ label: 'Open Clauddy', click: () => showPopover() },
|
|
220
|
+
{
|
|
221
|
+
label: 'Open Usage page',
|
|
222
|
+
click: () => shell.openExternal('https://claude.ai/settings/usage'),
|
|
223
|
+
},
|
|
224
|
+
{ type: 'separator' },
|
|
225
|
+
{ label: 'Quit Clauddy', click: () => app.quit() },
|
|
226
|
+
])
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function togglePopover() {
|
|
230
|
+
if (Date.now() - lastBlurHide < 250) return // this click just dismissed it
|
|
231
|
+
if (win.isVisible()) win.hide()
|
|
232
|
+
else showPopover()
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function showPopover() {
|
|
236
|
+
positionUnderTray()
|
|
237
|
+
win.show()
|
|
238
|
+
win.focus()
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// center the popover under the tray icon, kept on-screen
|
|
242
|
+
function positionUnderTray() {
|
|
243
|
+
const b = win.getBounds()
|
|
244
|
+
const { workAreaSize } = screen.getPrimaryDisplay()
|
|
245
|
+
let x = workAreaSize.width - b.width - 24
|
|
246
|
+
let y = 24
|
|
247
|
+
if (trayBounds?.width) {
|
|
248
|
+
x = Math.round(trayBounds.x + trayBounds.width / 2 - b.width / 2)
|
|
249
|
+
y = Math.round(trayBounds.y + trayBounds.height)
|
|
250
|
+
}
|
|
251
|
+
x = Math.max(8, Math.min(x, workAreaSize.width - b.width - 8))
|
|
252
|
+
win.setPosition(x, y)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function positionFloating() {
|
|
256
|
+
const { workAreaSize } = screen.getPrimaryDisplay()
|
|
257
|
+
const b = win.getBounds()
|
|
258
|
+
win.setPosition(workAreaSize.width - b.width - 24, workAreaSize.height - b.height - 24)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// the tray shows the live session % (macOS title), turning 🔥 near the limit
|
|
262
|
+
function updateTray() {
|
|
263
|
+
if (!tray) return
|
|
264
|
+
if (sessionPct == null) {
|
|
265
|
+
if (process.platform === 'darwin') tray.setTitle('')
|
|
266
|
+
tray.setToolTip('Clauddy — connect your account for live %')
|
|
267
|
+
return
|
|
268
|
+
}
|
|
269
|
+
const pct = Math.round(sessionPct)
|
|
270
|
+
const hot = pct >= (config?.fireThreshold ?? 90)
|
|
271
|
+
if (process.platform === 'darwin') tray.setTitle(hot ? ` ${pct}% 🔥` : ` ${pct}%`)
|
|
272
|
+
tray.setToolTip(`Clauddy — session ${pct}%`)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// send real usage to the renderer and refresh the tray title in one place
|
|
276
|
+
function pushRealUsage(u) {
|
|
277
|
+
sessionPct = u?.session ? u.session.pct : null
|
|
278
|
+
updateTray()
|
|
279
|
+
if (win && !win.isDestroyed()) win.webContents.send('real-usage', u)
|
|
144
280
|
}
|
|
145
281
|
|
|
146
282
|
// resize the window to fit the content
|
|
@@ -149,8 +285,12 @@ ipcMain.on('resize', (_e, w, h) => {
|
|
|
149
285
|
const width = Math.max(100, Math.round(w))
|
|
150
286
|
const height = Math.max(110, Math.round(h))
|
|
151
287
|
win.setContentSize(width, height)
|
|
152
|
-
|
|
153
|
-
|
|
288
|
+
if (currentMode === 'menubar') {
|
|
289
|
+
if (win.isVisible()) positionUnderTray() // keep it anchored under the tray
|
|
290
|
+
} else {
|
|
291
|
+
const { workAreaSize } = screen.getPrimaryDisplay()
|
|
292
|
+
win.setPosition(workAreaSize.width - width - 24, workAreaSize.height - height - 24)
|
|
293
|
+
}
|
|
154
294
|
})
|
|
155
295
|
|
|
156
296
|
ipcMain.on('open-usage', () => shell.openExternal('https://claude.ai/settings/usage'))
|
|
@@ -177,15 +317,15 @@ async function pollUsage() {
|
|
|
177
317
|
try {
|
|
178
318
|
const u = await auth.fetchUsage()
|
|
179
319
|
usageBackoff = 5 * 60 * 1000
|
|
180
|
-
|
|
320
|
+
pushRealUsage(u)
|
|
181
321
|
} catch (e) {
|
|
182
322
|
if (e && e.status === 429) {
|
|
183
323
|
usageBackoff = Math.min(usageBackoff * 2, 30 * 60 * 1000)
|
|
184
324
|
} else if (e && e.status === 401) {
|
|
185
325
|
auth.clear()
|
|
326
|
+
pushRealUsage(null)
|
|
186
327
|
if (win && !win.isDestroyed()) {
|
|
187
328
|
win.webContents.send('auth-state', { connected: false })
|
|
188
|
-
win.webContents.send('real-usage', null)
|
|
189
329
|
win.webContents.send('profile', null)
|
|
190
330
|
}
|
|
191
331
|
}
|
|
@@ -220,7 +360,7 @@ ipcMain.on('auth-code', async (_e, code) => {
|
|
|
220
360
|
try {
|
|
221
361
|
const u = await auth.fetchUsage() // validate the token
|
|
222
362
|
ok()
|
|
223
|
-
|
|
363
|
+
pushRealUsage(u)
|
|
224
364
|
} catch (e) {
|
|
225
365
|
if (e && e.status === 429) {
|
|
226
366
|
// token is fine, the usage endpoint is just throttled — keep it and retry later
|
|
@@ -239,9 +379,9 @@ ipcMain.on('auth-code', async (_e, code) => {
|
|
|
239
379
|
ipcMain.on('auth-logout', () => {
|
|
240
380
|
auth.clear()
|
|
241
381
|
clearTimeout(usageTimer)
|
|
382
|
+
pushRealUsage(null)
|
|
242
383
|
if (win && !win.isDestroyed()) {
|
|
243
384
|
win.webContents.send('auth-state', { connected: false })
|
|
244
|
-
win.webContents.send('real-usage', null)
|
|
245
385
|
win.webContents.send('profile', null)
|
|
246
386
|
}
|
|
247
387
|
})
|
|
@@ -263,6 +403,7 @@ ipcMain.on('save-config', (_e, patch) => {
|
|
|
263
403
|
armed.clear() // re-arm alerts with new thresholds
|
|
264
404
|
if (doTick) doTick()
|
|
265
405
|
if (win && !win.isDestroyed()) win.webContents.send('config', publicConfig(config))
|
|
406
|
+
applyMode(config.mode) // switch between floating widget and menu-bar popover live
|
|
266
407
|
})
|
|
267
408
|
|
|
268
409
|
ipcMain.on('quit', () => app.quit())
|
|
@@ -280,6 +421,7 @@ app.whenReady().then(() => {
|
|
|
280
421
|
app.on('window-all-closed', () => {
|
|
281
422
|
if (pollTimer) clearInterval(pollTimer)
|
|
282
423
|
fs.unwatchFile(DEBUG_FILE)
|
|
424
|
+
destroyTray()
|
|
283
425
|
app.quit()
|
|
284
426
|
})
|
|
285
427
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clauddy",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.9.0",
|
|
4
4
|
"description": "A cute desktop pet that tracks your Claude Code usage",
|
|
5
5
|
"main": "main.js",
|
|
6
6
|
"bin": {
|
|
@@ -68,7 +68,9 @@
|
|
|
68
68
|
"usage.js",
|
|
69
69
|
"auth.js",
|
|
70
70
|
"config.json",
|
|
71
|
-
"renderer/**"
|
|
71
|
+
"renderer/**",
|
|
72
|
+
"build/trayTemplate.png",
|
|
73
|
+
"build/trayTemplate@2x.png"
|
|
72
74
|
],
|
|
73
75
|
"mac": {
|
|
74
76
|
"category": "public.app-category.developer-tools",
|
package/renderer/index.html
CHANGED
|
@@ -69,12 +69,58 @@
|
|
|
69
69
|
</g>
|
|
70
70
|
</svg>
|
|
71
71
|
<svg id="scene" viewBox="0 0 216 135" preserveAspectRatio="none"></svg>
|
|
72
|
+
<!-- editing mode: a coding desk (laptop + coffee + plant) behind the pet -->
|
|
73
|
+
<svg id="desk" viewBox="0 0 216 135" preserveAspectRatio="none">
|
|
74
|
+
<rect class="dk-desk" x="4" y="113" width="208" height="3" />
|
|
75
|
+
<!-- laptop (shrunk + shifted right for breathing room) -->
|
|
76
|
+
<g class="dk-laptop" transform="translate(49 28) scale(0.7)">
|
|
77
|
+
<rect class="dk-screen" x="110" y="64" width="66" height="48" rx="3" />
|
|
78
|
+
<rect class="dk-glass" x="114" y="68" width="58" height="40" rx="2" />
|
|
79
|
+
<polygon class="dk-deck2" points="106,110 180,110 190,121 96,121" />
|
|
80
|
+
<rect class="dk-keys" x="112" y="112" width="7" height="2.4" rx="0.5" />
|
|
81
|
+
<rect class="dk-keys" x="121" y="112" width="7" height="2.4" rx="0.5" />
|
|
82
|
+
<rect class="dk-keys" x="130" y="112" width="7" height="2.4" rx="0.5" />
|
|
83
|
+
<rect class="dk-keys" x="139" y="112" width="7" height="2.4" rx="0.5" />
|
|
84
|
+
<rect class="dk-keys" x="148" y="112" width="7" height="2.4" rx="0.5" />
|
|
85
|
+
<rect class="dk-keys" x="157" y="112" width="7" height="2.4" rx="0.5" />
|
|
86
|
+
<rect class="dk-keys" x="109" y="115.5" width="8" height="2.6" rx="0.5" />
|
|
87
|
+
<rect class="dk-keys" x="119" y="115.5" width="8" height="2.6" rx="0.5" />
|
|
88
|
+
<rect class="dk-keys" x="129" y="115.5" width="18" height="2.6" rx="0.5" />
|
|
89
|
+
<rect class="dk-keys" x="149" y="115.5" width="8" height="2.6" rx="0.5" />
|
|
90
|
+
<rect class="dk-keys" x="159" y="115.5" width="8" height="2.6" rx="0.5" />
|
|
91
|
+
<rect class="c-grn" x="119" y="75" width="4" height="3" />
|
|
92
|
+
<rect class="c-grn" x="126" y="75" width="22" height="3" />
|
|
93
|
+
<rect class="c-grn" x="119" y="82" width="30" height="3" />
|
|
94
|
+
<rect class="c-yel" x="123" y="89" width="26" height="3" />
|
|
95
|
+
<rect class="c-blu" x="119" y="96" width="14" height="3" />
|
|
96
|
+
<rect class="c-yel" x="136" y="96" width="12" height="3" />
|
|
97
|
+
<rect class="c-grn" x="119" y="103" width="10" height="3" />
|
|
98
|
+
<rect class="c-cur" x="132" y="103" width="2.5" height="3.6" />
|
|
99
|
+
</g>
|
|
100
|
+
</svg>
|
|
101
|
+
<!-- potted plant, shared by the editing + reading desks -->
|
|
102
|
+
<svg id="plant" viewBox="0 0 216 135" preserveAspectRatio="none">
|
|
103
|
+
<polygon class="dk-pot" points="188,102 208,102 205,113 191,113" />
|
|
104
|
+
<path class="dk-leaf" d="M198 103 q-11 -7 -13 -21 q11 4 13 21" />
|
|
105
|
+
<path class="dk-leaf" d="M198 103 q11 -7 13 -21 q-11 4 -13 21" />
|
|
106
|
+
<path class="dk-leaf" d="M198 103 q-5 -12 -3 -25 q7 9 3 25" />
|
|
107
|
+
<path class="dk-leaf" d="M198 103 q5 -12 3 -25 q-7 9 -3 25" />
|
|
108
|
+
<path class="dk-leaf-c" d="M198 103 q0 -14 0 -27 q3 13 0 27" />
|
|
109
|
+
</svg>
|
|
72
110
|
<div id="shadow"></div>
|
|
73
111
|
<div id="pet">
|
|
74
112
|
<svg id="claude" viewBox="0 0 100 90" width="86" height="77">
|
|
75
113
|
<g id="body"></g>
|
|
76
114
|
<g id="eyes"></g>
|
|
77
115
|
<rect id="mouth" x="38" y="54" width="24" height="10" rx="2" />
|
|
116
|
+
<!-- reading glasses (reading mode) -->
|
|
117
|
+
<g id="glasses">
|
|
118
|
+
<circle class="gl-lens" cx="35" cy="35" r="8.5" />
|
|
119
|
+
<circle class="gl-lens" cx="65" cy="35" r="8.5" />
|
|
120
|
+
<path class="gl-bridge" d="M43 34 q7 -3 14 0" />
|
|
121
|
+
<line class="gl-arm" x1="26.5" y1="35" x2="20" y2="33" />
|
|
122
|
+
<line class="gl-arm" x1="73.5" y1="35" x2="80" y2="33" />
|
|
123
|
+
</g>
|
|
78
124
|
<!-- mechanic mode (settings): hard hat + screwdriver -->
|
|
79
125
|
<g id="robot">
|
|
80
126
|
<!-- yellow hard hat: pixel-art bitmap, built in pet.js (buildHat) -->
|
|
@@ -100,6 +146,73 @@
|
|
|
100
146
|
<div id="sweat"><i></i><i></i></div>
|
|
101
147
|
<div id="dropzone"></div>
|
|
102
148
|
</div>
|
|
149
|
+
<!-- editing mode, foreground: coffee mug in front of the pet -->
|
|
150
|
+
<!-- mug: its own SVG with UNIFORM scaling (no stage stretch) so it stays perfectly shaped -->
|
|
151
|
+
<svg id="mug" viewBox="0 0 46 44" preserveAspectRatio="xMidYMid meet" aria-hidden="true">
|
|
152
|
+
<g class="dk-steam">
|
|
153
|
+
<path class="dk-wisp" d="M19 15 q4 -4 0 -8 q-4 -4 0 -8" style="animation-delay: 0s" />
|
|
154
|
+
<path class="dk-wisp" d="M25 15 q4 -4 0 -8 q-4 -4 0 -8" style="animation-delay: 0.9s" />
|
|
155
|
+
</g>
|
|
156
|
+
<path class="mug-handle" d="M31 23 q8 1 8 7 q0 6 -8 6.5" />
|
|
157
|
+
<rect class="mug-body" x="12" y="18" width="19" height="26" rx="2.5" />
|
|
158
|
+
<rect class="mug-rim" x="12.5" y="18" width="18" height="3" rx="1.6" />
|
|
159
|
+
<path class="mug-code" d="M17 26 L14 32 L17 38 M20 38 L25 26 M26 26 L29 32 L26 38" />
|
|
160
|
+
</svg>
|
|
161
|
+
<!-- running mode: a task log — Claude working (uniform scale) -->
|
|
162
|
+
<svg id="terminal" viewBox="0 0 92 64" preserveAspectRatio="xMidYMid meet" aria-hidden="true">
|
|
163
|
+
<rect class="tm-win" x="2" y="2" width="88" height="60" rx="4" />
|
|
164
|
+
<rect class="tm-bar" x="2" y="2" width="88" height="8" rx="4" />
|
|
165
|
+
<rect class="tm-bar" x="2" y="6.5" width="88" height="3.5" />
|
|
166
|
+
<circle class="tm-d1" cx="8" cy="6" r="1.6" />
|
|
167
|
+
<circle class="tm-d2" cx="13" cy="6" r="1.6" />
|
|
168
|
+
<circle class="tm-d3" cx="18" cy="6" r="1.6" />
|
|
169
|
+
<!-- hairline border around the content area only (open at the top bar) -->
|
|
170
|
+
<path class="tm-border" d="M2.4 10 L2.4 58 Q2.4 61.6 6 61.6 L86 61.6 Q89.6 61.6 89.6 58 L89.6 10" />
|
|
171
|
+
<text class="tm-prompt-c" x="7" y="20">❯</text>
|
|
172
|
+
<text class="tm-head" x="13" y="20">Running</text>
|
|
173
|
+
<text class="tm-dots" x="33" y="20">...</text>
|
|
174
|
+
<rect class="tm-cursor" x="43" y="15" width="2.4" height="5.4" />
|
|
175
|
+
<g class="tskA">
|
|
176
|
+
<path class="tm-check" d="M7 30 l1.5 1.5 l2.7 -3.2" />
|
|
177
|
+
<text class="tm-txt" x="14" y="32">Reading files</text>
|
|
178
|
+
</g>
|
|
179
|
+
<g class="tskB">
|
|
180
|
+
<path class="tm-check" d="M7 38 l1.5 1.5 l2.7 -3.2" />
|
|
181
|
+
<text class="tm-txt" x="14" y="40">Editing code</text>
|
|
182
|
+
</g>
|
|
183
|
+
<g class="tskC">
|
|
184
|
+
<path class="tm-check" d="M7 46 l1.5 1.5 l2.7 -3.2" />
|
|
185
|
+
<text class="tm-txt" x="14" y="48">Executing</text>
|
|
186
|
+
</g>
|
|
187
|
+
<g class="tskWait">
|
|
188
|
+
<path class="tm-spin" d="M10.4 54 A2.4 2.4 0 1 1 8 51.6" />
|
|
189
|
+
<text class="tm-txt" x="14" y="56">Waiting</text>
|
|
190
|
+
<text class="tm-dots" x="33" y="56">...</text>
|
|
191
|
+
</g>
|
|
192
|
+
<g class="tskDone">
|
|
193
|
+
<path class="tm-check" d="M7 54 l1.5 1.5 l2.7 -3.2" />
|
|
194
|
+
<text class="tm-done-txt" x="14" y="56">Done</text>
|
|
195
|
+
</g>
|
|
196
|
+
</svg>
|
|
197
|
+
<!-- reading mode: a "docs.md" markdown file being read (uniform scale) -->
|
|
198
|
+
<svg id="reading" viewBox="0 0 84 74" preserveAspectRatio="xMidYMid meet" aria-hidden="true">
|
|
199
|
+
<rect class="dc-card" x="2" y="2" width="80" height="70" rx="4" />
|
|
200
|
+
<!-- title bar -->
|
|
201
|
+
<rect class="dc-ico" x="7" y="7" width="7" height="9" rx="1" />
|
|
202
|
+
<rect class="dc-ico-fold" x="11.5" y="7" width="2.5" height="2.5" />
|
|
203
|
+
<text id="dc-fname" class="dc-title" x="17" y="14">README.md</text>
|
|
204
|
+
<line class="dc-divider" x1="6" y1="19" x2="78" y2="19" />
|
|
205
|
+
<!-- content -->
|
|
206
|
+
<text class="dc-h dc-h1" x="7" y="29" style="animation-delay: 0s"># Getting Started</text>
|
|
207
|
+
<rect class="dc-body" x="9" y="32" width="42" height="2" />
|
|
208
|
+
<rect class="dc-body" x="9" y="35.5" width="34" height="2" />
|
|
209
|
+
<text class="dc-h dc-g" x="7" y="45" style="animation-delay: 1s">## Installation</text>
|
|
210
|
+
<rect class="dc-body" x="9" y="48" width="30" height="2" />
|
|
211
|
+
<text class="dc-h dc-b" x="7" y="57" style="animation-delay: 2s">## Usage</text>
|
|
212
|
+
<rect class="dc-body" x="9" y="60" width="40" height="2" />
|
|
213
|
+
<text class="dc-h dc-a" x="7" y="68" style="animation-delay: 3s">## Examples</text>
|
|
214
|
+
<rect class="dc-body" x="9" y="70.5" width="26" height="2" />
|
|
215
|
+
</svg>
|
|
103
216
|
</div>
|
|
104
217
|
|
|
105
218
|
<!-- shown only when collapsed -->
|
|
@@ -192,6 +305,14 @@
|
|
|
192
305
|
</div>
|
|
193
306
|
</div>
|
|
194
307
|
|
|
308
|
+
<div class="set-hint set-hint-left">Where Clauddy lives: a floating pet, or a menu-bar icon with a pop-up.</div>
|
|
309
|
+
<label class="set-field">Display
|
|
310
|
+
<span class="seg" id="set-mode">
|
|
311
|
+
<button type="button" class="seg-btn" data-mode="floating">Floating pet</button>
|
|
312
|
+
<button type="button" class="seg-btn" data-mode="menubar">Menu bar</button>
|
|
313
|
+
</span>
|
|
314
|
+
</label>
|
|
315
|
+
|
|
195
316
|
<label class="set-check"><input id="set-alerts" type="checkbox" /> Alerts</label>
|
|
196
317
|
<div class="set-hint set-hint-left">A macOS notification when your session or weekly usage crosses each level.</div>
|
|
197
318
|
<label class="set-field">Alert thresholds (%)
|
package/renderer/pet.js
CHANGED
|
@@ -114,6 +114,32 @@ const SPRITE = [
|
|
|
114
114
|
})
|
|
115
115
|
})()
|
|
116
116
|
|
|
117
|
+
// reading mode: the doc's filename slowly cycles through project docs, since
|
|
118
|
+
// it's a Claude app "reading" different files (a gentle fade between names)
|
|
119
|
+
;(function cycleDocName() {
|
|
120
|
+
const node = el('dc-fname')
|
|
121
|
+
if (!node) return
|
|
122
|
+
const names = [
|
|
123
|
+
'README.md',
|
|
124
|
+
'ARCHITECTURE.md',
|
|
125
|
+
'api.md',
|
|
126
|
+
'AUTH.md',
|
|
127
|
+
'MIGRATION.md',
|
|
128
|
+
'CONTRIBUTING.md',
|
|
129
|
+
'usage.md',
|
|
130
|
+
'config.md',
|
|
131
|
+
]
|
|
132
|
+
let i = 0
|
|
133
|
+
setInterval(() => {
|
|
134
|
+
node.style.opacity = '0'
|
|
135
|
+
setTimeout(() => {
|
|
136
|
+
i = (i + 1) % names.length
|
|
137
|
+
node.textContent = names[i]
|
|
138
|
+
node.style.opacity = '1'
|
|
139
|
+
}, 260)
|
|
140
|
+
}, 3800)
|
|
141
|
+
})()
|
|
142
|
+
|
|
117
143
|
// helpers
|
|
118
144
|
function fmtTokens(t) {
|
|
119
145
|
t = t || 0
|
|
@@ -413,7 +439,9 @@ function render(d) {
|
|
|
413
439
|
: d.sleeping
|
|
414
440
|
? 'sleeping'
|
|
415
441
|
: 'idle'
|
|
416
|
-
|
|
442
|
+
const acts = ['editing', 'reading', 'planning', 'running', 'researching', 'delegating', 'waiting']
|
|
443
|
+
let curActivity = d.activity
|
|
444
|
+
// dev override from `./pet <state>` (base states or an activity name)
|
|
417
445
|
if (debugState) {
|
|
418
446
|
const map = {
|
|
419
447
|
working: 'working',
|
|
@@ -422,9 +450,20 @@ function render(d) {
|
|
|
422
450
|
tired: 'tired',
|
|
423
451
|
idle: 'idle',
|
|
424
452
|
}
|
|
425
|
-
if (map[debugState])
|
|
453
|
+
if (map[debugState]) {
|
|
454
|
+
st = map[debugState]
|
|
455
|
+
curActivity = null
|
|
456
|
+
} else if (acts.includes(debugState)) {
|
|
457
|
+
st = 'working'
|
|
458
|
+
curActivity = debugState
|
|
459
|
+
}
|
|
426
460
|
}
|
|
427
461
|
setState(st)
|
|
462
|
+
// expose the current activity as a body class so per-action animations can
|
|
463
|
+
// hook it (e.g. body.act-reading). Only while actively working.
|
|
464
|
+
for (const a of acts) {
|
|
465
|
+
document.body.classList.toggle(`act-${a}`, st === 'working' && curActivity === a)
|
|
466
|
+
}
|
|
428
467
|
if (prevState && prevState !== st) {
|
|
429
468
|
if (st === 'sleeping') oneShot('drowse', 700)
|
|
430
469
|
else if (prevState === 'sleeping') oneShot('wake', 700)
|
|
@@ -433,7 +472,7 @@ function render(d) {
|
|
|
433
472
|
|
|
434
473
|
const word =
|
|
435
474
|
st === 'working'
|
|
436
|
-
? 'working'
|
|
475
|
+
? curActivity || 'working' // show what Claude's doing (editing/reading/…)
|
|
437
476
|
: st === 'sleeping'
|
|
438
477
|
? 'sleeping'
|
|
439
478
|
: st === 'stressed'
|
|
@@ -521,6 +560,7 @@ window.api.onError((msg) => {
|
|
|
521
560
|
})
|
|
522
561
|
window.api.onConfig((cfg) => {
|
|
523
562
|
currentConfig = cfg || {}
|
|
563
|
+
document.body.classList.toggle('is-menubar', currentConfig.mode === 'menubar')
|
|
524
564
|
})
|
|
525
565
|
window.api.onRealUsage((u) => {
|
|
526
566
|
realUsage = u || null
|
|
@@ -603,8 +643,16 @@ el('acc-logout').addEventListener('click', () => window.api.authLogout())
|
|
|
603
643
|
// settings panel
|
|
604
644
|
// snapshot of the editable fields, to tell whether there are unsaved changes
|
|
605
645
|
let settingsBaseline = null
|
|
646
|
+
let selectedMode = 'floating'
|
|
647
|
+
function setModeUI(mode) {
|
|
648
|
+
selectedMode = mode === 'menubar' ? 'menubar' : 'floating'
|
|
649
|
+
for (const b of document.querySelectorAll('#set-mode .seg-btn')) {
|
|
650
|
+
b.classList.toggle('on', b.dataset.mode === selectedMode)
|
|
651
|
+
}
|
|
652
|
+
}
|
|
606
653
|
function snapshotSettings() {
|
|
607
654
|
return JSON.stringify({
|
|
655
|
+
mode: selectedMode,
|
|
608
656
|
alerts: el('set-alerts').checked,
|
|
609
657
|
t1: el('set-t1').value,
|
|
610
658
|
t2: el('set-t2').value,
|
|
@@ -621,6 +669,7 @@ function clearSaveDirty() {
|
|
|
621
669
|
}
|
|
622
670
|
function populateSettings() {
|
|
623
671
|
const c = currentConfig || {}
|
|
672
|
+
setModeUI(c.mode)
|
|
624
673
|
el('set-alerts').checked = c.alerts !== false
|
|
625
674
|
const th = c.alertThresholds || [80, 95]
|
|
626
675
|
el('set-t1').value = th[0] != null ? th[0] : 80
|
|
@@ -653,6 +702,13 @@ for (const id of ['set-alerts', 'set-t1', 'set-t2', 'set-fire']) {
|
|
|
653
702
|
el(id).addEventListener('input', refreshSaveDirty)
|
|
654
703
|
el(id).addEventListener('change', refreshSaveDirty)
|
|
655
704
|
}
|
|
705
|
+
// display-mode segmented control (floating pet | menu bar)
|
|
706
|
+
for (const b of document.querySelectorAll('#set-mode .seg-btn')) {
|
|
707
|
+
b.addEventListener('click', () => {
|
|
708
|
+
setModeUI(b.dataset.mode)
|
|
709
|
+
refreshSaveDirty()
|
|
710
|
+
})
|
|
711
|
+
}
|
|
656
712
|
el('set-cancel').addEventListener('click', () => {
|
|
657
713
|
document.body.classList.remove('settings-open')
|
|
658
714
|
clearSaveDirty()
|
|
@@ -662,6 +718,7 @@ el('set-save').addEventListener('click', () => {
|
|
|
662
718
|
const num = (id) => parseFloat(el(id).value)
|
|
663
719
|
const fire = num('set-fire')
|
|
664
720
|
window.api.saveConfig({
|
|
721
|
+
mode: selectedMode,
|
|
665
722
|
alerts: el('set-alerts').checked,
|
|
666
723
|
alertThresholds: [num('set-t1'), num('set-t2')]
|
|
667
724
|
.filter((n) => n >= 1 && n <= 100)
|
package/renderer/style.css
CHANGED
|
@@ -238,6 +238,38 @@ body.settings-open #settings {
|
|
|
238
238
|
appearance: none;
|
|
239
239
|
margin: 0;
|
|
240
240
|
}
|
|
241
|
+
/* display-mode segmented control (floating pet | menu bar) */
|
|
242
|
+
.seg {
|
|
243
|
+
display: flex;
|
|
244
|
+
gap: 4px;
|
|
245
|
+
margin-top: 4px;
|
|
246
|
+
}
|
|
247
|
+
.seg-btn {
|
|
248
|
+
flex: 1;
|
|
249
|
+
padding: 6px 4px;
|
|
250
|
+
font-size: 11px;
|
|
251
|
+
color: var(--muted);
|
|
252
|
+
background: rgba(255, 255, 255, 0.06);
|
|
253
|
+
border: 0.5px solid var(--hair);
|
|
254
|
+
border-radius: 7px;
|
|
255
|
+
cursor: pointer;
|
|
256
|
+
transition:
|
|
257
|
+
background 0.15s,
|
|
258
|
+
color 0.15s,
|
|
259
|
+
border-color 0.15s;
|
|
260
|
+
}
|
|
261
|
+
.seg-btn:hover {
|
|
262
|
+
color: var(--text);
|
|
263
|
+
}
|
|
264
|
+
.seg-btn.on {
|
|
265
|
+
background: var(--coral);
|
|
266
|
+
border-color: var(--coral);
|
|
267
|
+
color: #fff;
|
|
268
|
+
}
|
|
269
|
+
/* menu-bar mode: the popover auto-sizes, so the minimize control is redundant */
|
|
270
|
+
body.is-menubar #min {
|
|
271
|
+
display: none;
|
|
272
|
+
}
|
|
241
273
|
.set-two {
|
|
242
274
|
display: flex;
|
|
243
275
|
gap: 6px;
|
|
@@ -628,6 +660,7 @@ body.settings-open #min {
|
|
|
628
660
|
position: absolute;
|
|
629
661
|
left: 26px;
|
|
630
662
|
bottom: 3px;
|
|
663
|
+
transition: left 0.35s ease;
|
|
631
664
|
width: 86px;
|
|
632
665
|
height: 77px;
|
|
633
666
|
zoom: 0.8; /* a bit smaller in the scene */
|
|
@@ -657,6 +690,16 @@ body.collapsed #stage {
|
|
|
657
690
|
body.collapsed #scene {
|
|
658
691
|
display: none;
|
|
659
692
|
}
|
|
693
|
+
/* minimized = just the pet's face: hide all activity scenery + props */
|
|
694
|
+
body.collapsed #desk,
|
|
695
|
+
body.collapsed #plant,
|
|
696
|
+
body.collapsed #mug,
|
|
697
|
+
body.collapsed #reading,
|
|
698
|
+
body.collapsed #terminal,
|
|
699
|
+
body.collapsed #gears,
|
|
700
|
+
body.collapsed #glasses {
|
|
701
|
+
display: none;
|
|
702
|
+
}
|
|
660
703
|
body.collapsed #pet {
|
|
661
704
|
position: static;
|
|
662
705
|
left: auto;
|
|
@@ -736,6 +779,259 @@ body.settings-open #flames,
|
|
|
736
779
|
body.settings-open #sweat {
|
|
737
780
|
display: none;
|
|
738
781
|
}
|
|
782
|
+
/* reading mode: an open book with a magnifying glass scanning the lines */
|
|
783
|
+
#reading {
|
|
784
|
+
position: absolute;
|
|
785
|
+
left: 64%;
|
|
786
|
+
bottom: 3px;
|
|
787
|
+
width: 66px;
|
|
788
|
+
height: 63px;
|
|
789
|
+
z-index: 3;
|
|
790
|
+
opacity: 0;
|
|
791
|
+
pointer-events: none;
|
|
792
|
+
transition: opacity 0.2s ease;
|
|
793
|
+
}
|
|
794
|
+
body.act-reading #reading {
|
|
795
|
+
opacity: 1;
|
|
796
|
+
}
|
|
797
|
+
/* reading glasses on the pet */
|
|
798
|
+
#glasses {
|
|
799
|
+
opacity: 0;
|
|
800
|
+
pointer-events: none;
|
|
801
|
+
transition: opacity 0.2s ease;
|
|
802
|
+
}
|
|
803
|
+
.gl-lens {
|
|
804
|
+
fill: rgba(190, 225, 255, 0.16);
|
|
805
|
+
stroke: #241c17;
|
|
806
|
+
stroke-width: 2.4;
|
|
807
|
+
}
|
|
808
|
+
.gl-bridge,
|
|
809
|
+
.gl-arm {
|
|
810
|
+
fill: none;
|
|
811
|
+
stroke: #241c17;
|
|
812
|
+
stroke-width: 2.4;
|
|
813
|
+
stroke-linecap: round;
|
|
814
|
+
}
|
|
815
|
+
body.act-reading #glasses {
|
|
816
|
+
opacity: 1;
|
|
817
|
+
}
|
|
818
|
+
.dc-card {
|
|
819
|
+
fill: #191d23; /* dark markdown viewer — easier to read on the dark scene */
|
|
820
|
+
}
|
|
821
|
+
.dc-ico {
|
|
822
|
+
fill: none;
|
|
823
|
+
stroke: #8a9098;
|
|
824
|
+
stroke-width: 1.1;
|
|
825
|
+
}
|
|
826
|
+
.dc-ico-fold {
|
|
827
|
+
fill: #8a9098;
|
|
828
|
+
}
|
|
829
|
+
.dc-title {
|
|
830
|
+
fill: #e2e5ea;
|
|
831
|
+
font-family: "SF Mono", ui-monospace, Menlo, monospace;
|
|
832
|
+
font-size: 7px;
|
|
833
|
+
font-weight: 700;
|
|
834
|
+
}
|
|
835
|
+
#dc-fname {
|
|
836
|
+
transition: opacity 0.25s ease;
|
|
837
|
+
}
|
|
838
|
+
.dc-divider {
|
|
839
|
+
stroke: rgba(255, 255, 255, 0.1);
|
|
840
|
+
stroke-width: 1;
|
|
841
|
+
}
|
|
842
|
+
.dc-h {
|
|
843
|
+
font-family: "SF Mono", ui-monospace, Menlo, monospace;
|
|
844
|
+
font-size: 6.5px;
|
|
845
|
+
font-weight: 700;
|
|
846
|
+
}
|
|
847
|
+
.dc-h1 {
|
|
848
|
+
fill: #c39cf0; /* purple */
|
|
849
|
+
}
|
|
850
|
+
.dc-g {
|
|
851
|
+
fill: #7ed07e; /* green */
|
|
852
|
+
}
|
|
853
|
+
.dc-b {
|
|
854
|
+
fill: #6fb3f2; /* blue */
|
|
855
|
+
}
|
|
856
|
+
.dc-a {
|
|
857
|
+
fill: #e0b46a; /* amber */
|
|
858
|
+
}
|
|
859
|
+
.dc-body {
|
|
860
|
+
fill: #626b76; /* muted body text */
|
|
861
|
+
}
|
|
862
|
+
/* each heading lights up in turn (a "being read" pulse) */
|
|
863
|
+
body.act-reading .dc-h {
|
|
864
|
+
animation: dc-read 4s ease-in-out infinite;
|
|
865
|
+
}
|
|
866
|
+
@keyframes dc-read {
|
|
867
|
+
0% {
|
|
868
|
+
opacity: 0.5;
|
|
869
|
+
}
|
|
870
|
+
7% {
|
|
871
|
+
opacity: 1;
|
|
872
|
+
}
|
|
873
|
+
24% {
|
|
874
|
+
opacity: 0.5;
|
|
875
|
+
}
|
|
876
|
+
100% {
|
|
877
|
+
opacity: 0.5;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
/* editing mode: a coding desk (laptop with colorful code, coffee, plant) */
|
|
881
|
+
#desk {
|
|
882
|
+
position: absolute;
|
|
883
|
+
inset: 0;
|
|
884
|
+
width: 100%;
|
|
885
|
+
height: 100%;
|
|
886
|
+
z-index: 1;
|
|
887
|
+
opacity: 0;
|
|
888
|
+
pointer-events: none;
|
|
889
|
+
transition: opacity 0.2s ease;
|
|
890
|
+
}
|
|
891
|
+
#mug {
|
|
892
|
+
position: absolute;
|
|
893
|
+
left: 36%; /* editing: on the desk, near the laptop */
|
|
894
|
+
bottom: 3px;
|
|
895
|
+
width: 44px;
|
|
896
|
+
height: 42px;
|
|
897
|
+
z-index: 3; /* the mug sits in front, on the desk */
|
|
898
|
+
opacity: 0;
|
|
899
|
+
pointer-events: none;
|
|
900
|
+
transition: opacity 0.2s ease;
|
|
901
|
+
}
|
|
902
|
+
body.act-editing #desk,
|
|
903
|
+
body.act-editing #mug,
|
|
904
|
+
body.act-reading #mug {
|
|
905
|
+
opacity: 1;
|
|
906
|
+
}
|
|
907
|
+
/* editing: raise the mug onto the desk, level with the plant */
|
|
908
|
+
body.act-editing #mug {
|
|
909
|
+
bottom: 22px;
|
|
910
|
+
}
|
|
911
|
+
/* the mug shifts right in reading (centered with the doc) */
|
|
912
|
+
body.act-reading #mug {
|
|
913
|
+
left: 38%;
|
|
914
|
+
}
|
|
915
|
+
/* slide the pet left in editing to open up the full desk */
|
|
916
|
+
body.act-editing:not(.collapsed) #pet {
|
|
917
|
+
left: 4px;
|
|
918
|
+
}
|
|
919
|
+
/* reading: pet + mug + doc, centered as a group */
|
|
920
|
+
body.act-reading:not(.collapsed) #pet {
|
|
921
|
+
left: 12px;
|
|
922
|
+
}
|
|
923
|
+
/* running: pet + terminal centered as a pair */
|
|
924
|
+
body.act-running:not(.collapsed) #pet {
|
|
925
|
+
left: 27px;
|
|
926
|
+
}
|
|
927
|
+
.dk-desk {
|
|
928
|
+
fill: #262626;
|
|
929
|
+
}
|
|
930
|
+
.dk-keys {
|
|
931
|
+
fill: #4d4d4d;
|
|
932
|
+
}
|
|
933
|
+
.mug-body {
|
|
934
|
+
fill: #d8cbaf;
|
|
935
|
+
}
|
|
936
|
+
.mug-rim {
|
|
937
|
+
fill: #c6b896;
|
|
938
|
+
}
|
|
939
|
+
.mug-handle {
|
|
940
|
+
fill: none;
|
|
941
|
+
stroke: #d8cbaf;
|
|
942
|
+
stroke-width: 2.8;
|
|
943
|
+
stroke-linecap: round;
|
|
944
|
+
}
|
|
945
|
+
.mug-code {
|
|
946
|
+
fill: none;
|
|
947
|
+
stroke: #2a211a;
|
|
948
|
+
stroke-width: 1.7;
|
|
949
|
+
stroke-linecap: square;
|
|
950
|
+
stroke-linejoin: miter;
|
|
951
|
+
}
|
|
952
|
+
.dk-wisp {
|
|
953
|
+
fill: none;
|
|
954
|
+
stroke: #c9c9c9;
|
|
955
|
+
stroke-width: 1.8;
|
|
956
|
+
stroke-linecap: round;
|
|
957
|
+
opacity: 0;
|
|
958
|
+
vector-effect: non-scaling-stroke;
|
|
959
|
+
}
|
|
960
|
+
.dk-deck2 {
|
|
961
|
+
fill: #3a3a3a;
|
|
962
|
+
}
|
|
963
|
+
.dk-screen {
|
|
964
|
+
fill: #1c1c1c;
|
|
965
|
+
}
|
|
966
|
+
.dk-glass {
|
|
967
|
+
fill: #0b0b0b;
|
|
968
|
+
}
|
|
969
|
+
.c-grn {
|
|
970
|
+
fill: #6fcf6f;
|
|
971
|
+
}
|
|
972
|
+
.c-yel {
|
|
973
|
+
fill: #e2c46a;
|
|
974
|
+
}
|
|
975
|
+
.c-blu {
|
|
976
|
+
fill: #6fb3f2;
|
|
977
|
+
}
|
|
978
|
+
.c-cur {
|
|
979
|
+
fill: #cdeccf;
|
|
980
|
+
}
|
|
981
|
+
.dk-pot {
|
|
982
|
+
fill: #b98a5e;
|
|
983
|
+
}
|
|
984
|
+
.dk-leaf {
|
|
985
|
+
fill: #3f9150;
|
|
986
|
+
}
|
|
987
|
+
.dk-leaf-c {
|
|
988
|
+
fill: #57a95f;
|
|
989
|
+
}
|
|
990
|
+
/* shared potted plant (editing + reading desks) */
|
|
991
|
+
#plant {
|
|
992
|
+
position: absolute;
|
|
993
|
+
inset: 0;
|
|
994
|
+
width: 100%;
|
|
995
|
+
height: 100%;
|
|
996
|
+
z-index: 1;
|
|
997
|
+
opacity: 0;
|
|
998
|
+
pointer-events: none;
|
|
999
|
+
transition: opacity 0.2s ease;
|
|
1000
|
+
}
|
|
1001
|
+
body.act-editing #plant {
|
|
1002
|
+
opacity: 1;
|
|
1003
|
+
}
|
|
1004
|
+
body.act-editing .c-cur {
|
|
1005
|
+
animation: cursor-blink 1s steps(1, end) infinite;
|
|
1006
|
+
}
|
|
1007
|
+
body.act-editing .dk-wisp,
|
|
1008
|
+
body.act-reading .dk-wisp {
|
|
1009
|
+
transform-box: view-box;
|
|
1010
|
+
animation: steam 2.6s ease-in-out infinite;
|
|
1011
|
+
}
|
|
1012
|
+
@keyframes cursor-blink {
|
|
1013
|
+
0%,
|
|
1014
|
+
50% {
|
|
1015
|
+
opacity: 1;
|
|
1016
|
+
}
|
|
1017
|
+
51%,
|
|
1018
|
+
100% {
|
|
1019
|
+
opacity: 0;
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
@keyframes steam {
|
|
1023
|
+
0% {
|
|
1024
|
+
opacity: 0;
|
|
1025
|
+
transform: translateY(4px);
|
|
1026
|
+
}
|
|
1027
|
+
35% {
|
|
1028
|
+
opacity: 0.7;
|
|
1029
|
+
}
|
|
1030
|
+
100% {
|
|
1031
|
+
opacity: 0;
|
|
1032
|
+
transform: translateY(-7px);
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
739
1035
|
#mouth {
|
|
740
1036
|
fill: var(--eye);
|
|
741
1037
|
transform-box: fill-box;
|
|
@@ -940,6 +1236,198 @@ body.drowse #claude {
|
|
|
940
1236
|
body.celebrate #claude {
|
|
941
1237
|
animation: celebrate 1.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
942
1238
|
}
|
|
1239
|
+
/* reading/editing/running: calm bob instead of the working hop */
|
|
1240
|
+
body.act-reading #claude,
|
|
1241
|
+
body.act-editing #claude,
|
|
1242
|
+
body.act-running #claude {
|
|
1243
|
+
animation: bob 3.6s ease-in-out infinite;
|
|
1244
|
+
}
|
|
1245
|
+
/* running mode: a mini terminal with a typing command */
|
|
1246
|
+
#terminal {
|
|
1247
|
+
position: absolute;
|
|
1248
|
+
left: 48%;
|
|
1249
|
+
bottom: 3px;
|
|
1250
|
+
width: 88px;
|
|
1251
|
+
height: 68px;
|
|
1252
|
+
z-index: 3;
|
|
1253
|
+
opacity: 0;
|
|
1254
|
+
pointer-events: none;
|
|
1255
|
+
transition: opacity 0.2s ease;
|
|
1256
|
+
}
|
|
1257
|
+
body.act-running #terminal {
|
|
1258
|
+
opacity: 1;
|
|
1259
|
+
}
|
|
1260
|
+
.tm-win {
|
|
1261
|
+
fill: #14161a;
|
|
1262
|
+
}
|
|
1263
|
+
.tm-border {
|
|
1264
|
+
fill: none;
|
|
1265
|
+
stroke: rgba(255, 255, 255, 0.14);
|
|
1266
|
+
stroke-width: 0.7;
|
|
1267
|
+
}
|
|
1268
|
+
.tm-bar {
|
|
1269
|
+
fill: #262a30;
|
|
1270
|
+
}
|
|
1271
|
+
.tm-d1 {
|
|
1272
|
+
fill: #ff5f57;
|
|
1273
|
+
}
|
|
1274
|
+
.tm-d2 {
|
|
1275
|
+
fill: #febc2e;
|
|
1276
|
+
}
|
|
1277
|
+
.tm-d3 {
|
|
1278
|
+
fill: #28c840;
|
|
1279
|
+
}
|
|
1280
|
+
.tm-prompt-c {
|
|
1281
|
+
fill: #6fcf6f;
|
|
1282
|
+
font-family: "SF Mono", ui-monospace, Menlo, monospace;
|
|
1283
|
+
font-size: 7.5px;
|
|
1284
|
+
font-weight: 700;
|
|
1285
|
+
}
|
|
1286
|
+
.tm-head {
|
|
1287
|
+
fill: #d6d9de;
|
|
1288
|
+
font-family: "SF Mono", ui-monospace, Menlo, monospace;
|
|
1289
|
+
font-size: 7px;
|
|
1290
|
+
font-weight: 600;
|
|
1291
|
+
}
|
|
1292
|
+
.tm-txt {
|
|
1293
|
+
fill: #cfd3d8;
|
|
1294
|
+
font-family: "SF Mono", ui-monospace, Menlo, monospace;
|
|
1295
|
+
font-size: 6.5px;
|
|
1296
|
+
}
|
|
1297
|
+
.tm-dots {
|
|
1298
|
+
fill: #8a9098;
|
|
1299
|
+
font-family: "SF Mono", ui-monospace, Menlo, monospace;
|
|
1300
|
+
font-size: 7px;
|
|
1301
|
+
font-weight: 700;
|
|
1302
|
+
}
|
|
1303
|
+
.tm-cursor {
|
|
1304
|
+
fill: #6fcf6f;
|
|
1305
|
+
}
|
|
1306
|
+
.tm-check {
|
|
1307
|
+
fill: none;
|
|
1308
|
+
stroke: #28c840;
|
|
1309
|
+
stroke-width: 1.5;
|
|
1310
|
+
stroke-linecap: round;
|
|
1311
|
+
stroke-linejoin: round;
|
|
1312
|
+
}
|
|
1313
|
+
.tm-spin {
|
|
1314
|
+
fill: none;
|
|
1315
|
+
stroke: #cfd3d8;
|
|
1316
|
+
stroke-width: 1.6;
|
|
1317
|
+
stroke-linecap: round;
|
|
1318
|
+
transform-box: view-box;
|
|
1319
|
+
transform-origin: 8px 54px; /* the spinner's own centre */
|
|
1320
|
+
}
|
|
1321
|
+
.tm-done-txt {
|
|
1322
|
+
fill: #6fcf6f;
|
|
1323
|
+
font-family: "SF Mono", ui-monospace, Menlo, monospace;
|
|
1324
|
+
font-size: 6.5px;
|
|
1325
|
+
font-weight: 600;
|
|
1326
|
+
}
|
|
1327
|
+
/* task log lifecycle (5s): lines stream in → Waiting spins → ✓ Done → restart */
|
|
1328
|
+
.tskA,
|
|
1329
|
+
.tskB,
|
|
1330
|
+
.tskC,
|
|
1331
|
+
.tskWait,
|
|
1332
|
+
.tskDone {
|
|
1333
|
+
opacity: 0;
|
|
1334
|
+
}
|
|
1335
|
+
body.act-running .tskA {
|
|
1336
|
+
animation: tskA 5s linear infinite;
|
|
1337
|
+
}
|
|
1338
|
+
body.act-running .tskB {
|
|
1339
|
+
animation: tskB 5s linear infinite;
|
|
1340
|
+
}
|
|
1341
|
+
body.act-running .tskC {
|
|
1342
|
+
animation: tskC 5s linear infinite;
|
|
1343
|
+
}
|
|
1344
|
+
body.act-running .tskWait {
|
|
1345
|
+
animation: tskWait 5s linear infinite;
|
|
1346
|
+
}
|
|
1347
|
+
body.act-running .tskDone {
|
|
1348
|
+
animation: tskDone 5s linear infinite;
|
|
1349
|
+
}
|
|
1350
|
+
body.act-running .tm-spin {
|
|
1351
|
+
animation: tm-spin-rot 0.8s linear infinite;
|
|
1352
|
+
}
|
|
1353
|
+
body.act-running .tm-cursor {
|
|
1354
|
+
animation: cursor-blink 1s steps(1, end) infinite;
|
|
1355
|
+
}
|
|
1356
|
+
@keyframes tm-spin-rot {
|
|
1357
|
+
to {
|
|
1358
|
+
transform: rotate(360deg);
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
@keyframes tskA {
|
|
1362
|
+
0%,
|
|
1363
|
+
4% {
|
|
1364
|
+
opacity: 0;
|
|
1365
|
+
}
|
|
1366
|
+
8%,
|
|
1367
|
+
90% {
|
|
1368
|
+
opacity: 1;
|
|
1369
|
+
}
|
|
1370
|
+
96%,
|
|
1371
|
+
100% {
|
|
1372
|
+
opacity: 0;
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
@keyframes tskB {
|
|
1376
|
+
0%,
|
|
1377
|
+
16% {
|
|
1378
|
+
opacity: 0;
|
|
1379
|
+
}
|
|
1380
|
+
20%,
|
|
1381
|
+
90% {
|
|
1382
|
+
opacity: 1;
|
|
1383
|
+
}
|
|
1384
|
+
96%,
|
|
1385
|
+
100% {
|
|
1386
|
+
opacity: 0;
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
@keyframes tskC {
|
|
1390
|
+
0%,
|
|
1391
|
+
30% {
|
|
1392
|
+
opacity: 0;
|
|
1393
|
+
}
|
|
1394
|
+
34%,
|
|
1395
|
+
90% {
|
|
1396
|
+
opacity: 1;
|
|
1397
|
+
}
|
|
1398
|
+
96%,
|
|
1399
|
+
100% {
|
|
1400
|
+
opacity: 0;
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
@keyframes tskWait {
|
|
1404
|
+
0%,
|
|
1405
|
+
6% {
|
|
1406
|
+
opacity: 0;
|
|
1407
|
+
}
|
|
1408
|
+
10%,
|
|
1409
|
+
46% {
|
|
1410
|
+
opacity: 1;
|
|
1411
|
+
}
|
|
1412
|
+
52%,
|
|
1413
|
+
100% {
|
|
1414
|
+
opacity: 0;
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
@keyframes tskDone {
|
|
1418
|
+
0%,
|
|
1419
|
+
52% {
|
|
1420
|
+
opacity: 0;
|
|
1421
|
+
}
|
|
1422
|
+
58%,
|
|
1423
|
+
90% {
|
|
1424
|
+
opacity: 1;
|
|
1425
|
+
}
|
|
1426
|
+
96%,
|
|
1427
|
+
100% {
|
|
1428
|
+
opacity: 0;
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
943
1431
|
|
|
944
1432
|
/* status */
|
|
945
1433
|
#statusline {
|
|
@@ -1188,17 +1676,6 @@ body.state-tired #status-text {
|
|
|
1188
1676
|
transform: scaleY(0.1);
|
|
1189
1677
|
}
|
|
1190
1678
|
}
|
|
1191
|
-
@keyframes shadowPulse {
|
|
1192
|
-
0%,
|
|
1193
|
-
100% {
|
|
1194
|
-
transform: translateX(-50%) scale(1);
|
|
1195
|
-
opacity: 1;
|
|
1196
|
-
}
|
|
1197
|
-
50% {
|
|
1198
|
-
transform: translateX(-50%) scale(0.84);
|
|
1199
|
-
opacity: 0.7;
|
|
1200
|
-
}
|
|
1201
|
-
}
|
|
1202
1679
|
@keyframes ping {
|
|
1203
1680
|
0% {
|
|
1204
1681
|
box-shadow: 0 0 0 0 rgba(126, 199, 125, 0.5);
|
package/usage.js
CHANGED
|
@@ -99,6 +99,69 @@ function parseFile(full, st) {
|
|
|
99
99
|
const DAYS = 30
|
|
100
100
|
const SESSION_MS = 5 * 3600 * 1000
|
|
101
101
|
|
|
102
|
+
// map a Claude Code tool name to what the pet is "doing"
|
|
103
|
+
const ACTIVITY_BY_TOOL = {
|
|
104
|
+
Edit: 'editing',
|
|
105
|
+
MultiEdit: 'editing',
|
|
106
|
+
Write: 'editing',
|
|
107
|
+
NotebookEdit: 'editing',
|
|
108
|
+
Read: 'reading',
|
|
109
|
+
Grep: 'reading',
|
|
110
|
+
Glob: 'reading',
|
|
111
|
+
LS: 'reading',
|
|
112
|
+
Bash: 'running',
|
|
113
|
+
BashOutput: 'running',
|
|
114
|
+
KillShell: 'running',
|
|
115
|
+
WebSearch: 'researching',
|
|
116
|
+
WebFetch: 'researching',
|
|
117
|
+
Task: 'delegating',
|
|
118
|
+
Agent: 'delegating',
|
|
119
|
+
TodoWrite: 'planning',
|
|
120
|
+
ExitPlanMode: 'planning',
|
|
121
|
+
AskUserQuestion: 'waiting',
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Classify what Claude Code is doing right now by reading the tail of the most
|
|
125
|
+
// recently touched session log: the latest tool_use → an activity, plan mode
|
|
126
|
+
// (permission-mode) wins over everything. Returns null when there's no signal.
|
|
127
|
+
function detectActivity(file) {
|
|
128
|
+
let raw
|
|
129
|
+
try {
|
|
130
|
+
const fd = fs.openSync(file, 'r')
|
|
131
|
+
const size = fs.fstatSync(fd).size
|
|
132
|
+
const len = Math.min(size, 65536) // last 64KB is plenty for the recent tail
|
|
133
|
+
const buf = Buffer.alloc(len)
|
|
134
|
+
fs.readSync(fd, buf, 0, len, size - len)
|
|
135
|
+
fs.closeSync(fd)
|
|
136
|
+
raw = buf.toString('utf8')
|
|
137
|
+
} catch {
|
|
138
|
+
return null
|
|
139
|
+
}
|
|
140
|
+
const lines = raw.split('\n').filter(Boolean)
|
|
141
|
+
let activity = null
|
|
142
|
+
let permMode = null
|
|
143
|
+
for (let i = lines.length - 1; i >= 0 && i >= lines.length - 80; i--) {
|
|
144
|
+
let o
|
|
145
|
+
try {
|
|
146
|
+
o = JSON.parse(lines[i])
|
|
147
|
+
} catch {
|
|
148
|
+
continue
|
|
149
|
+
}
|
|
150
|
+
if (permMode === null && (o.type === 'permission-mode' || o.type === 'mode')) {
|
|
151
|
+
const v = o.mode || o.permissionMode || o.value
|
|
152
|
+
if (typeof v === 'string') permMode = v
|
|
153
|
+
}
|
|
154
|
+
if (activity === null && o.type === 'assistant' && Array.isArray(o.message?.content)) {
|
|
155
|
+
const blocks = o.message.content
|
|
156
|
+
const tools = blocks.filter((b) => b && b.type === 'tool_use')
|
|
157
|
+
if (tools.length) activity = ACTIVITY_BY_TOOL[tools[tools.length - 1].name] || 'working'
|
|
158
|
+
}
|
|
159
|
+
if (activity !== null && permMode !== null) break
|
|
160
|
+
}
|
|
161
|
+
if (permMode === 'plan') return 'planning'
|
|
162
|
+
return activity
|
|
163
|
+
}
|
|
164
|
+
|
|
102
165
|
// Plan presets (token budgets ~= 100%). Calibrated for Max 5x from the official
|
|
103
166
|
// panel (5h ~24% at 152M tokens, weekly ~62% at 2.14B); Pro/Max20x scaled by the
|
|
104
167
|
// plan multiplier. ESTIMATES — Anthropic doesn't publish exact numbers.
|
|
@@ -134,7 +197,13 @@ function getUsage(config) {
|
|
|
134
197
|
walkJsonl(PROJECTS_DIR, files, scanCutoff)
|
|
135
198
|
|
|
136
199
|
let lastMtime = 0
|
|
137
|
-
|
|
200
|
+
let newestFile = null
|
|
201
|
+
for (const f of files) {
|
|
202
|
+
if (f.st.mtimeMs > lastMtime) {
|
|
203
|
+
lastMtime = f.st.mtimeMs
|
|
204
|
+
newestFile = f.full
|
|
205
|
+
}
|
|
206
|
+
}
|
|
138
207
|
|
|
139
208
|
const seen = new Set()
|
|
140
209
|
let todayTokens = 0
|
|
@@ -195,9 +264,12 @@ function getUsage(config) {
|
|
|
195
264
|
const weekPct = weeklyBudget ? Math.min(100, (weekTokens / weeklyBudget) * 100) : 0
|
|
196
265
|
|
|
197
266
|
const lastActivityMs = lastMtime ? now - lastMtime : Infinity
|
|
198
|
-
const active = lastActivityMs <= (config.activeThresholdMs ||
|
|
267
|
+
const active = lastActivityMs <= (config.activeThresholdMs || 20000)
|
|
199
268
|
const sleeping = lastActivityMs >= (config.sleepThresholdMs || 300000)
|
|
200
269
|
|
|
270
|
+
// what Claude is doing right now (only meaningful while active)
|
|
271
|
+
const activity = active && newestFile ? detectActivity(newestFile) : null
|
|
272
|
+
|
|
201
273
|
const byModelArr = [...byModel.entries()]
|
|
202
274
|
.map(([label, tokens]) => ({ label, tokens }))
|
|
203
275
|
.sort((a, b) => b.tokens - a.tokens)
|
|
@@ -212,6 +284,7 @@ function getUsage(config) {
|
|
|
212
284
|
tokensPerMin: Math.round(last5mTokens / 5),
|
|
213
285
|
active,
|
|
214
286
|
sleeping,
|
|
287
|
+
activity,
|
|
215
288
|
lastActivityMs,
|
|
216
289
|
ts: now,
|
|
217
290
|
}
|