clauddy 1.11.0 → 1.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/main.js +84 -6
  2. package/package.json +1 -1
package/main.js CHANGED
@@ -36,6 +36,9 @@ if (process.env.CLAUDE_CONFIG_DIR) {
36
36
  const EXTERNAL_CONFIG = path.join(DATA_DIR, 'config.json')
37
37
  // debug channel: `./pet <state>` writes here to force a state (dev only)
38
38
  const DEBUG_FILE = path.join(DATA_DIR, 'debug.json')
39
+ // remembered floating-widget position: survives restarts and lets the widget
40
+ // return to the monitor the user parked it on after a display is unplugged/replugged
41
+ const WINDOW_STATE = path.join(DATA_DIR, 'window.json')
39
42
 
40
43
  let win
41
44
  let pollTimer
@@ -49,6 +52,8 @@ let currentMode = 'floating' // 'floating' widget | 'menubar' popover
49
52
  let trayBounds = null // last known tray icon rect, to anchor the popover
50
53
  let lastBlurHide = 0 // debounce: ignore the tray click that dismissed the popover
51
54
  let sessionPct = null // authoritative session % shown in the tray title
55
+ let lastProgrammaticMove = 0 // ignore the 'moved' event our own setPosition triggers
56
+ let displayChanging = 0 // ignore OS window-shuffles while a display (dis)connects
52
57
 
53
58
  function publicConfig(c) {
54
59
  return {
@@ -151,6 +156,27 @@ function createWindow() {
151
156
  }
152
157
  })
153
158
 
159
+ // remember where the user parks the widget — but not the moves we make
160
+ // ourselves (resize re-anchoring) nor the ones the OS forces when a display
161
+ // (dis)connects, so a monitor going dark never overwrites the saved spot
162
+ win.on('moved', () => {
163
+ if (Date.now() - lastProgrammaticMove < 500) return
164
+ if (Date.now() - displayChanging < 2000) return
165
+ saveWindowState()
166
+ })
167
+
168
+ // when a monitor is unplugged/replugged (or its layout changes), put the
169
+ // floating widget back on the display the user parked it on — the OS dumps
170
+ // it on the primary display otherwise, and never moves it back on its own
171
+ const onDisplayChange = () => {
172
+ displayChanging = Date.now()
173
+ if (currentMode === 'floating' && win && !win.isDestroyed() && win.isVisible())
174
+ positionFloating()
175
+ }
176
+ screen.on('display-added', onDisplayChange)
177
+ screen.on('display-removed', onDisplayChange)
178
+ screen.on('display-metrics-changed', onDisplayChange)
179
+
154
180
  const tick = () => {
155
181
  if (!win || win.isDestroyed()) return
156
182
  try {
@@ -250,6 +276,43 @@ function showPopover() {
250
276
  win.focus()
251
277
  }
252
278
 
279
+ // route every programmatic move through here so the 'moved' handler can tell
280
+ // our own repositioning apart from a genuine user drag (and skip saving it)
281
+ function moveWindow(x, y) {
282
+ lastProgrammaticMove = Date.now()
283
+ win.setPosition(Math.round(x), Math.round(y))
284
+ }
285
+
286
+ // persist the widget's anchor — its bottom-right corner, since the window's
287
+ // size changes as the pet animates — so it can be restored later
288
+ function saveWindowState() {
289
+ if (currentMode !== 'floating' || !win || win.isDestroyed()) return
290
+ const b = win.getBounds()
291
+ try {
292
+ fs.mkdirSync(DATA_DIR, { recursive: true })
293
+ fs.writeFileSync(WINDOW_STATE, JSON.stringify({ right: b.x + b.width, bottom: b.y + b.height }))
294
+ } catch {}
295
+ }
296
+
297
+ function loadWindowState() {
298
+ try {
299
+ const s = JSON.parse(fs.readFileSync(WINDOW_STATE, 'utf8'))
300
+ return Number.isFinite(s?.right) && Number.isFinite(s?.bottom) ? s : null
301
+ } catch {
302
+ return null
303
+ }
304
+ }
305
+
306
+ // is the saved bottom-right corner on a display that's currently connected?
307
+ function cornerVisible(right, bottom) {
308
+ const x = Math.round(right - 1)
309
+ const y = Math.round(bottom - 1)
310
+ return screen.getAllDisplays().some((d) => {
311
+ const b = d.bounds
312
+ return x >= b.x && x < b.x + b.width && y >= b.y && y < b.y + b.height
313
+ })
314
+ }
315
+
253
316
  // center the popover under the tray icon, kept on-screen
254
317
  function positionUnderTray() {
255
318
  const b = win.getBounds()
@@ -261,13 +324,20 @@ function positionUnderTray() {
261
324
  y = Math.round(trayBounds.y + trayBounds.height)
262
325
  }
263
326
  x = Math.max(8, Math.min(x, workAreaSize.width - b.width - 8))
264
- win.setPosition(x, y)
327
+ moveWindow(x, y)
265
328
  }
266
329
 
267
330
  function positionFloating() {
268
- const { workAreaSize } = screen.getPrimaryDisplay()
269
331
  const b = win.getBounds()
270
- win.setPosition(workAreaSize.width - b.width - 24, workAreaSize.height - b.height - 24)
332
+ // reuse the saved spot when that monitor is still connected; otherwise fall
333
+ // back to the primary display's bottom-right corner
334
+ const saved = loadWindowState()
335
+ if (saved && cornerVisible(saved.right, saved.bottom)) {
336
+ moveWindow(saved.right - b.width, saved.bottom - b.height)
337
+ return
338
+ }
339
+ const { workAreaSize } = screen.getPrimaryDisplay()
340
+ moveWindow(workAreaSize.width - b.width - 24, workAreaSize.height - b.height - 24)
271
341
  }
272
342
 
273
343
  // the tray shows the live session % (macOS title), turning 🔥 near the limit
@@ -296,12 +366,20 @@ ipcMain.on('resize', (_e, w, h) => {
296
366
  if (!win || win.isDestroyed()) return
297
367
  const width = Math.max(100, Math.round(w))
298
368
  const height = Math.max(110, Math.round(h))
299
- win.setContentSize(width, height)
300
369
  if (currentMode === 'menubar') {
370
+ win.setContentSize(width, height)
301
371
  if (win.isVisible()) positionUnderTray() // keep it anchored under the tray
302
372
  } else {
303
- const { workAreaSize } = screen.getPrimaryDisplay()
304
- win.setPosition(workAreaSize.width - width - 24, workAreaSize.height - height - 24)
373
+ // keep the widget pinned to its current bottom-right corner on whatever
374
+ // display the user dragged it to. setContentSize grows from the top-left
375
+ // origin, so re-anchor by the old corner instead of snapping to the primary
376
+ // display's bottom-right (which yanked the widget back on every update).
377
+ const before = win.getBounds()
378
+ const right = before.x + before.width
379
+ const bottom = before.y + before.height
380
+ win.setContentSize(width, height)
381
+ const after = win.getBounds()
382
+ moveWindow(right - after.width, bottom - after.height)
305
383
  }
306
384
  })
307
385
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "clauddy",
3
3
  "desktopName": "clauddy.desktop",
4
- "version": "1.11.0",
4
+ "version": "1.11.1",
5
5
  "description": "A cute desktop pet that tracks your Claude Code usage",
6
6
  "main": "main.js",
7
7
  "bin": {