clauddy 1.6.0 → 1.8.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 +25 -3
- package/bin/clauddy.js +8 -0
- package/config.json +2 -1
- package/main.js +3 -1
- package/package.json +1 -1
- package/renderer/index.html +183 -1
- package/renderer/pet.js +107 -4
- package/renderer/style.css +526 -12
- 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.
|
|
@@ -115,9 +136,10 @@ Settings saved from the UI live in `~/.claude-usage-monitor/config.json`, so you
|
|
|
115
136
|
```jsonc
|
|
116
137
|
{
|
|
117
138
|
"alerts": true, // macOS notifications on/off
|
|
118
|
-
"alertThresholds": [80, 95], // notify when session/week cross these %
|
|
139
|
+
"alertThresholds": [80, 95], // notify when session/week cross these % (two levels)
|
|
140
|
+
"fireThreshold": 90, // session % at which the pet catches fire (maxed out stays 100)
|
|
119
141
|
"pollIntervalMs": 4000, // how often local logs are re-read
|
|
120
|
-
"activeThresholdMs":
|
|
142
|
+
"activeThresholdMs": 20000, // "active" if Claude wrote to its logs within this window
|
|
121
143
|
"sleepThresholdMs": 300000, // "sleeping" after this much idle time (5 min)
|
|
122
144
|
}
|
|
123
145
|
```
|
|
@@ -144,7 +166,7 @@ live. (Installed globally? Drop the `bunx`: `clauddy poke`. Working on the repo?
|
|
|
144
166
|
## How it works
|
|
145
167
|
|
|
146
168
|
- **`main.js`** — Electron main process: frameless, transparent, always-on-top window; polls usage; fires macOS notifications; watches `config.json` and `debug.json`.
|
|
147
|
-
- **`usage.js`** — reads `~/.claude/projects/**/*.jsonl`, sums tokens per model/day, detects the rolling 5-hour session window,
|
|
169
|
+
- **`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.
|
|
148
170
|
- **`auth.js`** — OAuth login (PKCE, same public client as Claude Code) that fetches the authoritative usage %. Token stored locally, never committed.
|
|
149
171
|
- **`renderer/`** — the pet itself: an SVG pixel sprite, CSS animations, and the Web Animations API for particles.
|
|
150
172
|
- **`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
|
@@ -2,7 +2,8 @@
|
|
|
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
3
|
"alerts": true,
|
|
4
4
|
"alertThresholds": [80, 95],
|
|
5
|
+
"fireThreshold": 90,
|
|
5
6
|
"pollIntervalMs": 4000,
|
|
6
|
-
"activeThresholdMs":
|
|
7
|
+
"activeThresholdMs": 20000,
|
|
7
8
|
"sleepThresholdMs": 300000
|
|
8
9
|
}
|
package/main.js
CHANGED
|
@@ -38,6 +38,7 @@ function publicConfig(c) {
|
|
|
38
38
|
weeklyAnchorIso: c.weeklyAnchorIso,
|
|
39
39
|
alerts: c.alerts,
|
|
40
40
|
alertThresholds: c.alertThresholds,
|
|
41
|
+
fireThreshold: c.fireThreshold,
|
|
41
42
|
}
|
|
42
43
|
}
|
|
43
44
|
|
|
@@ -49,8 +50,9 @@ function loadConfig() {
|
|
|
49
50
|
weeklyAnchorIso: null,
|
|
50
51
|
alerts: true,
|
|
51
52
|
alertThresholds: [80, 95],
|
|
53
|
+
fireThreshold: 90, // session % at which the pet catches fire (tired still fixed at 100)
|
|
52
54
|
pollIntervalMs: 4000,
|
|
53
|
-
activeThresholdMs:
|
|
55
|
+
activeThresholdMs: 20000,
|
|
54
56
|
sleepThresholdMs: 300000,
|
|
55
57
|
}
|
|
56
58
|
for (const p of [EXTERNAL_CONFIG, path.join(__dirname, 'config.json')]) {
|
package/package.json
CHANGED
package/renderer/index.html
CHANGED
|
@@ -31,13 +31,111 @@
|
|
|
31
31
|
|
|
32
32
|
<!-- Claude pixel pet -->
|
|
33
33
|
<div id="stage">
|
|
34
|
+
<!-- reusable gear shape -->
|
|
35
|
+
<svg width="0" height="0" aria-hidden="true" style="position: absolute">
|
|
36
|
+
<defs>
|
|
37
|
+
<g id="gear-unit">
|
|
38
|
+
<g id="gear-teeth">
|
|
39
|
+
<rect x="10.5" y="0.5" width="3" height="5" rx="0.5" />
|
|
40
|
+
<rect x="10.5" y="18.5" width="3" height="5" rx="0.5" />
|
|
41
|
+
<rect x="0.5" y="10.5" width="5" height="3" rx="0.5" />
|
|
42
|
+
<rect x="18.5" y="10.5" width="5" height="3" rx="0.5" />
|
|
43
|
+
</g>
|
|
44
|
+
<use href="#gear-teeth" transform="rotate(45 12 12)" />
|
|
45
|
+
<circle cx="12" cy="12" r="7.5" />
|
|
46
|
+
<circle cx="12" cy="12" r="2.6" fill="#0a0706" />
|
|
47
|
+
</g>
|
|
48
|
+
</defs>
|
|
49
|
+
</svg>
|
|
50
|
+
<!-- loose gears turning on the ground behind the pet (settings only) -->
|
|
51
|
+
<svg id="gears" viewBox="0 0 216 135" preserveAspectRatio="none">
|
|
52
|
+
<g transform="translate(150 118) scale(1.35)">
|
|
53
|
+
<g>
|
|
54
|
+
<use href="#gear-unit" x="-12" y="-12" />
|
|
55
|
+
<animateTransform attributeName="transform" type="rotate" from="0" to="360" dur="3s" repeatCount="indefinite" />
|
|
56
|
+
</g>
|
|
57
|
+
</g>
|
|
58
|
+
<g transform="translate(188 124) scale(0.78)">
|
|
59
|
+
<g>
|
|
60
|
+
<use href="#gear-unit" x="-12" y="-12" />
|
|
61
|
+
<animateTransform attributeName="transform" type="rotate" from="360" to="0" dur="2.3s" repeatCount="indefinite" />
|
|
62
|
+
</g>
|
|
63
|
+
</g>
|
|
64
|
+
<g transform="translate(118 125) scale(0.6)">
|
|
65
|
+
<g>
|
|
66
|
+
<use href="#gear-unit" x="-12" y="-12" />
|
|
67
|
+
<animateTransform attributeName="transform" type="rotate" from="0" to="360" dur="1.8s" repeatCount="indefinite" />
|
|
68
|
+
</g>
|
|
69
|
+
</g>
|
|
70
|
+
</svg>
|
|
34
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>
|
|
35
110
|
<div id="shadow"></div>
|
|
36
111
|
<div id="pet">
|
|
37
112
|
<svg id="claude" viewBox="0 0 100 90" width="86" height="77">
|
|
38
113
|
<g id="body"></g>
|
|
39
114
|
<g id="eyes"></g>
|
|
40
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>
|
|
124
|
+
<!-- mechanic mode (settings): hard hat + screwdriver -->
|
|
125
|
+
<g id="robot">
|
|
126
|
+
<!-- yellow hard hat: pixel-art bitmap, built in pet.js (buildHat) -->
|
|
127
|
+
<g id="hardhat"></g>
|
|
128
|
+
<!-- screwdriver held at an angle to the side, tapping away -->
|
|
129
|
+
<g id="driver">
|
|
130
|
+
<rect class="dr-tip" x="90" y="39" width="6" height="4" />
|
|
131
|
+
<rect class="dr-shaft" x="91" y="43" width="4" height="19" />
|
|
132
|
+
<rect class="dr-hi" x="91" y="43" width="1.6" height="19" />
|
|
133
|
+
<rect class="dr-collar" x="87" y="61" width="12" height="5" rx="1" />
|
|
134
|
+
<rect class="dr-handle" x="84" y="66" width="18" height="24" rx="8" />
|
|
135
|
+
<rect class="dr-handle-hi" x="86.5" y="69" width="2.6" height="17" rx="1.3" />
|
|
136
|
+
<animateTransform attributeName="transform" type="rotate" values="-32 93 70; -26 93 70; -32 93 70" dur="1s" repeatCount="indefinite" />
|
|
137
|
+
</g>
|
|
138
|
+
</g>
|
|
41
139
|
</svg>
|
|
42
140
|
<svg id="zzz" viewBox="0 0 44 44" width="44" height="44">
|
|
43
141
|
<text x="2" y="34" class="z1">Z</text>
|
|
@@ -48,6 +146,73 @@
|
|
|
48
146
|
<div id="sweat"><i></i><i></i></div>
|
|
49
147
|
<div id="dropzone"></div>
|
|
50
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>
|
|
51
216
|
</div>
|
|
52
217
|
|
|
53
218
|
<!-- shown only when collapsed -->
|
|
@@ -141,7 +306,7 @@
|
|
|
141
306
|
</div>
|
|
142
307
|
|
|
143
308
|
<label class="set-check"><input id="set-alerts" type="checkbox" /> Alerts</label>
|
|
144
|
-
<div class="set-hint set-hint-left">macOS
|
|
309
|
+
<div class="set-hint set-hint-left">A macOS notification when your session or weekly usage crosses each level.</div>
|
|
145
310
|
<label class="set-field">Alert thresholds (%)
|
|
146
311
|
<span class="set-two">
|
|
147
312
|
<span class="num">
|
|
@@ -159,6 +324,23 @@
|
|
|
159
324
|
</span>
|
|
160
325
|
</span>
|
|
161
326
|
</span>
|
|
327
|
+
<span class="set-two set-caps">
|
|
328
|
+
<span class="num-cap">heads-up</span>
|
|
329
|
+
<span class="num-cap">near limit</span>
|
|
330
|
+
</span>
|
|
331
|
+
</label>
|
|
332
|
+
<div class="set-hint set-hint-left">The pet catches fire past this session % (it's maxed out at 100%).</div>
|
|
333
|
+
<label class="set-field">Catch fire at (%)
|
|
334
|
+
<span class="set-two">
|
|
335
|
+
<span class="num">
|
|
336
|
+
<input id="set-fire" type="number" min="1" max="99" />
|
|
337
|
+
<span class="num-spin">
|
|
338
|
+
<button type="button" class="num-btn" data-for="set-fire" data-step="1">▲</button>
|
|
339
|
+
<button type="button" class="num-btn" data-for="set-fire" data-step="-1">▼</button>
|
|
340
|
+
</span>
|
|
341
|
+
</span>
|
|
342
|
+
<span class="num" aria-hidden="true"></span>
|
|
343
|
+
</span>
|
|
162
344
|
</label>
|
|
163
345
|
<div class="set-actions">
|
|
164
346
|
<button id="set-cancel">Cancel</button>
|
package/renderer/pet.js
CHANGED
|
@@ -40,6 +40,39 @@ const SPRITE = [
|
|
|
40
40
|
})
|
|
41
41
|
})()
|
|
42
42
|
|
|
43
|
+
// hard hat (mechanic mode): pixel-art bitmap so it matches the pet's blocky look
|
|
44
|
+
;(function buildHat() {
|
|
45
|
+
const g = el('hardhat')
|
|
46
|
+
if (!g) return
|
|
47
|
+
// L = highlight, Y = main, D = shadow rim, B = brim
|
|
48
|
+
const HAT = [
|
|
49
|
+
'.....YLLY.....',
|
|
50
|
+
'...YYYLLYYY...',
|
|
51
|
+
'..YYYYLLYYYY..',
|
|
52
|
+
'.YYYYYLLYYYYY.',
|
|
53
|
+
'.DDDDDDDDDDDD.',
|
|
54
|
+
'BBBBBBBBBBBBBB',
|
|
55
|
+
]
|
|
56
|
+
const COL = { L: '#ffe27a', Y: '#f5c518', D: '#d99a00', B: '#c98f14' }
|
|
57
|
+
const C = 8 // cell size in svg units
|
|
58
|
+
const cols = HAT[0].length
|
|
59
|
+
const ox = 50 - (cols * C) / 2 // centered on the head (center x = 50)
|
|
60
|
+
const oy = -24 // rises above the head, brim ends just over the eyes
|
|
61
|
+
HAT.forEach((row, r) => {
|
|
62
|
+
for (let c = 0; c < row.length; c++) {
|
|
63
|
+
const ch = row[c]
|
|
64
|
+
if (ch === '.') continue
|
|
65
|
+
const rect = document.createElementNS(SVGNS, 'rect')
|
|
66
|
+
rect.setAttribute('x', ox + c * C)
|
|
67
|
+
rect.setAttribute('y', oy + r * C)
|
|
68
|
+
rect.setAttribute('width', C)
|
|
69
|
+
rect.setAttribute('height', C)
|
|
70
|
+
rect.setAttribute('fill', COL[ch])
|
|
71
|
+
g.appendChild(rect)
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
})()
|
|
75
|
+
|
|
43
76
|
// night scene backdrop (clouds, crescent moon, stars, dotted ground/sky)
|
|
44
77
|
;(function buildScene() {
|
|
45
78
|
const s = el('scene')
|
|
@@ -81,6 +114,32 @@ const SPRITE = [
|
|
|
81
114
|
})
|
|
82
115
|
})()
|
|
83
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
|
+
|
|
84
143
|
// helpers
|
|
85
144
|
function fmtTokens(t) {
|
|
86
145
|
t = t || 0
|
|
@@ -369,17 +428,20 @@ function render(d) {
|
|
|
369
428
|
const wkPct = liveOn ? realUsage.week.pct : 0
|
|
370
429
|
const wkReset = liveOn ? realUsage.week.resetMs : null
|
|
371
430
|
|
|
431
|
+
const fireAt = currentConfig.fireThreshold ?? 90
|
|
372
432
|
let st =
|
|
373
433
|
liveOn && sessPct >= 100
|
|
374
434
|
? 'tired'
|
|
375
435
|
: d.active
|
|
376
436
|
? 'working'
|
|
377
|
-
: liveOn && sessPct >=
|
|
437
|
+
: liveOn && sessPct >= fireAt
|
|
378
438
|
? 'stressed'
|
|
379
439
|
: d.sleeping
|
|
380
440
|
? 'sleeping'
|
|
381
441
|
: 'idle'
|
|
382
|
-
|
|
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)
|
|
383
445
|
if (debugState) {
|
|
384
446
|
const map = {
|
|
385
447
|
working: 'working',
|
|
@@ -388,9 +450,20 @@ function render(d) {
|
|
|
388
450
|
tired: 'tired',
|
|
389
451
|
idle: 'idle',
|
|
390
452
|
}
|
|
391
|
-
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
|
+
}
|
|
392
460
|
}
|
|
393
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
|
+
}
|
|
394
467
|
if (prevState && prevState !== st) {
|
|
395
468
|
if (st === 'sleeping') oneShot('drowse', 700)
|
|
396
469
|
else if (prevState === 'sleeping') oneShot('wake', 700)
|
|
@@ -399,7 +472,7 @@ function render(d) {
|
|
|
399
472
|
|
|
400
473
|
const word =
|
|
401
474
|
st === 'working'
|
|
402
|
-
? 'working'
|
|
475
|
+
? curActivity || 'working' // show what Claude's doing (editing/reading/…)
|
|
403
476
|
: st === 'sleeping'
|
|
404
477
|
? 'sleeping'
|
|
405
478
|
: st === 'stressed'
|
|
@@ -567,12 +640,32 @@ el('acc-confirm').addEventListener('click', () => {
|
|
|
567
640
|
el('acc-logout').addEventListener('click', () => window.api.authLogout())
|
|
568
641
|
|
|
569
642
|
// settings panel
|
|
643
|
+
// snapshot of the editable fields, to tell whether there are unsaved changes
|
|
644
|
+
let settingsBaseline = null
|
|
645
|
+
function snapshotSettings() {
|
|
646
|
+
return JSON.stringify({
|
|
647
|
+
alerts: el('set-alerts').checked,
|
|
648
|
+
t1: el('set-t1').value,
|
|
649
|
+
t2: el('set-t2').value,
|
|
650
|
+
fire: el('set-fire').value,
|
|
651
|
+
})
|
|
652
|
+
}
|
|
653
|
+
function refreshSaveDirty() {
|
|
654
|
+
if (settingsBaseline == null) return
|
|
655
|
+
el('set-save').classList.toggle('dirty', snapshotSettings() !== settingsBaseline)
|
|
656
|
+
}
|
|
657
|
+
function clearSaveDirty() {
|
|
658
|
+
settingsBaseline = snapshotSettings()
|
|
659
|
+
el('set-save').classList.remove('dirty')
|
|
660
|
+
}
|
|
570
661
|
function populateSettings() {
|
|
571
662
|
const c = currentConfig || {}
|
|
572
663
|
el('set-alerts').checked = c.alerts !== false
|
|
573
664
|
const th = c.alertThresholds || [80, 95]
|
|
574
665
|
el('set-t1').value = th[0] != null ? th[0] : 80
|
|
575
666
|
el('set-t2').value = th[1] != null ? th[1] : 95
|
|
667
|
+
el('set-fire').value = c.fireThreshold != null ? c.fireThreshold : 90
|
|
668
|
+
clearSaveDirty() // fields now match the saved config
|
|
576
669
|
}
|
|
577
670
|
function openSettings() {
|
|
578
671
|
document.body.classList.remove('collapsed')
|
|
@@ -591,20 +684,30 @@ for (const b of document.querySelectorAll('.num-btn')) {
|
|
|
591
684
|
const max = Number(input.max) || 100
|
|
592
685
|
const next = (Number.parseInt(input.value, 10) || 0) + Number(b.dataset.step)
|
|
593
686
|
input.value = Math.min(max, Math.max(min, next))
|
|
687
|
+
refreshSaveDirty() // steppers change the value without an 'input' event
|
|
594
688
|
})
|
|
595
689
|
}
|
|
690
|
+
// light up Save whenever an editable field changes
|
|
691
|
+
for (const id of ['set-alerts', 'set-t1', 'set-t2', 'set-fire']) {
|
|
692
|
+
el(id).addEventListener('input', refreshSaveDirty)
|
|
693
|
+
el(id).addEventListener('change', refreshSaveDirty)
|
|
694
|
+
}
|
|
596
695
|
el('set-cancel').addEventListener('click', () => {
|
|
597
696
|
document.body.classList.remove('settings-open')
|
|
697
|
+
clearSaveDirty()
|
|
598
698
|
fitSize()
|
|
599
699
|
})
|
|
600
700
|
el('set-save').addEventListener('click', () => {
|
|
601
701
|
const num = (id) => parseFloat(el(id).value)
|
|
702
|
+
const fire = num('set-fire')
|
|
602
703
|
window.api.saveConfig({
|
|
603
704
|
alerts: el('set-alerts').checked,
|
|
604
705
|
alertThresholds: [num('set-t1'), num('set-t2')]
|
|
605
706
|
.filter((n) => n >= 1 && n <= 100)
|
|
606
707
|
.sort((a, b) => a - b),
|
|
708
|
+
fireThreshold: fire >= 1 && fire <= 99 ? fire : 90,
|
|
607
709
|
})
|
|
710
|
+
clearSaveDirty()
|
|
608
711
|
document.body.classList.remove('settings-open')
|
|
609
712
|
fitSize()
|
|
610
713
|
})
|
package/renderer/style.css
CHANGED
|
@@ -242,6 +242,16 @@ body.settings-open #settings {
|
|
|
242
242
|
display: flex;
|
|
243
243
|
gap: 6px;
|
|
244
244
|
}
|
|
245
|
+
/* small captions under each threshold box */
|
|
246
|
+
.set-caps {
|
|
247
|
+
margin-top: 3px;
|
|
248
|
+
}
|
|
249
|
+
.num-cap {
|
|
250
|
+
flex: 1;
|
|
251
|
+
font-size: 8.5px;
|
|
252
|
+
color: var(--muted);
|
|
253
|
+
padding-left: 2px;
|
|
254
|
+
}
|
|
245
255
|
/* custom number stepper */
|
|
246
256
|
.num {
|
|
247
257
|
position: relative;
|
|
@@ -324,12 +334,26 @@ body.settings-open #settings {
|
|
|
324
334
|
background: rgba(255, 255, 255, 0.2);
|
|
325
335
|
}
|
|
326
336
|
#set-save {
|
|
327
|
-
background:
|
|
337
|
+
background: rgba(217, 119, 87, 0.4); /* calm until there are unsaved changes */
|
|
328
338
|
color: #fff;
|
|
329
339
|
}
|
|
330
340
|
#set-save:hover {
|
|
331
341
|
background: #e2876a;
|
|
332
342
|
}
|
|
343
|
+
/* unsaved changes: light up and pulse so you don't forget to save */
|
|
344
|
+
#set-save.dirty {
|
|
345
|
+
background: var(--coral);
|
|
346
|
+
animation: save-pulse 1.6s ease-in-out infinite;
|
|
347
|
+
}
|
|
348
|
+
@keyframes save-pulse {
|
|
349
|
+
0%,
|
|
350
|
+
100% {
|
|
351
|
+
box-shadow: 0 0 0 0 rgba(217, 119, 87, 0);
|
|
352
|
+
}
|
|
353
|
+
50% {
|
|
354
|
+
box-shadow: 0 0 9px 2px rgba(217, 119, 87, 0.6);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
333
357
|
|
|
334
358
|
/* live tag */
|
|
335
359
|
.live-only {
|
|
@@ -604,6 +628,7 @@ body.settings-open #min {
|
|
|
604
628
|
position: absolute;
|
|
605
629
|
left: 26px;
|
|
606
630
|
bottom: 3px;
|
|
631
|
+
transition: left 0.35s ease;
|
|
607
632
|
width: 86px;
|
|
608
633
|
height: 77px;
|
|
609
634
|
zoom: 0.8; /* a bit smaller in the scene */
|
|
@@ -633,6 +658,16 @@ body.collapsed #stage {
|
|
|
633
658
|
body.collapsed #scene {
|
|
634
659
|
display: none;
|
|
635
660
|
}
|
|
661
|
+
/* minimized = just the pet's face: hide all activity scenery + props */
|
|
662
|
+
body.collapsed #desk,
|
|
663
|
+
body.collapsed #plant,
|
|
664
|
+
body.collapsed #mug,
|
|
665
|
+
body.collapsed #reading,
|
|
666
|
+
body.collapsed #terminal,
|
|
667
|
+
body.collapsed #gears,
|
|
668
|
+
body.collapsed #glasses {
|
|
669
|
+
display: none;
|
|
670
|
+
}
|
|
636
671
|
body.collapsed #pet {
|
|
637
672
|
position: static;
|
|
638
673
|
left: auto;
|
|
@@ -667,6 +702,304 @@ body.collapsed #shadow {
|
|
|
667
702
|
transform-origin: center;
|
|
668
703
|
animation: blink 5.5s infinite;
|
|
669
704
|
}
|
|
705
|
+
/* mechanic mode (settings): hard hat + screwdriver on the pet, loose gears
|
|
706
|
+
turning behind it. Hidden otherwise. */
|
|
707
|
+
#robot {
|
|
708
|
+
opacity: 0;
|
|
709
|
+
pointer-events: none;
|
|
710
|
+
transition: opacity 0.2s ease;
|
|
711
|
+
}
|
|
712
|
+
/* hard hat colors live inline (built pixel-by-pixel in pet.js buildHat) */
|
|
713
|
+
.dr-handle {
|
|
714
|
+
fill: #c0392b;
|
|
715
|
+
}
|
|
716
|
+
.dr-handle-hi {
|
|
717
|
+
fill: #e0685a;
|
|
718
|
+
}
|
|
719
|
+
.dr-collar {
|
|
720
|
+
fill: #9a9a9a;
|
|
721
|
+
}
|
|
722
|
+
.dr-tip,
|
|
723
|
+
.dr-shaft {
|
|
724
|
+
fill: #d0d0d0;
|
|
725
|
+
}
|
|
726
|
+
.dr-hi {
|
|
727
|
+
fill: #f2f2f2;
|
|
728
|
+
}
|
|
729
|
+
#gears {
|
|
730
|
+
position: absolute;
|
|
731
|
+
inset: 0;
|
|
732
|
+
width: 100%;
|
|
733
|
+
height: 100%;
|
|
734
|
+
z-index: 1;
|
|
735
|
+
fill: #7a6a5c;
|
|
736
|
+
opacity: 0;
|
|
737
|
+
pointer-events: none;
|
|
738
|
+
transition: opacity 0.2s ease;
|
|
739
|
+
}
|
|
740
|
+
body.settings-open #robot,
|
|
741
|
+
body.settings-open #gears {
|
|
742
|
+
opacity: 1;
|
|
743
|
+
}
|
|
744
|
+
/* declutter the scene while configuring */
|
|
745
|
+
body.settings-open #zzz,
|
|
746
|
+
body.settings-open #flames,
|
|
747
|
+
body.settings-open #sweat {
|
|
748
|
+
display: none;
|
|
749
|
+
}
|
|
750
|
+
/* reading mode: an open book with a magnifying glass scanning the lines */
|
|
751
|
+
#reading {
|
|
752
|
+
position: absolute;
|
|
753
|
+
left: 64%;
|
|
754
|
+
bottom: 3px;
|
|
755
|
+
width: 66px;
|
|
756
|
+
height: 63px;
|
|
757
|
+
z-index: 3;
|
|
758
|
+
opacity: 0;
|
|
759
|
+
pointer-events: none;
|
|
760
|
+
transition: opacity 0.2s ease;
|
|
761
|
+
}
|
|
762
|
+
body.act-reading #reading {
|
|
763
|
+
opacity: 1;
|
|
764
|
+
}
|
|
765
|
+
/* reading glasses on the pet */
|
|
766
|
+
#glasses {
|
|
767
|
+
opacity: 0;
|
|
768
|
+
pointer-events: none;
|
|
769
|
+
transition: opacity 0.2s ease;
|
|
770
|
+
}
|
|
771
|
+
.gl-lens {
|
|
772
|
+
fill: rgba(190, 225, 255, 0.16);
|
|
773
|
+
stroke: #241c17;
|
|
774
|
+
stroke-width: 2.4;
|
|
775
|
+
}
|
|
776
|
+
.gl-bridge,
|
|
777
|
+
.gl-arm {
|
|
778
|
+
fill: none;
|
|
779
|
+
stroke: #241c17;
|
|
780
|
+
stroke-width: 2.4;
|
|
781
|
+
stroke-linecap: round;
|
|
782
|
+
}
|
|
783
|
+
body.act-reading #glasses {
|
|
784
|
+
opacity: 1;
|
|
785
|
+
}
|
|
786
|
+
.dc-card {
|
|
787
|
+
fill: #191d23; /* dark markdown viewer — easier to read on the dark scene */
|
|
788
|
+
}
|
|
789
|
+
.dc-ico {
|
|
790
|
+
fill: none;
|
|
791
|
+
stroke: #8a9098;
|
|
792
|
+
stroke-width: 1.1;
|
|
793
|
+
}
|
|
794
|
+
.dc-ico-fold {
|
|
795
|
+
fill: #8a9098;
|
|
796
|
+
}
|
|
797
|
+
.dc-title {
|
|
798
|
+
fill: #e2e5ea;
|
|
799
|
+
font-family: "SF Mono", ui-monospace, Menlo, monospace;
|
|
800
|
+
font-size: 7px;
|
|
801
|
+
font-weight: 700;
|
|
802
|
+
}
|
|
803
|
+
#dc-fname {
|
|
804
|
+
transition: opacity 0.25s ease;
|
|
805
|
+
}
|
|
806
|
+
.dc-divider {
|
|
807
|
+
stroke: rgba(255, 255, 255, 0.1);
|
|
808
|
+
stroke-width: 1;
|
|
809
|
+
}
|
|
810
|
+
.dc-h {
|
|
811
|
+
font-family: "SF Mono", ui-monospace, Menlo, monospace;
|
|
812
|
+
font-size: 6.5px;
|
|
813
|
+
font-weight: 700;
|
|
814
|
+
}
|
|
815
|
+
.dc-h1 {
|
|
816
|
+
fill: #c39cf0; /* purple */
|
|
817
|
+
}
|
|
818
|
+
.dc-g {
|
|
819
|
+
fill: #7ed07e; /* green */
|
|
820
|
+
}
|
|
821
|
+
.dc-b {
|
|
822
|
+
fill: #6fb3f2; /* blue */
|
|
823
|
+
}
|
|
824
|
+
.dc-a {
|
|
825
|
+
fill: #e0b46a; /* amber */
|
|
826
|
+
}
|
|
827
|
+
.dc-body {
|
|
828
|
+
fill: #626b76; /* muted body text */
|
|
829
|
+
}
|
|
830
|
+
/* each heading lights up in turn (a "being read" pulse) */
|
|
831
|
+
body.act-reading .dc-h {
|
|
832
|
+
animation: dc-read 4s ease-in-out infinite;
|
|
833
|
+
}
|
|
834
|
+
@keyframes dc-read {
|
|
835
|
+
0% {
|
|
836
|
+
opacity: 0.5;
|
|
837
|
+
}
|
|
838
|
+
7% {
|
|
839
|
+
opacity: 1;
|
|
840
|
+
}
|
|
841
|
+
24% {
|
|
842
|
+
opacity: 0.5;
|
|
843
|
+
}
|
|
844
|
+
100% {
|
|
845
|
+
opacity: 0.5;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
/* editing mode: a coding desk (laptop with colorful code, coffee, plant) */
|
|
849
|
+
#desk {
|
|
850
|
+
position: absolute;
|
|
851
|
+
inset: 0;
|
|
852
|
+
width: 100%;
|
|
853
|
+
height: 100%;
|
|
854
|
+
z-index: 1;
|
|
855
|
+
opacity: 0;
|
|
856
|
+
pointer-events: none;
|
|
857
|
+
transition: opacity 0.2s ease;
|
|
858
|
+
}
|
|
859
|
+
#mug {
|
|
860
|
+
position: absolute;
|
|
861
|
+
left: 36%; /* editing: on the desk, near the laptop */
|
|
862
|
+
bottom: 3px;
|
|
863
|
+
width: 44px;
|
|
864
|
+
height: 42px;
|
|
865
|
+
z-index: 3; /* the mug sits in front, on the desk */
|
|
866
|
+
opacity: 0;
|
|
867
|
+
pointer-events: none;
|
|
868
|
+
transition: opacity 0.2s ease;
|
|
869
|
+
}
|
|
870
|
+
body.act-editing #desk,
|
|
871
|
+
body.act-editing #mug,
|
|
872
|
+
body.act-reading #mug {
|
|
873
|
+
opacity: 1;
|
|
874
|
+
}
|
|
875
|
+
/* editing: raise the mug onto the desk, level with the plant */
|
|
876
|
+
body.act-editing #mug {
|
|
877
|
+
bottom: 22px;
|
|
878
|
+
}
|
|
879
|
+
/* the mug shifts right in reading (centered with the doc) */
|
|
880
|
+
body.act-reading #mug {
|
|
881
|
+
left: 38%;
|
|
882
|
+
}
|
|
883
|
+
/* slide the pet left in editing to open up the full desk */
|
|
884
|
+
body.act-editing:not(.collapsed) #pet {
|
|
885
|
+
left: 4px;
|
|
886
|
+
}
|
|
887
|
+
/* reading: pet + mug + doc, centered as a group */
|
|
888
|
+
body.act-reading:not(.collapsed) #pet {
|
|
889
|
+
left: 12px;
|
|
890
|
+
}
|
|
891
|
+
/* running: pet + terminal centered as a pair */
|
|
892
|
+
body.act-running:not(.collapsed) #pet {
|
|
893
|
+
left: 27px;
|
|
894
|
+
}
|
|
895
|
+
.dk-desk {
|
|
896
|
+
fill: #262626;
|
|
897
|
+
}
|
|
898
|
+
.dk-keys {
|
|
899
|
+
fill: #4d4d4d;
|
|
900
|
+
}
|
|
901
|
+
.mug-body {
|
|
902
|
+
fill: #d8cbaf;
|
|
903
|
+
}
|
|
904
|
+
.mug-rim {
|
|
905
|
+
fill: #c6b896;
|
|
906
|
+
}
|
|
907
|
+
.mug-handle {
|
|
908
|
+
fill: none;
|
|
909
|
+
stroke: #d8cbaf;
|
|
910
|
+
stroke-width: 2.8;
|
|
911
|
+
stroke-linecap: round;
|
|
912
|
+
}
|
|
913
|
+
.mug-code {
|
|
914
|
+
fill: none;
|
|
915
|
+
stroke: #2a211a;
|
|
916
|
+
stroke-width: 1.7;
|
|
917
|
+
stroke-linecap: square;
|
|
918
|
+
stroke-linejoin: miter;
|
|
919
|
+
}
|
|
920
|
+
.dk-wisp {
|
|
921
|
+
fill: none;
|
|
922
|
+
stroke: #c9c9c9;
|
|
923
|
+
stroke-width: 1.8;
|
|
924
|
+
stroke-linecap: round;
|
|
925
|
+
opacity: 0;
|
|
926
|
+
vector-effect: non-scaling-stroke;
|
|
927
|
+
}
|
|
928
|
+
.dk-deck2 {
|
|
929
|
+
fill: #3a3a3a;
|
|
930
|
+
}
|
|
931
|
+
.dk-screen {
|
|
932
|
+
fill: #1c1c1c;
|
|
933
|
+
}
|
|
934
|
+
.dk-glass {
|
|
935
|
+
fill: #0b0b0b;
|
|
936
|
+
}
|
|
937
|
+
.c-grn {
|
|
938
|
+
fill: #6fcf6f;
|
|
939
|
+
}
|
|
940
|
+
.c-yel {
|
|
941
|
+
fill: #e2c46a;
|
|
942
|
+
}
|
|
943
|
+
.c-blu {
|
|
944
|
+
fill: #6fb3f2;
|
|
945
|
+
}
|
|
946
|
+
.c-cur {
|
|
947
|
+
fill: #cdeccf;
|
|
948
|
+
}
|
|
949
|
+
.dk-pot {
|
|
950
|
+
fill: #b98a5e;
|
|
951
|
+
}
|
|
952
|
+
.dk-leaf {
|
|
953
|
+
fill: #3f9150;
|
|
954
|
+
}
|
|
955
|
+
.dk-leaf-c {
|
|
956
|
+
fill: #57a95f;
|
|
957
|
+
}
|
|
958
|
+
/* shared potted plant (editing + reading desks) */
|
|
959
|
+
#plant {
|
|
960
|
+
position: absolute;
|
|
961
|
+
inset: 0;
|
|
962
|
+
width: 100%;
|
|
963
|
+
height: 100%;
|
|
964
|
+
z-index: 1;
|
|
965
|
+
opacity: 0;
|
|
966
|
+
pointer-events: none;
|
|
967
|
+
transition: opacity 0.2s ease;
|
|
968
|
+
}
|
|
969
|
+
body.act-editing #plant {
|
|
970
|
+
opacity: 1;
|
|
971
|
+
}
|
|
972
|
+
body.act-editing .c-cur {
|
|
973
|
+
animation: cursor-blink 1s steps(1, end) infinite;
|
|
974
|
+
}
|
|
975
|
+
body.act-editing .dk-wisp,
|
|
976
|
+
body.act-reading .dk-wisp {
|
|
977
|
+
transform-box: view-box;
|
|
978
|
+
animation: steam 2.6s ease-in-out infinite;
|
|
979
|
+
}
|
|
980
|
+
@keyframes cursor-blink {
|
|
981
|
+
0%,
|
|
982
|
+
50% {
|
|
983
|
+
opacity: 1;
|
|
984
|
+
}
|
|
985
|
+
51%,
|
|
986
|
+
100% {
|
|
987
|
+
opacity: 0;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
@keyframes steam {
|
|
991
|
+
0% {
|
|
992
|
+
opacity: 0;
|
|
993
|
+
transform: translateY(4px);
|
|
994
|
+
}
|
|
995
|
+
35% {
|
|
996
|
+
opacity: 0.7;
|
|
997
|
+
}
|
|
998
|
+
100% {
|
|
999
|
+
opacity: 0;
|
|
1000
|
+
transform: translateY(-7px);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
670
1003
|
#mouth {
|
|
671
1004
|
fill: var(--eye);
|
|
672
1005
|
transform-box: fill-box;
|
|
@@ -871,6 +1204,198 @@ body.drowse #claude {
|
|
|
871
1204
|
body.celebrate #claude {
|
|
872
1205
|
animation: celebrate 1.3s cubic-bezier(0.34, 1.56, 0.64, 1);
|
|
873
1206
|
}
|
|
1207
|
+
/* reading/editing/running: calm bob instead of the working hop */
|
|
1208
|
+
body.act-reading #claude,
|
|
1209
|
+
body.act-editing #claude,
|
|
1210
|
+
body.act-running #claude {
|
|
1211
|
+
animation: bob 3.6s ease-in-out infinite;
|
|
1212
|
+
}
|
|
1213
|
+
/* running mode: a mini terminal with a typing command */
|
|
1214
|
+
#terminal {
|
|
1215
|
+
position: absolute;
|
|
1216
|
+
left: 48%;
|
|
1217
|
+
bottom: 3px;
|
|
1218
|
+
width: 88px;
|
|
1219
|
+
height: 68px;
|
|
1220
|
+
z-index: 3;
|
|
1221
|
+
opacity: 0;
|
|
1222
|
+
pointer-events: none;
|
|
1223
|
+
transition: opacity 0.2s ease;
|
|
1224
|
+
}
|
|
1225
|
+
body.act-running #terminal {
|
|
1226
|
+
opacity: 1;
|
|
1227
|
+
}
|
|
1228
|
+
.tm-win {
|
|
1229
|
+
fill: #14161a;
|
|
1230
|
+
}
|
|
1231
|
+
.tm-border {
|
|
1232
|
+
fill: none;
|
|
1233
|
+
stroke: rgba(255, 255, 255, 0.14);
|
|
1234
|
+
stroke-width: 0.7;
|
|
1235
|
+
}
|
|
1236
|
+
.tm-bar {
|
|
1237
|
+
fill: #262a30;
|
|
1238
|
+
}
|
|
1239
|
+
.tm-d1 {
|
|
1240
|
+
fill: #ff5f57;
|
|
1241
|
+
}
|
|
1242
|
+
.tm-d2 {
|
|
1243
|
+
fill: #febc2e;
|
|
1244
|
+
}
|
|
1245
|
+
.tm-d3 {
|
|
1246
|
+
fill: #28c840;
|
|
1247
|
+
}
|
|
1248
|
+
.tm-prompt-c {
|
|
1249
|
+
fill: #6fcf6f;
|
|
1250
|
+
font-family: "SF Mono", ui-monospace, Menlo, monospace;
|
|
1251
|
+
font-size: 7.5px;
|
|
1252
|
+
font-weight: 700;
|
|
1253
|
+
}
|
|
1254
|
+
.tm-head {
|
|
1255
|
+
fill: #d6d9de;
|
|
1256
|
+
font-family: "SF Mono", ui-monospace, Menlo, monospace;
|
|
1257
|
+
font-size: 7px;
|
|
1258
|
+
font-weight: 600;
|
|
1259
|
+
}
|
|
1260
|
+
.tm-txt {
|
|
1261
|
+
fill: #cfd3d8;
|
|
1262
|
+
font-family: "SF Mono", ui-monospace, Menlo, monospace;
|
|
1263
|
+
font-size: 6.5px;
|
|
1264
|
+
}
|
|
1265
|
+
.tm-dots {
|
|
1266
|
+
fill: #8a9098;
|
|
1267
|
+
font-family: "SF Mono", ui-monospace, Menlo, monospace;
|
|
1268
|
+
font-size: 7px;
|
|
1269
|
+
font-weight: 700;
|
|
1270
|
+
}
|
|
1271
|
+
.tm-cursor {
|
|
1272
|
+
fill: #6fcf6f;
|
|
1273
|
+
}
|
|
1274
|
+
.tm-check {
|
|
1275
|
+
fill: none;
|
|
1276
|
+
stroke: #28c840;
|
|
1277
|
+
stroke-width: 1.5;
|
|
1278
|
+
stroke-linecap: round;
|
|
1279
|
+
stroke-linejoin: round;
|
|
1280
|
+
}
|
|
1281
|
+
.tm-spin {
|
|
1282
|
+
fill: none;
|
|
1283
|
+
stroke: #cfd3d8;
|
|
1284
|
+
stroke-width: 1.6;
|
|
1285
|
+
stroke-linecap: round;
|
|
1286
|
+
transform-box: view-box;
|
|
1287
|
+
transform-origin: 8px 54px; /* the spinner's own centre */
|
|
1288
|
+
}
|
|
1289
|
+
.tm-done-txt {
|
|
1290
|
+
fill: #6fcf6f;
|
|
1291
|
+
font-family: "SF Mono", ui-monospace, Menlo, monospace;
|
|
1292
|
+
font-size: 6.5px;
|
|
1293
|
+
font-weight: 600;
|
|
1294
|
+
}
|
|
1295
|
+
/* task log lifecycle (5s): lines stream in → Waiting spins → ✓ Done → restart */
|
|
1296
|
+
.tskA,
|
|
1297
|
+
.tskB,
|
|
1298
|
+
.tskC,
|
|
1299
|
+
.tskWait,
|
|
1300
|
+
.tskDone {
|
|
1301
|
+
opacity: 0;
|
|
1302
|
+
}
|
|
1303
|
+
body.act-running .tskA {
|
|
1304
|
+
animation: tskA 5s linear infinite;
|
|
1305
|
+
}
|
|
1306
|
+
body.act-running .tskB {
|
|
1307
|
+
animation: tskB 5s linear infinite;
|
|
1308
|
+
}
|
|
1309
|
+
body.act-running .tskC {
|
|
1310
|
+
animation: tskC 5s linear infinite;
|
|
1311
|
+
}
|
|
1312
|
+
body.act-running .tskWait {
|
|
1313
|
+
animation: tskWait 5s linear infinite;
|
|
1314
|
+
}
|
|
1315
|
+
body.act-running .tskDone {
|
|
1316
|
+
animation: tskDone 5s linear infinite;
|
|
1317
|
+
}
|
|
1318
|
+
body.act-running .tm-spin {
|
|
1319
|
+
animation: tm-spin-rot 0.8s linear infinite;
|
|
1320
|
+
}
|
|
1321
|
+
body.act-running .tm-cursor {
|
|
1322
|
+
animation: cursor-blink 1s steps(1, end) infinite;
|
|
1323
|
+
}
|
|
1324
|
+
@keyframes tm-spin-rot {
|
|
1325
|
+
to {
|
|
1326
|
+
transform: rotate(360deg);
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
@keyframes tskA {
|
|
1330
|
+
0%,
|
|
1331
|
+
4% {
|
|
1332
|
+
opacity: 0;
|
|
1333
|
+
}
|
|
1334
|
+
8%,
|
|
1335
|
+
90% {
|
|
1336
|
+
opacity: 1;
|
|
1337
|
+
}
|
|
1338
|
+
96%,
|
|
1339
|
+
100% {
|
|
1340
|
+
opacity: 0;
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
@keyframes tskB {
|
|
1344
|
+
0%,
|
|
1345
|
+
16% {
|
|
1346
|
+
opacity: 0;
|
|
1347
|
+
}
|
|
1348
|
+
20%,
|
|
1349
|
+
90% {
|
|
1350
|
+
opacity: 1;
|
|
1351
|
+
}
|
|
1352
|
+
96%,
|
|
1353
|
+
100% {
|
|
1354
|
+
opacity: 0;
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
@keyframes tskC {
|
|
1358
|
+
0%,
|
|
1359
|
+
30% {
|
|
1360
|
+
opacity: 0;
|
|
1361
|
+
}
|
|
1362
|
+
34%,
|
|
1363
|
+
90% {
|
|
1364
|
+
opacity: 1;
|
|
1365
|
+
}
|
|
1366
|
+
96%,
|
|
1367
|
+
100% {
|
|
1368
|
+
opacity: 0;
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
@keyframes tskWait {
|
|
1372
|
+
0%,
|
|
1373
|
+
6% {
|
|
1374
|
+
opacity: 0;
|
|
1375
|
+
}
|
|
1376
|
+
10%,
|
|
1377
|
+
46% {
|
|
1378
|
+
opacity: 1;
|
|
1379
|
+
}
|
|
1380
|
+
52%,
|
|
1381
|
+
100% {
|
|
1382
|
+
opacity: 0;
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
@keyframes tskDone {
|
|
1386
|
+
0%,
|
|
1387
|
+
52% {
|
|
1388
|
+
opacity: 0;
|
|
1389
|
+
}
|
|
1390
|
+
58%,
|
|
1391
|
+
90% {
|
|
1392
|
+
opacity: 1;
|
|
1393
|
+
}
|
|
1394
|
+
96%,
|
|
1395
|
+
100% {
|
|
1396
|
+
opacity: 0;
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
874
1399
|
|
|
875
1400
|
/* status */
|
|
876
1401
|
#statusline {
|
|
@@ -1119,17 +1644,6 @@ body.state-tired #status-text {
|
|
|
1119
1644
|
transform: scaleY(0.1);
|
|
1120
1645
|
}
|
|
1121
1646
|
}
|
|
1122
|
-
@keyframes shadowPulse {
|
|
1123
|
-
0%,
|
|
1124
|
-
100% {
|
|
1125
|
-
transform: translateX(-50%) scale(1);
|
|
1126
|
-
opacity: 1;
|
|
1127
|
-
}
|
|
1128
|
-
50% {
|
|
1129
|
-
transform: translateX(-50%) scale(0.84);
|
|
1130
|
-
opacity: 0.7;
|
|
1131
|
-
}
|
|
1132
|
-
}
|
|
1133
1647
|
@keyframes ping {
|
|
1134
1648
|
0% {
|
|
1135
1649
|
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
|
}
|