dsh-capyreporter 0.1.2 → 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 +3 -1
- package/README.zh.md +31 -36
- package/lib/index.js +2 -1
- package/package.json +2 -1
- package/runtime/electron-helper/index.html +6 -4
- package/runtime/electron-helper/main.js +40 -4
- package/runtime/electron-helper/preload.js +8 -2
- package/runtime/electron-helper/renderer.js +36 -21
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# 🐾 CapyReporter — the capybara that reports for you
|
|
2
2
|
|
|
3
|
+
English | [简体中文](README.zh.md)
|
|
4
|
+
|
|
3
5
|
**An always-on-top capybara task reporter for the DeepSeek Harness Web UI.**
|
|
4
6
|
|
|
5
7
|
CapyReporter is a tiny transparent window that floats above **every** app and tab of your desktop — not just inside the page — and quietly narrates what your deepseek harness agent is doing, one step at a time, so you can focus on your other work and never miss the moment a task finishes.
|
|
@@ -43,7 +45,7 @@ Restart `dsh web` (or just refresh the page). Requires DSH web 0.1.0-rc.6 or new
|
|
|
43
45
|
| Mouse wheel over her | Resize |
|
|
44
46
|
| Single click the bubble | Expand / collapse the step log |
|
|
45
47
|
| Double-click the bubble | Dismiss until the next update |
|
|
46
|
-
|
|
|
48
|
+
| Double-click the capybara | Return to DeepSeek Harness (single click does nothing — it fires too easily) |
|
|
47
49
|
| Right-click the capybara | Menu: Restore bubble · Open DSH · Hide desktop pet |
|
|
48
50
|
|
|
49
51
|
## 🎨 Art & licensing
|
package/README.zh.md
CHANGED
|
@@ -1,60 +1,55 @@
|
|
|
1
|
-
#
|
|
1
|
+
# CapyReporter 🦫
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[English](README.md) | 简体中文
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
一只常驻桌面的水豚,替你盯着 DeepSeek Harness 里跑着的任务。
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
它是个透明小窗口,永远浮在所有应用之上——切到浏览器也好、写代码也好、打游戏也好,它都在。Agent 每跑一步(调工具、写回复、进新阶段),气泡就更新一行;任务跑完了,"✅ 任务完成"会一直挂在屏幕上等你回来,点一下水豚就直接跳回 DSH。
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
- **播报运行轨迹。** 每到一个检查点,气泡就更新一行:`🔧 工具调用`、`💭 思考中…`、`🧭 调度/干预`、`🚀 工作流阶段`、`⚠️ 报错与重试`——实时看着你的 agent「思考」。
|
|
11
|
-
- **智能气泡,不挡视线。**
|
|
12
|
-
- *紧凑模式*:标题 + 最新状态一行。
|
|
13
|
-
- *单击展开*:滚动查看本次运行的步骤历史。
|
|
14
|
-
- *双击隐藏*:水豚安静下来,直到下一条更新。
|
|
15
|
-
- *右键 → 恢复气泡*:随时找回小报。
|
|
16
|
-
- **认得到项目的名字。** 气泡直接显示运行所属的**工作区/项目名**(与左侧边栏一致)。多会话并行时,你一眼就知道是谁在汇报。
|
|
17
|
-
- **离开时也不错过完成。** `✅ 任务完成 — <项目名>` 会一直留在置顶窗口上,点击水豚一键回到 DeepSeek Harness;还可选桌面通知。
|
|
18
|
-
- **完全可自定义。** 在 **设置 → 🐾 CapyReporter** 上传任意透明 PNG 换掉形象;在水豚身上滚动鼠标滚轮即可调整大小。设置里也能一键开启/关闭、卸载插件。
|
|
19
|
-
- **持久且本地优先。** 偏好(形象、大小、位置)存于 `$DSH_HOME/capyreporter.json`;无云、无账号、无遥测。
|
|
9
|
+
## 能干什么
|
|
20
10
|
|
|
21
|
-
|
|
11
|
+
- **真的全局置顶**:基于 Electron 的透明窗口,浮在所有应用之上。透明部分点击穿透,不挡下面的操作;只有点在水豚身上才算点到它。没装 Electron 也不影响,会自动退回页面内悬浮,气泡功能一样不少。
|
|
12
|
+
- **逐步播报**:工具调用、模型输出、steering、workflow 阶段、报错重试——每到一个节点,气泡换一行,像看轨迹时间线一样看着 agent 干活。
|
|
13
|
+
- **气泡不吵**:默认只显示最新一条状态;单击展开完整的步骤历史(带滚动条,按时间排序);双击先藏起来,有新动静会自己冒出来;右键菜单里随时能找回。
|
|
14
|
+
- **认项目**:气泡标题直接用任务所属的工作区名字(和左侧边栏一致)。多会话并行的时候,一眼就知道是谁在汇报。
|
|
15
|
+
- **摸鱼友好**:切去干别的,任务完成了水豚会在置顶窗口上提醒你,桌面通知也可以开。
|
|
16
|
+
- **形象随你换**:设置里上传任意透明 PNG 就能换角色;在水豚身上滚动滚轮缩放;偏好本地保存在 `$DSH_HOME/capyreporter.json`,重启不丢。
|
|
22
17
|
|
|
23
|
-
|
|
18
|
+
## 安装
|
|
24
19
|
|
|
25
|
-
|
|
20
|
+
推荐先装 [dsh-market](https://github.com/dsh-market/dsh-market#readme),然后在 设置 → 插件市场 里搜 "capy" 一键安装。
|
|
21
|
+
|
|
22
|
+
命令行也行:
|
|
26
23
|
|
|
27
24
|
```sh
|
|
28
25
|
dsh plugin --profile web add dsh-capyreporter
|
|
29
26
|
```
|
|
30
27
|
|
|
31
|
-
|
|
28
|
+
装完重启 `dsh web`(或者直接刷新页面)。需要 dsh web 0.1.0-rc.6 以上。
|
|
29
|
+
|
|
30
|
+
## 关于置顶窗口
|
|
32
31
|
|
|
33
|
-
|
|
32
|
+
页面内的水豚只有切回 DSH 才看得到,想要"切走也在",得靠一个真正的置顶窗口,这块用的是 Electron:
|
|
34
33
|
|
|
35
|
-
-
|
|
36
|
-
-
|
|
34
|
+
- `%USERPROFILE%\.dsh\electron\electron.exe` 已经存在的话(或者用 `DSH_PET_ELECTRON_PATH` 指定路径),重启后置顶窗口会自动出现;
|
|
35
|
+
- 没有也不影响,水豚会以页面内悬浮的形式工作,功能一个不少。
|
|
37
36
|
|
|
38
|
-
##
|
|
37
|
+
## 操作一览
|
|
39
38
|
|
|
40
39
|
| 操作 | 效果 |
|
|
41
40
|
| --- | --- |
|
|
42
|
-
| 拖动水豚 |
|
|
41
|
+
| 拖动水豚 | 挪位置(窗口跟着走,甩多快都跟得上) |
|
|
43
42
|
| 在水豚上滚轮 | 缩放 |
|
|
44
43
|
| 单击气泡 | 展开 / 收起步骤日志 |
|
|
45
|
-
| 双击气泡 |
|
|
46
|
-
|
|
|
47
|
-
|
|
|
48
|
-
|
|
49
|
-
## 🎨 美术与许可
|
|
50
|
-
|
|
51
|
-
- 内置默认水豚形象,可随时在设置中用你自己的透明 PNG 替换。
|
|
52
|
-
- MIT 许可;默认水豚形象刻意设计为「可替换」,换图无任何限制。
|
|
44
|
+
| 双击气泡 | 先藏起来,有新动静再冒出来 |
|
|
45
|
+
| 双击水豚 | 回到 DeepSeek Harness(单击不做事,太容易误触了) |
|
|
46
|
+
| 右键水豚 | 菜单:恢复气泡 · 打开 DSH · 隐藏桌面宠物 |
|
|
53
47
|
|
|
54
|
-
##
|
|
48
|
+
## 已知限制
|
|
55
49
|
|
|
56
|
-
|
|
50
|
+
- 置顶窗口依赖本机已安装的 Electron,目前不会自动下载(后面版本打算支持)。
|
|
51
|
+
- 个别独占全屏的程序(比如某些游戏)会盖住置顶窗口,这是系统层面的限制,绕不过去。
|
|
57
52
|
|
|
58
|
-
|
|
53
|
+
## License
|
|
59
54
|
|
|
60
|
-
|
|
55
|
+
[MIT](LICENSE)
|
package/lib/index.js
CHANGED
|
@@ -491,4 +491,5 @@ function apply(ctx) {
|
|
|
491
491
|
if (state.enabled && state.desktopEnabled) startHelper()
|
|
492
492
|
}
|
|
493
493
|
|
|
494
|
-
|
|
494
|
+
// Pure helpers are exported for unit tests (node --test test/).
|
|
495
|
+
export { apply, inject, name, asText, titleOf, extractEventText, tailPreview, stepLineOf }
|
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",
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
"LICENSE"
|
|
44
44
|
],
|
|
45
45
|
"scripts": {
|
|
46
|
+
"test": "node --test \"test/*.test.mjs\"",
|
|
46
47
|
"verify": "node scripts/verify.mjs",
|
|
47
48
|
"prepack": "node scripts/check-bom.mjs"
|
|
48
49
|
},
|
|
@@ -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;
|
|
@@ -110,6 +121,10 @@ app.whenReady().then(() => {
|
|
|
110
121
|
const ch = Math.round(Number(size && size.h) || 0);
|
|
111
122
|
if (cw < 80 || ch < 80 || cw > 4000 || ch > 4000) return;
|
|
112
123
|
const b = w.getBounds();
|
|
124
|
+
// Same size → nothing to do. Re-issuing setBounds with identical values is
|
|
125
|
+
// not a no-op under fractional display scaling: every DIP↔physical round
|
|
126
|
+
// trip can round the position a pixel further (the "pet sinks" bug).
|
|
127
|
+
if (b.width === cw && b.height === ch) return;
|
|
113
128
|
const area = screen.getPrimaryDisplay().workArea;
|
|
114
129
|
const width = Math.min(cw, area.width);
|
|
115
130
|
const height = Math.min(ch, area.height);
|
|
@@ -122,16 +137,37 @@ app.whenReady().then(() => {
|
|
|
122
137
|
reassertTopmost();
|
|
123
138
|
});
|
|
124
139
|
|
|
125
|
-
|
|
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) => {
|
|
126
147
|
const w = BrowserWindow.fromWebContents(event.sender);
|
|
127
148
|
if (!w || w.isDestroyed()) return;
|
|
128
|
-
const
|
|
129
|
-
const
|
|
130
|
-
|
|
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);
|
|
131
164
|
});
|
|
165
|
+
|
|
166
|
+
ipcMain.on('homura:drag-end', () => stopDrag());
|
|
132
167
|
});
|
|
133
168
|
|
|
134
169
|
app.on('window-all-closed', () => {
|
|
170
|
+
stopDrag();
|
|
135
171
|
if (topmostTimer) {
|
|
136
172
|
clearInterval(topmostTimer);
|
|
137
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,18 +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;
|
|
62
|
+
// Only resize when the size actually changed: re-issuing an identical resize
|
|
63
|
+
// every tick makes Windows' fractional-DPI bounds round-trip walk the window
|
|
64
|
+
// down the screen a pixel at a time.
|
|
65
|
+
const lastFit = { w: 0, h: 0 };
|
|
60
66
|
function refit() {
|
|
61
|
-
const
|
|
62
|
-
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);
|
|
63
72
|
bubble.style.bottom = (imgH + 8) + 'px';
|
|
64
|
-
|
|
65
|
-
let w = Math.max(150, Math.round(220 * scale), Math.round(imgH * aspect) + 24);
|
|
73
|
+
let w = Math.max(150, imgW + 24);
|
|
66
74
|
let h = Math.max(120, imgH + Math.max(Math.round(72 * scale), 96));
|
|
67
75
|
if (bubble.style.display !== 'none' && bubble.scrollWidth > 0) {
|
|
68
76
|
const bw = Math.min(Math.ceil(bubble.scrollWidth) + 28, 1060); // bubble is capped at 1000px; grow the window with it
|
|
@@ -71,7 +79,12 @@
|
|
|
71
79
|
w = Math.max(w, bw);
|
|
72
80
|
h = Math.max(h, imgH + 12 + bh);
|
|
73
81
|
}
|
|
74
|
-
|
|
82
|
+
w = Math.round(w);
|
|
83
|
+
h = Math.round(h);
|
|
84
|
+
if (w === lastFit.w && h === lastFit.h) return;
|
|
85
|
+
lastFit.w = w;
|
|
86
|
+
lastFit.h = h;
|
|
87
|
+
window.petBridge.resize({ w: w, h: h });
|
|
75
88
|
}
|
|
76
89
|
img.addEventListener('load', () => refit());
|
|
77
90
|
window.addEventListener('wheel', (e) => {
|
|
@@ -202,28 +215,29 @@
|
|
|
202
215
|
hideBubble();
|
|
203
216
|
});
|
|
204
217
|
|
|
205
|
-
// drag —
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
|
|
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;
|
|
210
224
|
img.addEventListener('pointerdown', (e) => {
|
|
211
225
|
if (e.button !== 0) return;
|
|
212
226
|
try { img.setPointerCapture(e.pointerId); } catch (err) {}
|
|
213
|
-
drag =
|
|
227
|
+
drag = true;
|
|
214
228
|
window.petBridge.setInteractive(true);
|
|
229
|
+
window.petBridge.dragStart();
|
|
215
230
|
e.preventDefault();
|
|
216
231
|
});
|
|
217
|
-
window.addEventListener('pointermove', (e) => {
|
|
218
|
-
if (drag) window.petBridge.move(drag.wx + (e.screenX - drag.sx), drag.wy + (e.screenY - drag.sy));
|
|
219
|
-
});
|
|
220
232
|
const endDrag = () => {
|
|
221
233
|
if (!drag) return;
|
|
222
|
-
drag =
|
|
234
|
+
drag = false;
|
|
235
|
+
window.petBridge.dragEnd();
|
|
223
236
|
window.petBridge.setInteractive(true); // pointer is still over the pet when a drag ends
|
|
224
237
|
};
|
|
225
238
|
window.addEventListener('pointerup', endDrag);
|
|
226
239
|
window.addEventListener('pointercancel', endDrag);
|
|
240
|
+
window.addEventListener('blur', endDrag); // alt-tab mid-drag must not leave the pet glued to the cursor
|
|
227
241
|
|
|
228
242
|
// hover -> interactive (so clicks work); leave -> click-through (never mid-drag)
|
|
229
243
|
root.addEventListener('mouseenter', () => window.petBridge.setInteractive(true));
|
|
@@ -231,8 +245,9 @@
|
|
|
231
245
|
if (!drag) window.petBridge.setInteractive(false);
|
|
232
246
|
});
|
|
233
247
|
|
|
234
|
-
//
|
|
235
|
-
|
|
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));
|
|
236
251
|
|
|
237
252
|
// right-click menu
|
|
238
253
|
img.addEventListener('contextmenu', (e) => {
|