clauddy 1.7.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 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 &amp; 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.
@@ -118,7 +139,7 @@ Settings saved from the UI live in `~/.claude-usage-monitor/config.json`, so you
118
139
  "alertThresholds": [80, 95], // notify when session/week cross these % (two levels)
119
140
  "fireThreshold": 90, // session % at which the pet catches fire (maxed out stays 100)
120
141
  "pollIntervalMs": 4000, // how often local logs are re-read
121
- "activeThresholdMs": 8000, // "working" if Claude was active within this window
142
+ "activeThresholdMs": 20000, // "active" if Claude wrote to its logs within this window
122
143
  "sleepThresholdMs": 300000, // "sleeping" after this much idle time (5 min)
123
144
  }
124
145
  ```
@@ -145,7 +166,7 @@ live. (Installed globally? Drop the `bunx`: `clauddy poke`. Working on the repo?
145
166
  ## How it works
146
167
 
147
168
  - **`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, and the working/sleeping activity status.
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.
149
170
  - **`auth.js`** — OAuth login (PKCE, same public client as Claude Code) that fetches the authoritative usage %. Token stored locally, never committed.
150
171
  - **`renderer/`** — the pet itself: an SVG pixel sprite, CSS animations, and the Web Animations API for particles.
151
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
@@ -4,6 +4,6 @@
4
4
  "alertThresholds": [80, 95],
5
5
  "fireThreshold": 90,
6
6
  "pollIntervalMs": 4000,
7
- "activeThresholdMs": 8000,
7
+ "activeThresholdMs": 20000,
8
8
  "sleepThresholdMs": 300000
9
9
  }
package/main.js CHANGED
@@ -52,7 +52,7 @@ function loadConfig() {
52
52
  alertThresholds: [80, 95],
53
53
  fireThreshold: 90, // session % at which the pet catches fire (tired still fixed at 100)
54
54
  pollIntervalMs: 4000,
55
- activeThresholdMs: 8000,
55
+ activeThresholdMs: 20000,
56
56
  sleepThresholdMs: 300000,
57
57
  }
58
58
  for (const p of [EXTERNAL_CONFIG, path.join(__dirname, 'config.json')]) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clauddy",
3
- "version": "1.7.0",
3
+ "version": "1.8.0",
4
4
  "description": "A cute desktop pet that tracks your Claude Code usage",
5
5
  "main": "main.js",
6
6
  "bin": {
@@ -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">&#10095;</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 -->
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
- // dev override from `./pet <state>`
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]) st = 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'
@@ -628,6 +628,7 @@ body.settings-open #min {
628
628
  position: absolute;
629
629
  left: 26px;
630
630
  bottom: 3px;
631
+ transition: left 0.35s ease;
631
632
  width: 86px;
632
633
  height: 77px;
633
634
  zoom: 0.8; /* a bit smaller in the scene */
@@ -657,6 +658,16 @@ body.collapsed #stage {
657
658
  body.collapsed #scene {
658
659
  display: none;
659
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
+ }
660
671
  body.collapsed #pet {
661
672
  position: static;
662
673
  left: auto;
@@ -736,6 +747,259 @@ body.settings-open #flames,
736
747
  body.settings-open #sweat {
737
748
  display: none;
738
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
+ }
739
1003
  #mouth {
740
1004
  fill: var(--eye);
741
1005
  transform-box: fill-box;
@@ -940,6 +1204,198 @@ body.drowse #claude {
940
1204
  body.celebrate #claude {
941
1205
  animation: celebrate 1.3s cubic-bezier(0.34, 1.56, 0.64, 1);
942
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
+ }
943
1399
 
944
1400
  /* status */
945
1401
  #statusline {
@@ -1188,17 +1644,6 @@ body.state-tired #status-text {
1188
1644
  transform: scaleY(0.1);
1189
1645
  }
1190
1646
  }
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
1647
  @keyframes ping {
1203
1648
  0% {
1204
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
- for (const f of files) lastMtime = Math.max(lastMtime, f.st.mtimeMs)
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 || 8000)
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
  }