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
|
-
|
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-capyreporter",
|
|
3
|
-
"version": "0.1.
|
|
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
|
|
27
|
-
bottom:
|
|
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
|
-
|
|
107
|
-
|
|
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
|
-
|
|
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
|
|
133
|
-
const
|
|
134
|
-
|
|
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
|
-
|
|
12
|
-
|
|
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;
|
|
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
|
|
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
|
|
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
|
|
66
|
-
img.style.
|
|
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
|
-
|
|
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 —
|
|
215
|
-
//
|
|
216
|
-
//
|
|
217
|
-
//
|
|
218
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
//
|
|
244
|
-
|
|
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) => {
|