deepseek-computer-use 0.1.0 → 0.1.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.
package/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # deepseek-computer-use
2
2
 
3
+ [![npm](https://img.shields.io/npm/v/deepseek-computer-use.svg)](https://www.npmjs.com/package/deepseek-computer-use)
4
+
3
5
  > 社区插件,**非 DeepSeek 官方出品**。官方包在 `@deepseek-ai/*` scope 下。
4
6
 
5
7
  给 DeepSeek Harness 的 **Windows 桌面控制(Computer Use)** 插件。
@@ -19,13 +21,11 @@ Windows 原生三件套:**UI Automation 观测 + SendInput 注入 + Windows.Gr
19
21
  UI Automation、WGC)。
20
22
 
21
23
  ```bash
22
- # 发布到 npm 之后
23
- npm install --save deepseek-computer-use
24
-
25
- # 现在还只能用 GitHub 源
26
- npm install --save github:STARDUSTLC666/deepseek-computer-use
24
+ npm install deepseek-computer-use
27
25
  ```
28
26
 
27
+ [npmjs.com/package/deepseek-computer-use](https://www.npmjs.com/package/deepseek-computer-use)
28
+
29
29
  然后在本机 profile 的 `cordis.patch.yml` 里启用(详见本文件末尾的配置示例):
30
30
 
31
31
  ```yaml
@@ -111,7 +111,17 @@ Codex 用两层覆盖层告诉用户"AI 正在动你的电脑",这里做的是
111
111
  **Cursor Overlay — 虚拟光标 + 系统光标抑制**
112
112
  - 覆盖层存在期间常驻一个带描边和投影的白色指针,启动时落在真实指针位置,
113
113
  之后 0.35 缓动滑向每个动作点;背后一层极淡的强调色光晕
114
- - 默认抑制真实系统光标,退出/Esc/超时用 `SPI_SETCURSORS` 恢复;
114
+ - 默认抑制真实系统光标(`SetSystemCursor` 换成透明图标),五层恢复保障:
115
+ 1. 正常退出 / Esc / 超时 → 退出钩子里 `SPI_SETCURSORS`
116
+ 2. **独立守卫进程**(`--cursor-guard <ownerPid>`)盯着覆盖层,覆盖层被强杀后
117
+ 它负责还原——和 Codex 的 `system-cursor-manager` 同一思路
118
+ 3. helper 的 **500ms 看门狗**:标记里的 PID 失效就还原,不需要任何请求
119
+ 4. helper **启动即自检**:上次会话残留的标记会被清掉
120
+ 5. 插件 dispose 时发 `restore_cursor` RPC
121
+ - 抑制期间 `%LOCALAPPDATA%\dsh-computer-use\cursor-hidden.flag` 记录自己的 PID,
122
+ 守卫只清理属于自己 owner 的标记,不会误伤新一代覆盖层
123
+ - Windows 在休眠恢复 / 换分辨率后会重置光标,覆盖层每约 1.8 秒**重新抑制**一次
124
+ - 完全不想抑制:`DSH_CU_KEEP_CURSOR=1`
115
125
  helper 强杀覆盖层时也会兜底恢复一次
116
126
 
117
127
  **DPI 关键点**:覆盖层进程必须 Per-Monitor-V2 感知,否则在 150% 缩放屏上
@@ -238,7 +248,7 @@ node test/integration.mjs
238
248
 
239
249
  固定装置是一个独立的 WinForms 可执行文件(`test/bin/dsh-cu-fixture.exe`,一个文本框 +
240
250
  一个名为 "Delete all" 的按钮;特意不用 shell 托管——shell 窗口本身被硬拒绝)。覆盖:
241
- 14 个工具注册、截图数组与 origin/zIndex、`accessibility:null` 语义、`set_value`、
251
+ 16 个工具注册、截图数组与 origin/zIndex、`accessibility:null` 语义、`set_value`、
242
252
  `type_text`、危险按钮触发审批、`dryRun` 不落地、`denyApps` 拒绝、Win 键硬拒绝、
243
253
  `scroll` 契约。
244
254
 
@@ -252,7 +262,7 @@ MIT。第三方商用前请自行做安全评审:这个插件能让模型操
252
262
  | 文件 | 用途 | 大小 |
253
263
  |---|---|---|
254
264
  | `dsh-computer-use-helper.exe` | UIA / SendInput / 抓屏调度 / 音频调度 | ~50KB |
255
- | `dsh-cu-overlay.exe` | 鲸鱼娘 + 虚拟光标覆盖层 | ~30KB |
265
+ | `dsh-cu-overlay.exe` | 呼吸光边 + 虚拟光标覆盖层 | ~30KB |
256
266
  | `capture/` | WGC 抓屏(.NET Framework 4.8 + SharpDX) | 0.68MB |
257
267
  | `audio/` | WASAPI 环回录音(NAudio) | 0.60MB |
258
268
  | **合计** | | **~1.4MB** |
@@ -289,6 +299,9 @@ marker 文件按 **会话 + turn** 命名,避免旧会话的 Esc 标记误伤
289
299
  audio: false # 开启 WASAPI 环回录音(start/stop_audio_recording)
290
300
  redactApps: [] # 这些应用的截图永不出机器,例如 ["1password", "银行"]
291
301
  defaultIncludeScreenshot: true
302
+ timeoutMs: 10000 # 单次 RPC 超时(launch_app 用 launchTimeoutMs)
303
+ helperPath: null # 覆盖 helper 路径(仅信任场景)
304
+ riskyPattern: '' # 自定义「危险动作」正则,命中走审批
292
305
  toolPrefix: '' # 例如 cu_ 可避免和其他插件的 click 重名
293
306
  ```
294
307
 
@@ -297,6 +310,8 @@ marker 文件按 **会话 + turn** 命名,避免旧会话的 Esc 标记误伤
297
310
  - 只支持 Windows;驱动层是接口(`IPlatformDriver`),macOS / Linux 可照着补。
298
311
  - Codex window2 没有的接口(`paste` / `clipboard_*` / `key_down` / `move`)这里也没有。
299
312
  - `App.useCount` 没有数据来源,按可选字段留空,不伪造。
313
+ - `redactApps` 在**插件层**执行:直接调用 helper 的 RPC 不受它约束,别把 helper 单独暴露给非受信调用方。
314
+ - 抢焦点类工具(如 ProcessLasso)会让 `type_text` 返回 `focus_failed`,这是设计行为——宁可不输入,也不打到别的窗口。
300
315
 
301
316
  ## License
302
317
 
package/lib/index.js CHANGED
@@ -143,6 +143,9 @@ export function apply(ctx, rawConfig = {}) {
143
143
 
144
144
  if (typeof ctx.on === 'function') {
145
145
  ctx.on('dispose', () => {
146
+ // The overlay hides the system cursor while it is up; make sure teardown puts it
147
+ // back even if the overlay process was killed instead of exiting cleanly.
148
+ try { bridge?.request('restore_cursor', {}, { timeoutMs: 2000, skipTurnTransition: false }); } catch { /* best effort */ }
146
149
  try { bridge?.dispose(); } catch { /* already gone */ }
147
150
  bridge = null;
148
151
  for (const dispose of disposers) {
package/native/Driver.cs CHANGED
@@ -19,6 +19,8 @@ public interface IPlatformDriver : IDisposable
19
19
  string PlatformName { get; }
20
20
  Dictionary<string, object> Ping(Dictionary<string, object> p);
21
21
  Dictionary<string, object> Hello(Dictionary<string, object> p);
22
+ Dictionary<string, object> RestoreCursor(Dictionary<string, object> p);
23
+ void Maintenance();
22
24
  Dictionary<string, object> Close(Dictionary<string, object> p);
23
25
  Dictionary<string, object> ListWindows(Dictionary<string, object> p);
24
26
  Dictionary<string, object> ListApps(Dictionary<string, object> p);
@@ -182,14 +184,12 @@ public class WindowsDriver : IPlatformDriver
182
184
  // border and a virtual cursor; this is the DSH equivalent, and Esc is the kill
183
185
  // switch: the overlay drops a cancel file that every input path checks first.
184
186
  Process _overlay;
187
+ System.Threading.Timer _cursorWatchdog;
185
188
  DateTime _overlayUntil = DateTime.MinValue;
186
189
  string _overlayCancelFile;
187
- string _overlayImage = "";
188
- string _overlayCrop = "";
189
190
  string _overlayText = "DSH 正在使用您的电脑";
190
191
  string _overlayHint = "Esc 取消";
191
192
  string _overlayAccent = "#3f7fd8";
192
- string _overlayCorner = "br";
193
193
  string _cursorFile; // the overlay polls this to draw the virtual cursor
194
194
  /// Turn token that the user interrupted with Esc. Codex persists the same
195
195
  /// fact as a marker file per conversation+turn; here the token travels with
@@ -213,6 +213,7 @@ public class WindowsDriver : IPlatformDriver
213
213
 
214
214
  public WindowsDriver()
215
215
  {
216
+ Maintenance(); // startup repair: a previous session may have died with cursors hidden
216
217
  try { SetThreadDpiAwarenessContext(new IntPtr(-4)); } catch { }
217
218
  try { SetProcessDPIAware(); } catch { }
218
219
  }
@@ -340,18 +341,12 @@ public class WindowsDriver : IPlatformDriver
340
341
 
341
342
  public Dictionary<string, object> OverlayShow(Dictionary<string, object> p)
342
343
  {
343
- string image = Json.GetString(p, "image");
344
- if (!string.IsNullOrEmpty(image)) _overlayImage = image;
345
- string crop = Json.GetString(p, "crop");
346
- if (!string.IsNullOrEmpty(crop)) _overlayCrop = crop;
347
344
  string text = Json.GetString(p, "text");
348
345
  if (!string.IsNullOrEmpty(text)) _overlayText = text;
349
346
  string hint = Json.GetString(p, "hint");
350
347
  if (!string.IsNullOrEmpty(hint)) _overlayHint = hint;
351
348
  string accent = Json.GetString(p, "accent");
352
349
  if (!string.IsNullOrEmpty(accent)) _overlayAccent = accent;
353
- string corner = Json.GetString(p, "corner");
354
- if (!string.IsNullOrEmpty(corner)) _overlayCorner = corner;
355
350
  EnsureOverlay(Json.GetLong(p, "ttlMs", 20000));
356
351
  Dictionary<string, object> r = new Dictionary<string, object>();
357
352
  r["visible"] = true;
@@ -391,15 +386,22 @@ public class WindowsDriver : IPlatformDriver
391
386
  args.Append(" --text \"").Append(_overlayText).Append('"');
392
387
  args.Append(" --hint \"").Append(_overlayHint).Append('"');
393
388
  args.Append(" --accent \"").Append(_overlayAccent).Append('"');
394
- args.Append(" --corner ").Append(_overlayCorner);
395
389
  if (_cursorFile != null) args.Append(" --cursor-file \"").Append(_cursorFile).Append('"');
396
- if (Environment.GetEnvironmentVariable("DSH_CU_SUPPRESS_CURSOR") == "1") args.Append(" --suppress-cursor");
390
+ // Suppression is the default (the virtual cursor stands in for the real one);
391
+ // DSH_CU_KEEP_CURSOR=1 keeps the real pointer visible instead.
392
+ if (Environment.GetEnvironmentVariable("DSH_CU_KEEP_CURSOR") == "1") args.Append(" --no-suppress-cursor");
397
393
  try
398
394
  {
399
395
  ProcessStartInfo psi = new ProcessStartInfo(exe, args.ToString());
400
396
  psi.UseShellExecute = false;
401
397
  _overlay = Process.Start(psi);
402
398
  _overlayUntil = DateTime.UtcNow.AddMilliseconds(ttlMs);
399
+ // Codex runs a dedicated system-cursor-manager process that restores the
400
+ // pointer the moment its parent dies. This timer gives the same guarantee
401
+ // without a second exe: if the overlay vanishes with cursors hidden, the
402
+ // pointer comes back within half a second, with no request needed.
403
+ if (_cursorWatchdog == null)
404
+ _cursorWatchdog = new System.Threading.Timer(delegate { try { Maintenance(); } catch { } }, null, 500, 500);
403
405
  }
404
406
  catch { _overlay = null; }
405
407
  }
@@ -410,7 +412,8 @@ public class WindowsDriver : IPlatformDriver
410
412
  // Kill() skips the overlay's exit handlers, which is where it would restore the
411
413
  // system cursors. Restore them from here so a terminated overlay can never leave
412
414
  // the user without a pointer.
413
- try { SystemParametersInfo(0x0057, 0, IntPtr.Zero, 0); } catch { }
415
+ RestoreCursors();
416
+ try { File.Delete(OverlayCursorFlagPath()); } catch { }
414
417
  try { if (_overlayCancelFile != null && File.Exists(_overlayCancelFile)) File.Delete(_overlayCancelFile); } catch { }
415
418
  _overlay = null;
416
419
  _overlayUntil = DateTime.MinValue;
@@ -609,6 +612,43 @@ public class WindowsDriver : IPlatformDriver
609
612
  return d;
610
613
  }
611
614
 
615
+ /// Run cheaply before every request: if a previous overlay died while it was hiding
616
+ /// the system cursors, put them back. A blank desktop cursor is worse than a stray
617
+ /// visible one, so this never assumes the overlay cleaned up after itself.
618
+ public void Maintenance()
619
+ {
620
+ try
621
+ {
622
+ string flag = OverlayCursorFlagPath();
623
+ if (!File.Exists(flag)) return;
624
+ int pid;
625
+ if (!int.TryParse(File.ReadAllText(flag).Trim(), out pid)) { RestoreCursors(); return; }
626
+ bool alive;
627
+ try { Process.GetProcessById(pid); alive = true; } catch { alive = false; }
628
+ if (!alive) { RestoreCursors(); try { File.Delete(flag); } catch { } }
629
+ }
630
+ catch { }
631
+ }
632
+
633
+ static string OverlayCursorFlagPath()
634
+ {
635
+ return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "dsh-computer-use", "cursor-hidden.flag");
636
+ }
637
+
638
+ static void RestoreCursors()
639
+ {
640
+ try { SystemParametersInfo(0x0057, 0, IntPtr.Zero, 0); } catch { }
641
+ }
642
+
643
+ public Dictionary<string, object> RestoreCursor(Dictionary<string, object> p)
644
+ {
645
+ RestoreCursors();
646
+ try { File.Delete(OverlayCursorFlagPath()); } catch { }
647
+ Dictionary<string, object> r = new Dictionary<string, object>();
648
+ r["restored"] = true;
649
+ return r;
650
+ }
651
+
612
652
  public Dictionary<string, object> Hello(Dictionary<string, object> p)
613
653
  {
614
654
  Dictionary<string, object> d = new Dictionary<string, object>();
@@ -811,8 +851,13 @@ public class WindowsDriver : IPlatformDriver
811
851
  for (int i = 0; i < args.Count; i++)
812
852
  {
813
853
  if (i > 0) argLine.Append(' ');
814
- string a = Convert.ToString(args[i], System.Globalization.CultureInfo.InvariantCulture);
815
- if (a.IndexOf(' ') >= 0) argLine.Append('"').Append(a).Append('"');
854
+ string a = Convert.ToString(args[i], System.Globalization.CultureInfo.InvariantCulture) ?? "";
855
+ // A quote or newline would let a caller smuggle extra arguments past the
856
+ // single quoting rule below, so refuse instead of guessing.
857
+ if (a.IndexOf('"') >= 0 || a.IndexOf('\n') >= 0 || a.IndexOf('\r') >= 0)
858
+ throw new ToolError("bad_request", "launch_app argument " + i + " contains a quote or newline; pass a single plain argument");
859
+ if (a.Length == 0 || a.IndexOf(' ') >= 0 || a.IndexOf('\t') >= 0)
860
+ argLine.Append('"').Append(a).Append('"');
816
861
  else argLine.Append(a);
817
862
  }
818
863
  psi.Arguments = argLine.ToString();
@@ -1642,13 +1687,16 @@ public class WindowsDriver : IPlatformDriver
1642
1687
  EnsureOverlay(8000);
1643
1688
  IntPtr h = ResolveWindow(p);
1644
1689
  AssertAppApproved(h);
1645
- Activate(h);
1690
+ // Validation first: an invalid call must not activate a window or move the pointer.
1646
1691
  double scrollY = Json.GetDouble(p, "scrollY", 0);
1647
1692
  double scrollX = Json.GetDouble(p, "scrollX", 0);
1648
1693
  double x = Json.GetDouble(p, "x", double.NaN);
1649
1694
  double y = Json.GetDouble(p, "y", double.NaN);
1650
1695
  if (double.IsNaN(x) || double.IsNaN(y))
1651
1696
  throw new ToolError("bad_request", "scroll requires finite 'x' and 'y' (the point to scroll from, in window/screenshot coordinates); click a pane first if it needs focus");
1697
+ if (Math.Abs(scrollX) < 0.0001 && Math.Abs(scrollY) < 0.0001)
1698
+ throw new ToolError("bad_request", "scroll requires a non-zero scrollX or scrollY");
1699
+ Activate(h);
1652
1700
  int sx, sy;
1653
1701
  string scrollShot = Json.GetString(p, "screenshotId");
1654
1702
  int originLeft, originTop;
@@ -1685,6 +1733,12 @@ public class WindowsDriver : IPlatformDriver
1685
1733
  EnsureOverlay(8000);
1686
1734
  IntPtr h = ResolveWindow(p);
1687
1735
  AssertAppApproved(h);
1736
+ double fromX = Json.GetDouble(p, "from_x", double.NaN);
1737
+ double fromY = Json.GetDouble(p, "from_y", double.NaN);
1738
+ double toX = Json.GetDouble(p, "to_x", double.NaN);
1739
+ double toY = Json.GetDouble(p, "to_y", double.NaN);
1740
+ if (double.IsNaN(fromX) || double.IsNaN(fromY) || double.IsNaN(toX) || double.IsNaN(toY))
1741
+ throw new ToolError("bad_request", "drag requires finite from_x/from_y/to_x/to_y (window-relative pixels)");
1688
1742
  Activate(h);
1689
1743
  int originX, originY;
1690
1744
  double dragScaleX = 1.0, dragScaleY = 1.0;
@@ -1692,8 +1746,8 @@ public class WindowsDriver : IPlatformDriver
1692
1746
  if (!string.IsNullOrEmpty(dragShot))
1693
1747
  {
1694
1748
  ShotRegion region = ResolveShot(h, dragShot);
1695
- AssertPointInsideShot(Json.GetDouble(p, "from_x", double.NaN), Json.GetDouble(p, "from_y", double.NaN), region, dragShot, "drag start");
1696
- AssertPointInsideShot(Json.GetDouble(p, "to_x", double.NaN), Json.GetDouble(p, "to_y", double.NaN), region, dragShot, "drag end");
1749
+ AssertPointInsideShot(fromX, fromY, region, dragShot, "drag start");
1750
+ AssertPointInsideShot(toX, toY, region, dragShot, "drag end");
1697
1751
  originX = region.OriginX; originY = region.OriginY;
1698
1752
  dragScaleX = region.ScaleX; dragScaleY = region.ScaleY;
1699
1753
  }
@@ -1702,10 +1756,10 @@ public class WindowsDriver : IPlatformDriver
1702
1756
  RECT b = BoundsOf(h);
1703
1757
  originX = b.Left; originY = b.Top;
1704
1758
  }
1705
- int fx = originX + (int)Math.Round(Json.GetDouble(p, "from_x", 0) * dragScaleX);
1706
- int fy = originY + (int)Math.Round(Json.GetDouble(p, "from_y", 0) * dragScaleY);
1707
- int tx = originX + (int)Math.Round(Json.GetDouble(p, "to_x", 0) * dragScaleX);
1708
- int ty = originY + (int)Math.Round(Json.GetDouble(p, "to_y", 0) * dragScaleY);
1759
+ int fx = originX + (int)Math.Round(fromX * dragScaleX);
1760
+ int fy = originY + (int)Math.Round(fromY * dragScaleY);
1761
+ int tx = originX + (int)Math.Round(toX * dragScaleX);
1762
+ int ty = originY + (int)Math.Round(toY * dragScaleY);
1709
1763
 
1710
1764
  MarkCursor(fx, fy);
1711
1765
  SetCursorPos(fx, fy);
package/native/Overlay.cs CHANGED
@@ -52,9 +52,6 @@ public static class Native
52
52
  public const int ULW_ALPHA = 2;
53
53
  public const uint SPI_SETCURSORS = 0x0057;
54
54
 
55
- public static bool LastLayerOk = true;
56
- public static int LastLayerError = 0;
57
-
58
55
  public static void ApplyLayered(Form form, Bitmap surface)
59
56
  {
60
57
  IntPtr screenDc = GetDC(IntPtr.Zero);
@@ -67,8 +64,7 @@ public static class Native
67
64
  POINT src = new POINT { X = 0, Y = 0 };
68
65
  SIZE size = new SIZE { CX = surface.Width, CY = surface.Height };
69
66
  BLENDFUNCTION blend = new BLENDFUNCTION { BlendOp = 0, BlendFlags = 0, SourceConstantAlpha = 255, AlphaFormat = 1 };
70
- LastLayerOk = UpdateLayeredWindow(form.Handle, screenDc, ref dst, ref size, memDc, ref src, 0, ref blend, ULW_ALPHA);
71
- LastLayerError = LastLayerOk ? 0 : Marshal.GetLastWin32Error();
67
+ UpdateLayeredWindow(form.Handle, screenDc, ref dst, ref size, memDc, ref src, 0, ref blend, ULW_ALPHA);
72
68
  }
73
69
  finally
74
70
  {
@@ -84,19 +80,48 @@ public static class Native
84
80
  try { SetWindowPos(form.Handle, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOACTIVATE | SWP_SHOWWINDOW); } catch { }
85
81
  }
86
82
 
83
+ /// Path of the "cursors are hidden" flag. It carries our PID so a long-lived
84
+ /// supervisor can tell "the overlay is still hiding them" from "the overlay died
85
+ /// and left the desktop without a pointer".
86
+ public static string CursorFlagPath()
87
+ {
88
+ string dir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "dsh-computer-use");
89
+ return Path.Combine(dir, "cursor-hidden.flag");
90
+ }
91
+
92
+ static IntPtr _blankCursor;
93
+
87
94
  public static void SuppressSystemCursors(bool suppress)
88
95
  {
89
96
  try
90
97
  {
91
- if (!suppress) { SystemParametersInfo(SPI_SETCURSORS, 0, IntPtr.Zero, 0); return; }
92
- byte[] andMask = new byte[128];
93
- byte[] xorMask = new byte[128];
94
- for (int i = 0; i < andMask.Length; i++) andMask[i] = 0xFF;
95
- IntPtr blank = CreateCursor(IntPtr.Zero, 0, 0, 32, 32, andMask, xorMask);
98
+ if (!suppress)
99
+ {
100
+ SystemParametersInfo(SPI_SETCURSORS, 0, IntPtr.Zero, 0);
101
+ try { File.Delete(CursorFlagPath()); } catch { }
102
+ if (_blankCursor != IntPtr.Zero) { try { DestroyCursor(_blankCursor); } catch { } _blankCursor = IntPtr.Zero; }
103
+ return;
104
+ }
105
+ // Cache the blank cursor: Windows resets the cursors on resume, display
106
+ // change and some logins, so the controller calls this again periodically
107
+ // (Codex calls the same "system cursor re-suppression").
108
+ if (_blankCursor == IntPtr.Zero)
109
+ {
110
+ byte[] andMask = new byte[128];
111
+ byte[] xorMask = new byte[128];
112
+ for (int i = 0; i < andMask.Length; i++) andMask[i] = 0xFF;
113
+ _blankCursor = CreateCursor(IntPtr.Zero, 0, 0, 32, 32, andMask, xorMask);
114
+ }
115
+ IntPtr blank = _blankCursor;
96
116
  if (blank == IntPtr.Zero) return;
97
117
  uint[] ids = new uint[] { 32512, 32513, 32514, 32515, 32516, 32631, 32640, 32641, 32642, 32643, 32644, 32645, 32646, 32648, 32649, 32650, 32651, 32671 };
98
118
  foreach (uint id in ids) SetSystemCursor(CopyIcon(blank), id);
99
- DestroyCursor(blank);
119
+ try
120
+ {
121
+ Directory.CreateDirectory(Path.GetDirectoryName(CursorFlagPath()));
122
+ File.WriteAllText(CursorFlagPath(), System.Diagnostics.Process.GetCurrentProcess().Id.ToString());
123
+ }
124
+ catch { }
100
125
  }
101
126
  catch { }
102
127
  }
@@ -204,19 +229,6 @@ public sealed class CursorForm : Form
204
229
  g.DrawPolygon(outline, arrow);
205
230
  }
206
231
  }
207
- string dump = Environment.GetEnvironmentVariable("DSH_CU_DUMP");
208
- if (!string.IsNullOrEmpty(dump))
209
- {
210
- try
211
- {
212
- surface.Save(dump, ImageFormat.Png);
213
- System.IO.File.AppendAllText(dump + ".log",
214
- DateTime.Now.ToString("HH:mm:ss.fff") + " visible=" + Visible + " handle=" + Handle.ToInt64() +
215
- " bounds=" + Left + "," + Top + " " + Width + "x" + Height +
216
- " layerOk=" + Native.LastLayerOk + " err=" + Native.LastLayerError + "\n");
217
- }
218
- catch { }
219
- }
220
232
  if (!Visible) Show();
221
233
  Native.ApplyLayered(this, surface);
222
234
  }
@@ -391,7 +403,7 @@ public sealed class StripForm : Form
391
403
  // halo: widest + faintest first, then tighten
392
404
  for (int i = 3; i >= 1; i--)
393
405
  {
394
- float width = i * 2.6f;
406
+ float width = i * 3.2f;
395
407
  int alpha = Math.Max(6, glowAlpha / (i + 1));
396
408
  using (Pen halo = new Pen(Color.FromArgb(alpha, accent), width))
397
409
  {
@@ -410,7 +422,7 @@ public sealed class StripForm : Form
410
422
  }
411
423
 
412
424
  // core hairline
413
- using (Pen core = new Pen(Color.FromArgb(coreAlpha, accent), 1.4f))
425
+ using (Pen core = new Pen(Color.FromArgb(coreAlpha, accent), 2f))
414
426
  {
415
427
  core.Alignment = PenAlignment.Center;
416
428
  if (vertical)
@@ -514,7 +526,7 @@ internal sealed class Controller : ApplicationContext
514
526
  delegate IntPtr LowLevelKeyboardProc(int nCode, IntPtr wParam, IntPtr lParam);
515
527
 
516
528
  const int Radius = 18; // corner radius
517
- const int Glow = 12; // band the halo fades inside
529
+ const int Glow = 16; // band the halo fades inside
518
530
 
519
531
  public Controller(int ttlMs)
520
532
  {
@@ -550,8 +562,8 @@ internal sealed class Controller : ApplicationContext
550
562
  Rectangle screen = SystemInformation.VirtualScreen;
551
563
  // slow 3.2s breath: the frame gets a little brighter and a touch wider
552
564
  double phase = (Math.Sin(_tick / 26.0 - 1.57) + 1.0) / 2.0;
553
- int coreAlpha = 170 + (int)Math.Round(70 * phase);
554
- int glowAlpha = 26 + (int)Math.Round(20 * phase);
565
+ int coreAlpha = 200 + (int)Math.Round(55 * phase); // clearly visible, still a hairline
566
+ int glowAlpha = 42 + (int)Math.Round(26 * phase);
555
567
  int thickness = Glow;
556
568
 
557
569
  int x0 = screen.Left, y0 = screen.Top, w = screen.Width, h = screen.Height;
@@ -602,6 +614,10 @@ internal sealed class Controller : ApplicationContext
602
614
  }
603
615
  catch { }
604
616
 
617
+ // Windows restores the real cursors on resume / display change; put the blank
618
+ // ones back while the indicator is up (Codex: "schedule system cursor re-suppression").
619
+ if (Config.SuppressCursor && _tick % 40 == 0) Native.SuppressSystemCursors(true);
620
+
605
621
  if (!_cursor.Live)
606
622
  {
607
623
  PointNative p;
@@ -641,6 +657,36 @@ internal static class Program
641
657
  try { SetProcessDpiAwarenessContext(new IntPtr(-4)); } catch { }
642
658
  try { SetProcessDPIAware(); } catch { }
643
659
 
660
+ // Guard mode: wait for the owner, then restore the cursors. This must run before
661
+ // anything else - a guard that reached the suppression block would hide the
662
+ // cursors again and spawn another guard, and the chain would never stop.
663
+ int guardIndex = Array.IndexOf(args, "--cursor-guard");
664
+ if (guardIndex >= 0 && args.Length > guardIndex + 1)
665
+ {
666
+ int ownerPid;
667
+ if (int.TryParse(args[guardIndex + 1], out ownerPid) && ownerPid != System.Diagnostics.Process.GetCurrentProcess().Id)
668
+ {
669
+ try
670
+ {
671
+ System.Diagnostics.Process owner = System.Diagnostics.Process.GetProcessById(ownerPid);
672
+ owner.WaitForExit(600000); // hard cap: a guard never lives forever
673
+ }
674
+ catch { /* owner already gone */ }
675
+ // Only clean up our own owner's suppression: a newer overlay may have
676
+ // taken over the cursors in the meantime, and restoring them here would
677
+ // make the real pointer reappear under a live indicator.
678
+ bool ours = false;
679
+ try
680
+ {
681
+ string flagPath = Native.CursorFlagPath();
682
+ ours = File.Exists(flagPath) && File.ReadAllText(flagPath).Trim() == ownerPid.ToString();
683
+ }
684
+ catch { }
685
+ if (ours) Native.SuppressSystemCursors(false);
686
+ }
687
+ return;
688
+ }
689
+
644
690
  int ttlMs = 20000;
645
691
  for (int i = 0; i < args.Length - 1; i++)
646
692
  {
@@ -660,6 +706,16 @@ internal static class Program
660
706
  {
661
707
  Native.SuppressSystemCursors(true);
662
708
  AppDomain.CurrentDomain.ProcessExit += (s, e) => Native.SuppressSystemCursors(false);
709
+ try
710
+ {
711
+ System.Diagnostics.ProcessStartInfo guard = new System.Diagnostics.ProcessStartInfo(
712
+ System.Reflection.Assembly.GetExecutingAssembly().Location,
713
+ "--cursor-guard " + System.Diagnostics.Process.GetCurrentProcess().Id);
714
+ guard.UseShellExecute = false;
715
+ guard.CreateNoWindow = true;
716
+ System.Diagnostics.Process.Start(guard);
717
+ }
718
+ catch { /* the helper watchdog still covers us */ }
663
719
  }
664
720
 
665
721
  Application.EnableVisualStyles();
package/native/Program.cs CHANGED
@@ -89,6 +89,7 @@ public static class Program
89
89
  // Codex ships the budget with every call (x-oai-cua-request-budget-ms) and the
90
90
  // helper honours it; here it bounds capture waits and marks overruns.
91
91
  Budget.Begin(Json.GetLong(p, "__budgetMs", 0));
92
+ try { if (_driver != null) _driver.Maintenance(); } catch { }
92
93
  object result = Dispatch(method, p);
93
94
  return Ok(id, result);
94
95
  }
@@ -109,6 +110,7 @@ public static class Program
109
110
  {
110
111
  case "ping": return _driver.Ping(p);
111
112
  case "hello": return _driver.Hello(p);
113
+ case "restore_cursor": return _driver.RestoreCursor(p);
112
114
  case "close": return _driver.Close(p);
113
115
  case "list_windows": return _driver.ListWindows(p);
114
116
  case "list_apps": return _driver.ListApps(p);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepseek-computer-use",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Windows Computer Use for DeepSeek Harness: UIA observation, SendInput actions, Windows.Graphics.Capture — modeled on Codex's window2 API. Community plugin, not an official DeepSeek package.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",