@momasiku/dsh-pilot 0.5.11 → 0.5.14

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
@@ -232,6 +232,18 @@ and what `dsh plugin add` checks before it installs anything. Requirements:
232
232
 
233
233
  ### Recent changes
234
234
 
235
+ **0.5.12** — a cold tree, and an empty list that explained nothing.
236
+
237
+ - **Chromium builds its accessibility tree lazily**, and the first caller pays for it:
238
+ measured on the harness window right after a restart, 3 elements at 1.2 s and 77
239
+ elements at 0.3 s on the next call. A caller that trusts the first answer concludes
240
+ the window has no tree and falls back to pixels for nothing. `controls` now retries
241
+ once, 300 ms later, when the worker has not yet seen a real tree and the answer is
242
+ suspiciously small.
243
+ - **A filtered listing that legitimately matches nothing** now says so, instead of
244
+ returning an empty list that reads as "this window has no tree":
245
+ `(no operable controls here; the window has 2 elements - pass meaningfulOnly:false to see them)`.
246
+
235
247
  **0.5.11** — the control tree, and two fixes that make a batch honest.
236
248
 
237
249
  - **`controls` reads the window's UI Automation tree**, and `target` clicks a listed
@@ -616,6 +628,11 @@ dsh plugin --profile <你的profile> add "file:E:\path\to\dsh-pilot"
616
628
 
617
629
  ### 最近改动
618
630
 
631
+ **0.5.12** —— 冷树,以及一个什么都没说的空列表。
632
+
633
+ - **Chromium 的无障碍树是懒加载的**,而且由第一个来问的人付账:实测重启后在 1.2 秒时只有 3 个元素,下一次调用 0.3 秒时是 77 个。信任第一个答案的人会认定「这窗口没有树」,白白退回像素。现在 `controls` 在「worker 还没见过完整树、且结果异常少」时会等 300 ms 重试一次。
634
+ - **过滤后确实什么都没匹配到时,现在会明说**,而不是返回一个读起来像「这里没有树」的空列表:`(no operable controls here; the window has 2 elements - pass meaningfulOnly:false to see them)`。
635
+
619
636
  **0.5.11** —— 控件树,外加两个让批量操作「不说谎」的修复。
620
637
 
621
638
  - **`controls` 读窗口的 UI Automation 树**,`target` 点列表里那个控件的精确矩形,于是有名字的控件不必再去像素里找。它不截图,所以也不付 settle。在 673 个元素的窗口上实测:过滤后 **0.65 秒、约 850 token**(77 个可操作元素),不过滤约 9k。暖 worker 记着它发出去的那份列表,所以某一行换了身份会报错,而不是点到搬进来的那个。
package/lib/index.js CHANGED
@@ -654,6 +654,11 @@ async function capture(ctx, exec, config, args) {
654
654
  ...(ref.name === undefined ? {} : { name: ref.name }),
655
655
  ...(ref.originalDimensions === undefined ? {} : { originalDimensions: { ...ref.originalDimensions } })
656
656
  };
657
+ // Predict where the request pipeline will put this frame, from the same budget
658
+ // it uses. A prediction, always paired with the deferral to the preview size the
659
+ // model is actually shown.
660
+ const projected = projectedRequestSize(Number(result.image.width), Number(result.image.height), limits);
661
+ if (projected !== undefined) result.requestPreview = projected;
657
662
  } catch (error) {
658
663
  result.imageError = `the frame could not be attached: ${error instanceof Error ? error.message : String(error)}`;
659
664
  }
@@ -736,25 +741,83 @@ function captureContent(value) {
736
741
  * @param value - a capture, or a tool result carrying one under `capture`.
737
742
  * @returns the contract text for that frame.
738
743
  */
744
+ /**
745
+ * Where the request pipeline will put this frame.
746
+ *
747
+ * The store hands back a normalized image, and then the request pipeline shrinks it
748
+ * again to fit the model's image budget - after this plugin has let go of it, which
749
+ * is why a frame that really was 1:1 at capture can still reach the model smaller.
750
+ * The geometry is a pure function of the budget the deployment publishes, so the
751
+ * pipeline can be predicted instead of described. Mirrors requestImageDimensions()
752
+ * in @deepseek-ai/dsh-attachment, and kept local so a drift between the two can only
753
+ * cost accuracy: the contract always defers to the preview size the message states.
754
+ *
755
+ * @param width - normalized image width.
756
+ * @param height - normalized image height.
757
+ * @param limits - the deployment's ImageAttachmentLimits.
758
+ * @returns predicted request dimensions plus the budget they came from, or undefined.
759
+ */
760
+ function projectedRequestSize(width, height, limits) {
761
+ const maxPixels = Number(limits?.maxImagePixels);
762
+ const maxDimension = Number(limits?.maxImageDimension);
763
+ if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) return undefined;
764
+ let w = width;
765
+ let h = height;
766
+ if (Number.isFinite(maxDimension) && maxDimension > 0 && Math.max(w, h) > maxDimension) {
767
+ if (w >= h) { h = Math.max(1, Math.round(maxDimension * h / w)); w = maxDimension; }
768
+ else { w = Math.max(1, Math.round(maxDimension * w / h)); h = maxDimension; }
769
+ }
770
+ if (Number.isFinite(maxPixels) && maxPixels > 0 && w * h > maxPixels) {
771
+ const s = Math.min(1, Math.sqrt(maxPixels / (w * h)));
772
+ if (w >= h) {
773
+ w = Math.max(1, Math.floor(w * s));
774
+ h = Math.max(1, Math.round(w * height / width));
775
+ while (w * h > maxPixels && w > 1) { w -= 1; h = Math.max(1, Math.round(w * height / width)); }
776
+ } else {
777
+ h = Math.max(1, Math.floor(h * s));
778
+ w = Math.max(1, Math.round(h * width / height));
779
+ while (w * h > maxPixels && h > 1) { h -= 1; w = Math.max(1, Math.round(h * width / height)); }
780
+ }
781
+ }
782
+ const out = { width: w, height: h };
783
+ if (Number.isFinite(maxPixels) && maxPixels > 0) out.maxPixels = maxPixels;
784
+ return out;
785
+ }
739
786
  function coordinateContract(value) {
740
787
  const capture = value?.capture ?? value;
741
- const scale = deliveredScale(capture);
742
- if (scaleIsOne(scale)) {
788
+ const width = Number(capture?.imageWidth);
789
+ const height = Number(capture?.imageHeight);
790
+ const sizeKnown = Number.isFinite(width) && width > 0 && Number.isFinite(height) && height > 0;
791
+ const frame = sizeKnown ? `${width}x${height}` : 'the captured frame';
792
+ const delivered = capture?.image;
793
+ const preview = capture?.requestPreview;
794
+ const predicted = Number.isFinite(Number(preview?.width)) && Number(preview.width) > 0 && Number.isFinite(Number(preview?.height)) && Number(preview.height) > 0
795
+ ? { width: Number(preview.width), height: Number(preview.height), maxPixels: Number(preview.maxPixels) }
796
+ : undefined;
797
+ const final = predicted ?? (delivered === undefined || delivered === null ? undefined : { width: Number(delivered.width), height: Number(delivered.height) });
798
+ const kx = final !== undefined && sizeKnown && final.width > 0 ? width / final.width : 1;
799
+ const ky = final !== undefined && sizeKnown && final.height > 0 ? height / final.height : 1;
800
+ const oneToOne = Math.abs(kx - 1) < 0.002 && Math.abs(ky - 1) < 0.002;
801
+ const budgetLine = predicted?.maxPixels > 0 ? `This deployment's image budget is ${predicted.maxPixels} px. ` : '';
802
+ const guard = 'The message carrying this frame states its own preview size; if it differs from the size named here, multiply every reading by capture_width / preview_width before passing it to desktop_control, because a step after capture decides it.';
803
+ if (oneToOne) {
743
804
  return [
744
- 'Image pixel (0,0) is the top-left of this frame. Both halves of this device are per-monitor DPI aware and this frame is delivered at capture size, so one image pixel is one real screen pixel:',
745
- 'to act on a feature at image (x,y), call desktop_control with exactly x and y. Do not rescale for display scaling.'
746
- ].join(' ');
747
- }
748
- if (axesAgree(scale)) {
749
- const ratio = (scale.x + scale.y) / 2;
750
- return [
751
- `This frame was delivered at ${printed(ratio)}x the capture, so image pixels are NOT screen pixels:`,
752
- `to act on a feature at image (x,y), multiply both readings by ${printed(1 / ratio)} and pass those physical screen pixels to desktop_control.`
805
+ `Image pixel (0,0) is the top-left of this frame, which was captured at ${frame} physical screen pixels. Both halves of this device are per-monitor DPI aware.`,
806
+ `${budgetLine}At that size one image pixel is one real screen pixel: to act on a feature at image (x,y), call desktop_control with exactly x and y.`,
807
+ guard
753
808
  ].join(' ');
754
809
  }
810
+ if (!sizeKnown || final === undefined) return guard;
811
+ const axes = Math.abs(kx - ky) <= 0.005 * Math.max(kx, ky);
812
+ const multiply = axes
813
+ ? `multiply every reading by ${printed(kx)}`
814
+ : `multiply x readings by ${printed(kx)} and y readings by ${printed(ky)}`;
815
+ const shown = predicted === undefined ? 'delivered' : 'the request pipeline will show it to you';
755
816
  return [
756
- `This frame was delivered at ${printed(scale.x)}x horizontally and ${printed(scale.y)}x vertically of the capture, so image pixels are NOT screen pixels:`,
757
- `to act on a feature at image (x,y), multiply the readings by ${printed(1 / scale.x)} and ${printed(1 / scale.y)} (x*${printed(1 / scale.x)}, y*${printed(1 / scale.y)}) and pass those physical screen pixels to desktop_control.`
817
+ `This frame was captured at ${frame} physical screen pixels and ${shown} at ${final.width}x${final.height} (${printed(final.width / width)}x the capture), so image pixels are NOT screen pixels.`,
818
+ `${budgetLine}To act on a feature at image (x,y), ${multiply} and pass those physical screen pixels to desktop_control, or read the ruler labels, which are already screen coordinates.`,
819
+ 'A capture whose area is at or under the budget arrives at capture size: framing the window or a region instead of the whole screen gives a true 1:1 frame.',
820
+ guard
758
821
  ].join(' ');
759
822
  }
760
823
 
@@ -941,6 +1004,17 @@ function windowInfoOf(window) {
941
1004
  };
942
1005
  }
943
1006
 
1007
+ /** Where the request pipeline is predicted to put a frame, and the budget used. */
1008
+ const REQUEST_PREVIEW_SCHEMA = {
1009
+ type: 'object',
1010
+ additionalProperties: false,
1011
+ properties: {
1012
+ width: { type: 'integer', required: true },
1013
+ height: { type: 'integer', required: true },
1014
+ maxPixels: { type: 'number' }
1015
+ }
1016
+ };
1017
+
944
1018
  const CAPTURE_SCHEMA = {
945
1019
  type: 'object',
946
1020
  additionalProperties: false,
@@ -969,6 +1043,7 @@ const CAPTURE_SCHEMA = {
969
1043
  frameHash: { type: 'string' },
970
1044
  unchanged: { type: 'boolean' },
971
1045
  rulers: { type: 'boolean' },
1046
+ requestPreview: REQUEST_PREVIEW_SCHEMA,
972
1047
  capturedAt: { type: 'string' }
973
1048
  }
974
1049
  };
@@ -24,6 +24,12 @@ $script:ActionStopPayload = $null
24
24
  # point at a different control; this is what lets a click notice that.
25
25
  $script:UiListing = $null
26
26
 
27
+ # Chromium builds its accessibility tree lazily: the first client to ask gets a
28
+ # nearly empty answer while the tree is still being constructed. Measured on the
29
+ # harness window right after a restart: 3 elements, then 77 a moment later. One
30
+ # retry per worker turns that into a correct first answer.
31
+ $script:UiWarmed = $false
32
+
27
33
  function Write-ActionError([string] $message) {
28
34
  $script:ActionStopPayload = [ordered]@{ ok = $false; error = $message }
29
35
  throw [System.Exception]::new($message)
@@ -394,11 +400,26 @@ switch ($action) {
394
400
  $moRaw = Get-Param 'meaningfulOnly'
395
401
  if ($null -ne $moRaw) { $meaningfulOnly = [bool] $moRaw }
396
402
  $tree = Get-DshUiaNodes -Handle $treeHandle -IncludeValues $includeValues -MeaningfulOnly $meaningfulOnly
403
+ if (-not $script:UiWarmed -and $tree.total -lt 8) {
404
+ # A window that really has fewer than eight elements pays one extra read
405
+ # once per worker; a cold Electron window is the case this exists for.
406
+ Start-Sleep -Milliseconds 300
407
+ $tree = Get-DshUiaNodes -Handle $treeHandle -IncludeValues $includeValues -MeaningfulOnly $meaningfulOnly
408
+ }
409
+ if ($tree.total -ge 8) { $script:UiWarmed = $true }
397
410
  if (-not $tree.ok) { Write-ActionError $tree.error }
398
411
  $items = @{}
399
412
  foreach ($n in $tree.nodes) { $items[[int] $n.index] = @{ role = [string] $n.role; name = [string] $n.name } }
400
413
  $script:UiListing = @{ handle = [long] $treeHandle; items = $items }
401
414
  $lines = Format-DshUiaNodes -Nodes $tree.nodes -Truncated $tree.truncated
415
+ if ($meaningfulOnly -and $tree.nodes.Count -eq 0) {
416
+ # An empty list would read as 'this window has no tree', which is the one
417
+ # wrong conclusion to leave a caller with: say what is actually there.
418
+ $full = Get-DshUiaNodes -Handle $treeHandle -MeaningfulOnly $false
419
+ if ($full.nodes.Count -gt 0) {
420
+ $lines = @(('(no operable controls here; the window has {0} elements - pass meaningfulOnly:false to see them)' -f $full.nodes.Count))
421
+ }
422
+ }
402
423
  $treeCursor = [DshInput]::Cursor()
403
424
  $result = [ordered]@{
404
425
  ok = $true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@momasiku/dsh-pilot",
3
- "version": "0.5.11",
3
+ "version": "0.5.14",
4
4
  "description": "Desktop vision and control for DeepSeek Harness - the whole Windows machine, any application, not browser automation: screen_view captures the live screen as an image the model can see, desktop_control drives the mouse, keyboard, and windows against the exact same pixel space; desktop_sequence runs a whole batch of actions in one call and looks once at the end.",
5
5
  "license": "MIT",
6
6
  "author": "momasiku",