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 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
- | 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) |
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
- # 🐾 CapyReporter — 替你盯梢的水豚小报
1
+ # CapyReporter 🦫
2
2
 
3
- **一个永远置顶的 「水豚小报」:为 DeepSeek Harness Web UI 实时播报任务进度的桌面宠物。**
3
+ [English](README.md) | 简体中文
4
4
 
5
- CapyReporter 是一个小小的透明窗口,悬浮在**你桌面上的每一个应用与标签页之上**——不止于网页内部。它会在 agent 工作的每一个关键节点轻声播报,让你安心处理手头的事,绝不错过任务完成的瞬间。
5
+ 一只常驻桌面的水豚,替你盯着 DeepSeek Harness 里跑着的任务。
6
6
 
7
- ## 🦫 亮点
7
+ 它是个透明小窗口,永远浮在所有应用之上——切到浏览器也好、写代码也好、打游戏也好,它都在。Agent 每跑一步(调工具、写回复、进新阶段),气泡就更新一行;任务跑完了,"✅ 任务完成"会一直挂在屏幕上等你回来,点一下水豚就直接跳回 DSH。
8
8
 
9
- - **真正的全局置顶。** 透明、无边框、除水豚本体外点击穿透。用记事本、看视频、玩游戏、写代码——她就静静浮在那里,大小字都看得清。Windows 上由 Electron 桌面窗口实现,未安装 Electron 时自动回落到页面内悬浮。
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
- **插件市场**(上架后):*设置 → 插件市场 → 搜索「capy」→ 安装 → 刷新页面*。
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
- 然后重启 `dsh web`(或直接刷新页面)。需要 DSH web 0.1.0-rc.6 或更新版本。
28
+ 装完重启 `dsh web`(或者直接刷新页面)。需要 dsh web 0.1.0-rc.6 以上。
29
+
30
+ ## 关于置顶窗口
32
31
 
33
- **为置顶窗口准备的 Electron(可选,推荐):**
32
+ 页面内的水豚只有切回 DSH 才看得到,想要"切走也在",得靠一个真正的置顶窗口,这块用的是 Electron
34
33
 
35
- - 若 Electron 已存在于 `%USERPROFILE%\.dsh\electron\electron.exe`(或 `DSH_PET_ELECTRON_PATH` 指向的任意路径),桌面窗口开箱即用;
36
- - 没有的话,CapyReporter 仍会在 **页面内** 工作——一个悬浮在 DeepSeek Harness 页面上的透明水豚,气泡功能一个不少。
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
- | 左键点击水豚 | 回到 DeepSeek Harness |
47
- | 右键点击水豚 | 菜单:恢复气泡 · 打开 DSH · 隐藏桌面宠物 |
48
-
49
- ## 🎨 美术与许可
50
-
51
- - 内置默认水豚形象,可随时在设置中用你自己的透明 PNG 替换。
52
- - MIT 许可;默认水豚形象刻意设计为「可替换」,换图无任何限制。
44
+ | 双击气泡 | 先藏起来,有新动静再冒出来 |
45
+ | 双击水豚 | 回到 DeepSeek Harness(单击不做事,太容易误触了) |
46
+ | 右键水豚 | 菜单:恢复气泡 · 打开 DSH · 隐藏桌面宠物 |
53
47
 
54
- ## 🌍 贡献与反馈
48
+ ## 已知限制
55
49
 
56
- 欢迎提 Issue 与 PR。*本插件为社区作品——请只安装你信任的来源。*
50
+ - 置顶窗口依赖本机已安装的 Electron,目前不会自动下载(后面版本打算支持)。
51
+ - 个别独占全屏的程序(比如某些游戏)会盖住置顶窗口,这是系统层面的限制,绕不过去。
57
52
 
58
- ---
53
+ ## License
59
54
 
60
- 为 DeepSeek Harness 社区而作 🧡
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
- export { apply, inject, name }
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.2",
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 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;
@@ -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
- 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) => {
126
147
  const w = BrowserWindow.fromWebContents(event.sender);
127
148
  if (!w || w.isDestroyed()) return;
128
- const x = Number(p && p.x);
129
- const y = Number(p && p.y);
130
- 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);
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
- 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,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 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;
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 imgH = Math.round(BASE_IMG_H * scale);
62
- 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);
63
72
  bubble.style.bottom = (imgH + 8) + 'px';
64
- const aspect = img.naturalWidth && img.naturalHeight ? img.naturalWidth / img.naturalHeight : 0.65;
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
- window.petBridge.resize({ w: Math.round(w), h: Math.round(h) });
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 — pointer capture keeps the move stream alive even when the cursor
206
- // outruns the small window, and we hold the window interactive for the whole
207
- // drag (a mouseleave during a fast drag would otherwise flip it click-through
208
- // mid-drag and drop the trail).
209
- 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;
210
224
  img.addEventListener('pointerdown', (e) => {
211
225
  if (e.button !== 0) return;
212
226
  try { img.setPointerCapture(e.pointerId); } catch (err) {}
213
- drag = { sx: e.screenX, sy: e.screenY, wx: window.screenX, wy: window.screenY };
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 = null;
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
- // left-click (not a drag) -> open DSH in default browser
235
- 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));
236
251
 
237
252
  // right-click menu
238
253
  img.addEventListener('contextmenu', (e) => {