dsh-capyreporter 0.1.3 → 0.1.5

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
@@ -45,7 +45,7 @@ Restart `dsh web` (or just refresh the page). Requires DSH web 0.1.0-rc.6 or new
45
45
  | Mouse wheel over her | Resize |
46
46
  | Single click the bubble | Expand / collapse the step log |
47
47
  | Double-click the bubble | Dismiss until the next update |
48
- | Left-click the capybara | Return to DeepSeek Harness |
48
+ | Double-click the capybara | Return to DeepSeek Harness (single click does nothing — it fires too easily) |
49
49
  | Right-click the capybara | Menu: Restore bubble · Open DSH · Hide desktop pet |
50
50
 
51
51
  ## 🎨 Art & licensing
package/README.zh.md CHANGED
@@ -42,7 +42,7 @@ dsh plugin --profile web add dsh-capyreporter
42
42
  | 在水豚上滚轮 | 缩放 |
43
43
  | 单击气泡 | 展开 / 收起步骤日志 |
44
44
  | 双击气泡 | 先藏起来,有新动静再冒出来 |
45
- | 左键点水豚 | 回到 DeepSeek Harness |
45
+ | 双击水豚 | 回到 DeepSeek Harness(单击不做事,太容易误触了) |
46
46
  | 右键水豚 | 菜单:恢复气泡 · 打开 DSH · 隐藏桌面宠物 |
47
47
 
48
48
  ## 已知限制
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-capyreporter",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "CapyReporter — an always-on-top capybara task reporter for the DeepSeek Harness Web UI. A transparent floating pet that narrates each trajectory step in a speech bubble, tells you which project is reporting, alerts you when a task completes while you are away, and lets you upload your own pet art.",
5
5
  "keywords": [
6
6
  "dsh",
@@ -23,8 +23,8 @@
23
23
  #bubble {
24
24
  position: absolute;
25
25
  /* JS pins this just above the pet image on every size change;
26
- the default covers the unscaled 182px pet. */
27
- bottom: 190px;
26
+ the default covers the unscaled 132px-wide pet. */
27
+ bottom: 143px;
28
28
  left: 50%;
29
29
  transform: translateX(-50%);
30
30
  min-width: 90px;
@@ -103,8 +103,10 @@
103
103
  bottom: 0;
104
104
  left: 50%;
105
105
  transform: translateX(-50%);
106
- height: 182px;
107
- width: auto;
106
+ /* width is the base metric (132 * scale), the same as the in-page pet;
107
+ JS keeps it in sync and height follows the image aspect ratio */
108
+ width: 132px;
109
+ height: auto;
108
110
  pointer-events: auto;
109
111
  cursor: grab;
110
112
  touch-action: none;
@@ -19,10 +19,20 @@ app.disableHardwareAcceleration();
19
19
 
20
20
  let win = null;
21
21
  let topmostTimer = null;
22
+ let dragTimer = null;
23
+ let dragState = null;
22
24
  const baseUrl = process.env.DSH_CAPYREP_BASE_URL || 'http://127.0.0.1:3080';
23
25
  const W = 220;
24
26
  const H = 250;
25
27
 
28
+ function stopDrag() {
29
+ if (dragTimer) {
30
+ clearInterval(dragTimer);
31
+ dragTimer = null;
32
+ }
33
+ dragState = null;
34
+ }
35
+
26
36
  function reassertTopmost() {
27
37
  if (!win || win.isDestroyed()) return;
28
38
  try {
@@ -72,6 +82,7 @@ function createWindow() {
72
82
  win.on('blur', () => reassertTopmost());
73
83
  win.on('closed', () => {
74
84
  win = null;
85
+ stopDrag();
75
86
  if (topmostTimer) {
76
87
  clearInterval(topmostTimer);
77
88
  topmostTimer = null;
@@ -126,16 +137,37 @@ app.whenReady().then(() => {
126
137
  reassertTopmost();
127
138
  });
128
139
 
129
- ipcMain.on('homura:move', (event, p) => {
140
+ // ---- dragging ----------------------------------------------------------
141
+ // The drag loop lives here and reads the OS cursor position
142
+ // (screen.getCursorScreenPoint) instead of renderer pointer coordinates.
143
+ // A window that moves under the cursor feeds its own movement back into the
144
+ // next pointer coordinate the renderer sees, which makes the pet drift away
145
+ // from the cursor; the OS cursor position cannot be affected by our window.
146
+ ipcMain.on('homura:drag-start', (event) => {
130
147
  const w = BrowserWindow.fromWebContents(event.sender);
131
148
  if (!w || w.isDestroyed()) return;
132
- const x = Number(p && p.x);
133
- const y = Number(p && p.y);
134
- if (Number.isFinite(x) && Number.isFinite(y)) w.setPosition(Math.round(x), Math.round(y), false);
149
+ const c = screen.getCursorScreenPoint();
150
+ const b = w.getBounds();
151
+ dragState = { win: w, cx: c.x, cy: c.y, wx: b.x, wy: b.y };
152
+ if (dragTimer) clearInterval(dragTimer);
153
+ dragTimer = setInterval(() => {
154
+ if (!dragState) return stopDrag();
155
+ const win = dragState.win;
156
+ if (!win || win.isDestroyed()) return stopDrag();
157
+ const p = screen.getCursorScreenPoint();
158
+ const area = screen.getDisplayNearestPoint(p).workArea;
159
+ const x = Math.min(Math.max(area.x, dragState.wx + (p.x - dragState.cx)), area.x + Math.max(0, area.width - 60));
160
+ const y = Math.min(Math.max(area.y, dragState.wy + (p.y - dragState.cy)), area.y + Math.max(0, area.height - 60));
161
+ const cur = win.getBounds();
162
+ if (cur.x !== x || cur.y !== y) win.setPosition(x, y, false);
163
+ }, 16);
135
164
  });
165
+
166
+ ipcMain.on('homura:drag-end', () => stopDrag());
136
167
  });
137
168
 
138
169
  app.on('window-all-closed', () => {
170
+ stopDrag();
139
171
  if (topmostTimer) {
140
172
  clearInterval(topmostTimer);
141
173
  topmostTimer = null;
@@ -8,8 +8,14 @@ contextBridge.exposeInMainWorld('petBridge', {
8
8
  openSite(url) {
9
9
  ipcRenderer.send('homura:open-site', url);
10
10
  },
11
- move(x, y) {
12
- ipcRenderer.send('homura:move', { x, y });
11
+ // Dragging is owned by the main process: it tracks the OS cursor position and
12
+ // moves the window itself, so a moving window can never feed back into the
13
+ // coordinates the renderer sees.
14
+ dragStart() {
15
+ ipcRenderer.send('homura:drag-start');
16
+ },
17
+ dragEnd() {
18
+ ipcRenderer.send('homura:drag-end');
13
19
  },
14
20
  resize(size) {
15
21
  ipcRenderer.send('homura:resize', size);
@@ -1,5 +1,5 @@
1
1
  // renderer for the always-on-top CapyReporter window. Fetches image + activity from the
2
- // DSH host over HTTP; drags to move; left-click opens DSH; right-click shows a menu.
2
+ // DSH host over HTTP; drags to move; double-click opens DSH; right-click shows a menu.
3
3
  //
4
4
  // Bubble UX: compact by default (headline + latest status line); single click
5
5
  // expands into a scrollable step log; double click dismisses; the right-click
@@ -51,22 +51,26 @@
51
51
  }
52
52
 
53
53
  // Pet size: a uniform multiplier owned by the host config (shared with the
54
- // in-page pet and the Settings slider). Plain wheel over the window resizes;
55
- // the image keeps its aspect ratio, the bubble is re-pinned just above it.
54
+ // in-page pet and the Settings slider). Plain wheel over the window resizes.
55
+ // The base metric is the WIDTH (132 * scale) the same base the in-page pet
56
+ // uses — so the pet is exactly the same size in both modes; height follows the
57
+ // image's aspect ratio.
56
58
  // refit() fits the window around BOTH the pet and the bubble, so the bubble
57
59
  // can report a fuller status (expanded step log) without the window blowing up.
58
- const BASE_IMG_H = 182;
60
+ const BASE_IMG_W = 132;
59
61
  let scale = 1;
60
62
  // Only resize when the size actually changed: re-issuing an identical resize
61
63
  // every tick makes Windows' fractional-DPI bounds round-trip walk the window
62
64
  // down the screen a pixel at a time.
63
65
  const lastFit = { w: 0, h: 0 };
64
66
  function refit() {
65
- const imgH = Math.round(BASE_IMG_H * scale);
66
- img.style.height = imgH + 'px';
67
+ const imgW = Math.max(40, Math.round(BASE_IMG_W * scale));
68
+ img.style.width = imgW + 'px';
69
+ img.style.height = 'auto';
70
+ const aspect = img.naturalWidth && img.naturalHeight ? img.naturalWidth / img.naturalHeight : 0.9;
71
+ const imgH = Math.round(imgW / aspect);
67
72
  bubble.style.bottom = (imgH + 8) + 'px';
68
- const aspect = img.naturalWidth && img.naturalHeight ? img.naturalWidth / img.naturalHeight : 0.65;
69
- let w = Math.max(150, Math.round(220 * scale), Math.round(imgH * aspect) + 24);
73
+ let w = Math.max(150, imgW + 24);
70
74
  let h = Math.max(120, imgH + Math.max(Math.round(72 * scale), 96));
71
75
  if (bubble.style.display !== 'none' && bubble.scrollWidth > 0) {
72
76
  const bw = Math.min(Math.ceil(bubble.scrollWidth) + 28, 1060); // bubble is capped at 1000px; grow the window with it
@@ -211,28 +215,29 @@
211
215
  hideBubble();
212
216
  });
213
217
 
214
- // drag — pointer capture keeps the move stream alive even when the cursor
215
- // outruns the small window, and we hold the window interactive for the whole
216
- // drag (a mouseleave during a fast drag would otherwise flip it click-through
217
- // mid-drag and drop the trail).
218
- let drag = null;
218
+ // drag — the main process owns the motion: it follows the OS cursor position
219
+ // (screen.getCursorScreenPoint), which cannot be perturbed by our own window
220
+ // moving. The renderer only opens/closes the drag and keeps the window
221
+ // interactive for its whole duration (pointer capture guarantees we still get
222
+ // the pointerup even if the cursor leaves the window).
223
+ let drag = false;
219
224
  img.addEventListener('pointerdown', (e) => {
220
225
  if (e.button !== 0) return;
221
226
  try { img.setPointerCapture(e.pointerId); } catch (err) {}
222
- drag = { sx: e.screenX, sy: e.screenY, wx: window.screenX, wy: window.screenY };
227
+ drag = true;
223
228
  window.petBridge.setInteractive(true);
229
+ window.petBridge.dragStart();
224
230
  e.preventDefault();
225
231
  });
226
- window.addEventListener('pointermove', (e) => {
227
- if (drag) window.petBridge.move(drag.wx + (e.screenX - drag.sx), drag.wy + (e.screenY - drag.sy));
228
- });
229
232
  const endDrag = () => {
230
233
  if (!drag) return;
231
- drag = null;
234
+ drag = false;
235
+ window.petBridge.dragEnd();
232
236
  window.petBridge.setInteractive(true); // pointer is still over the pet when a drag ends
233
237
  };
234
238
  window.addEventListener('pointerup', endDrag);
235
239
  window.addEventListener('pointercancel', endDrag);
240
+ window.addEventListener('blur', endDrag); // alt-tab mid-drag must not leave the pet glued to the cursor
236
241
 
237
242
  // hover -> interactive (so clicks work); leave -> click-through (never mid-drag)
238
243
  root.addEventListener('mouseenter', () => window.petBridge.setInteractive(true));
@@ -240,8 +245,9 @@
240
245
  if (!drag) window.petBridge.setInteractive(false);
241
246
  });
242
247
 
243
- // left-click (not a drag) -> open DSH in default browser
244
- img.addEventListener('click', () => window.petBridge.openSite(base));
248
+ // double-click (not a drag) -> return to DeepSeek Harness. A single click must
249
+ // stay inert: it fires on every stray click and on drags that barely move.
250
+ img.addEventListener('dblclick', () => window.petBridge.openSite(base));
245
251
 
246
252
  // right-click menu
247
253
  img.addEventListener('contextmenu', (e) => {