dsh-click 0.3.23 → 0.3.24
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/CHANGELOG.md +6 -0
- package/README.md +1 -0
- package/lib/index.js +1 -1
- package/lib/types/version.d.ts +1 -1
- package/native/win32/dsh-click-helper.ps1 +145 -69
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.24] - 2026-10-19
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **Real keyboard input and foreground focus for the Windows helper (PR #21).** `type` now uses `SendInput` with `KEYEVENTF_UNICODE` (CJK-safe) instead of relying only on posted `WM_CHAR`, and element/key actions can bring the target window to the foreground via `AttachThreadInput` + `SetForegroundWindow` + `SetFocus`. The UIA `ValuePattern` fast path remains. Credit: @LIU12312-Source.
|
|
8
|
+
|
|
3
9
|
## [0.3.23] - 2026-10-19
|
|
4
10
|
|
|
5
11
|
### Fixed
|
package/README.md
CHANGED
|
@@ -208,6 +208,7 @@ pnpm pack # the published tarball
|
|
|
208
208
|
|
|
209
209
|
## Contributors
|
|
210
210
|
|
|
211
|
+
- [@LIU12312-Source](https://github.com/LIU12312-Source) — real `SendInput`/foreground-focus fallback for the Windows helper (PR #21).
|
|
211
212
|
- [@PerryLink](https://github.com/PerryLink) — creator and maintainer: tool surface, action safety boundary, Windows native helper, sanitizers, and the five-language docs.
|
|
212
213
|
- [@Mchsd](https://github.com/Mchsd) — added the `auditSessionEvents` opt-out for harnesses whose session reader rejects the `dsh-click` audit events (#2).
|
|
213
214
|
|
package/lib/index.js
CHANGED
|
@@ -586,7 +586,7 @@ var ObservationStore = class {
|
|
|
586
586
|
* @module dsh-click/version
|
|
587
587
|
*/
|
|
588
588
|
/** Plugin version advertised in the helper-protocol handshake. */
|
|
589
|
-
const VERSION = "0.3.
|
|
589
|
+
const VERSION = "0.3.24";
|
|
590
590
|
/** The helper protocol revision; bump only on incompatible wire changes. */
|
|
591
591
|
const HELPER_PROTOCOL_VERSION = 1;
|
|
592
592
|
//#endregion
|
package/lib/types/version.d.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* @module dsh-click/version
|
|
8
8
|
*/
|
|
9
9
|
/** Plugin version advertised in the helper-protocol handshake. */
|
|
10
|
-
export declare const VERSION = "0.3.
|
|
10
|
+
export declare const VERSION = "0.3.24";
|
|
11
11
|
/** The helper protocol revision; bump only on incompatible wire changes. */
|
|
12
12
|
export declare const HELPER_PROTOCOL_VERSION = 1;
|
|
13
13
|
//# sourceMappingURL=version.d.ts.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# dsh-click native helper (Windows PowerShell 5.1).
|
|
1
|
+
# dsh-click native helper (Windows PowerShell 5.1).
|
|
2
2
|
#
|
|
3
3
|
# One request = one process: JSON request on stdin, JSON response on stdout
|
|
4
4
|
# (exit code 0 even for handled failures; the error envelope is the response).
|
|
@@ -57,6 +57,87 @@ public static class DshClickWin32 {
|
|
|
57
57
|
[DllImport("user32.dll")]
|
|
58
58
|
public static extern bool SetForegroundWindow(IntPtr hWnd);
|
|
59
59
|
|
|
60
|
+
// ---- dsh-click input patch: real keyboard injection + reliable focus ----
|
|
61
|
+
[DllImport("user32.dll")]
|
|
62
|
+
public static extern bool AttachThreadInput(uint idAttach, uint idAttachTo, bool fAttach);
|
|
63
|
+
|
|
64
|
+
[DllImport("user32.dll")]
|
|
65
|
+
public static extern IntPtr SetFocus(IntPtr hWnd);
|
|
66
|
+
|
|
67
|
+
[DllImport("user32.dll")]
|
|
68
|
+
public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
|
|
69
|
+
|
|
70
|
+
[DllImport("user32.dll")]
|
|
71
|
+
public static extern bool IsIconic(IntPtr hWnd);
|
|
72
|
+
|
|
73
|
+
[DllImport("kernel32.dll")]
|
|
74
|
+
public static extern uint GetCurrentThreadId();
|
|
75
|
+
|
|
76
|
+
[DllImport("user32.dll")]
|
|
77
|
+
public static extern short VkKeyScanW(char ch);
|
|
78
|
+
|
|
79
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
80
|
+
public struct KEYBDINPUT {
|
|
81
|
+
public ushort wVk;
|
|
82
|
+
public ushort wScan;
|
|
83
|
+
public uint dwFlags;
|
|
84
|
+
public uint time;
|
|
85
|
+
public IntPtr dwExtraInfo;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
[StructLayout(LayoutKind.Explicit, Size = 40)]
|
|
89
|
+
public struct INPUT {
|
|
90
|
+
[FieldOffset(0)] public uint type;
|
|
91
|
+
[FieldOffset(8)] public KEYBDINPUT ki;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
[DllImport("user32.dll", SetLastError = true)]
|
|
95
|
+
public static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize);
|
|
96
|
+
|
|
97
|
+
public static uint SendUnicodeText(string text) {
|
|
98
|
+
if (string.IsNullOrEmpty(text)) return 0;
|
|
99
|
+
var list = new List<INPUT>();
|
|
100
|
+
foreach (char c in text) {
|
|
101
|
+
INPUT down = new INPUT();
|
|
102
|
+
down.type = 1;
|
|
103
|
+
down.ki.wScan = (ushort)c;
|
|
104
|
+
down.ki.dwFlags = 0x0004;
|
|
105
|
+
list.Add(down);
|
|
106
|
+
INPUT up = new INPUT();
|
|
107
|
+
up.type = 1;
|
|
108
|
+
up.ki.wScan = (ushort)c;
|
|
109
|
+
up.ki.dwFlags = 0x0004 | 0x0002;
|
|
110
|
+
list.Add(up);
|
|
111
|
+
}
|
|
112
|
+
return SendInput((uint)list.Count, list.ToArray(), Marshal.SizeOf(typeof(INPUT)));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
public static uint SendVirtualKey(ushort vk, bool up) {
|
|
116
|
+
INPUT inp = new INPUT();
|
|
117
|
+
inp.type = 1;
|
|
118
|
+
inp.ki.wVk = vk;
|
|
119
|
+
inp.ki.dwFlags = up ? 0x0002u : 0u;
|
|
120
|
+
return SendInput(1, new INPUT[] { inp }, Marshal.SizeOf(typeof(INPUT)));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
public static bool BringToFront(IntPtr hWnd) {
|
|
124
|
+
if (IsIconic(hWnd)) ShowWindow(hWnd, 9);
|
|
125
|
+
if (GetForegroundWindow() == hWnd) return true;
|
|
126
|
+
IntPtr fg = GetForegroundWindow();
|
|
127
|
+
uint ignored;
|
|
128
|
+
uint fgThread = GetWindowThreadProcessId(fg, out ignored);
|
|
129
|
+
uint targetThread = GetWindowThreadProcessId(hWnd, out ignored);
|
|
130
|
+
uint myThread = GetCurrentThreadId();
|
|
131
|
+
bool a1 = AttachThreadInput(myThread, fgThread, true);
|
|
132
|
+
bool a2 = AttachThreadInput(myThread, targetThread, true);
|
|
133
|
+
SetForegroundWindow(hWnd);
|
|
134
|
+
SetFocus(hWnd);
|
|
135
|
+
if (a2) AttachThreadInput(myThread, targetThread, false);
|
|
136
|
+
if (a1) AttachThreadInput(myThread, fgThread, false);
|
|
137
|
+
System.Threading.Thread.Sleep(40);
|
|
138
|
+
return GetForegroundWindow() == hWnd;
|
|
139
|
+
}
|
|
140
|
+
|
|
60
141
|
[StructLayout(LayoutKind.Sequential)]
|
|
61
142
|
public struct POINT { public int X; public int Y; }
|
|
62
143
|
|
|
@@ -396,7 +477,7 @@ function Find-ElementByRuntimeId($windowElement, $runtimeIdText) {
|
|
|
396
477
|
$ids = @()
|
|
397
478
|
foreach ($part in ($runtimeIdText -split '\.')) { $ids += [int]$part }
|
|
398
479
|
if ($ids.Count -eq 0) { return $null }
|
|
399
|
-
$condition =
|
|
480
|
+
$condition = New-Object System.Windows.Automation.PropertyCondition([System.Windows.Automation.AutomationElement]::RuntimeIdProperty, [int[]]$ids)
|
|
400
481
|
try {
|
|
401
482
|
return $windowElement.FindFirst([System.Windows.Automation.TreeScope]::Descendants, $condition)
|
|
402
483
|
} catch {
|
|
@@ -449,45 +530,30 @@ function Post-Click($hwnd, $cx, $cy, $button) {
|
|
|
449
530
|
[void][DshClickWin32]::PostMessage($hwnd, $up, [IntPtr]0, $lparam)
|
|
450
531
|
}
|
|
451
532
|
|
|
452
|
-
function Get-
|
|
453
|
-
$
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
$
|
|
466
|
-
$
|
|
467
|
-
$
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
$map['RIGHT'] = @(0x27, $true)
|
|
474
|
-
$map['CTRL'] = @(0x11, $false)
|
|
475
|
-
$map['CONTROL'] = @(0x11, $false)
|
|
476
|
-
$map['SHIFT'] = @(0x10, $false)
|
|
477
|
-
$map['ALT'] = @(0x12, $false)
|
|
478
|
-
$map['MENU'] = @(0x12, $false)
|
|
479
|
-
$map['WIN'] = @(0x5B, $false)
|
|
480
|
-
$map['WINDOWS'] = @(0x5B, $false)
|
|
481
|
-
foreach ($n in 1..12) { $map["F$n"] = @((0x6F + $n), $true) }
|
|
482
|
-
foreach ($c in 0..9) { $map["$c"] = @((0x30 + $c), $true) }
|
|
483
|
-
foreach ($code in 65..90) {
|
|
484
|
-
$map[[string][char]$code] = @((0x41 + ($code - 65)), $true)
|
|
485
|
-
}
|
|
486
|
-
return $map
|
|
533
|
+
function Get-VirtualKey([string]$token) {
|
|
534
|
+
$names = @{
|
|
535
|
+
'ENTER' = 0x0D; 'RETURN' = 0x0D; 'TAB' = 0x09; 'ESC' = 0x1B; 'ESCAPE' = 0x1B;
|
|
536
|
+
'SPACE' = 0x20; 'BACKSPACE' = 0x08; 'DELETE' = 0x2E; 'DEL' = 0x2E; 'INSERT' = 0x2D;
|
|
537
|
+
'HOME' = 0x24; 'END' = 0x23; 'PAGEUP' = 0x21; 'PGUP' = 0x21; 'PAGEDOWN' = 0x22; 'PGDN' = 0x22;
|
|
538
|
+
'UP' = 0x26; 'DOWN' = 0x28; 'LEFT' = 0x25; 'RIGHT' = 0x27;
|
|
539
|
+
'CTRL' = 0x11; 'CONTROL' = 0x11; 'SHIFT' = 0x10; 'ALT' = 0x12; 'MENU' = 0x12;
|
|
540
|
+
'WIN' = 0x5B; 'WINDOWS' = 0x5B; 'LWIN' = 0x5B; 'RWIN' = 0x5C; 'APPS' = 0x5D;
|
|
541
|
+
'CAPSLOCK' = 0x14; 'NUMLOCK' = 0x90; 'SCROLLLOCK' = 0x91; 'PRINTSCREEN' = 0x2C;
|
|
542
|
+
'PAUSE' = 0x13; 'COMMA' = 0xBC; 'PERIOD' = 0xBE; 'SLASH' = 0xBF; 'SEMICOLON' = 0xBA;
|
|
543
|
+
'QUOTE' = 0xDE; 'LBRACKET' = 0xDB; 'RBRACKET' = 0xDD; 'BACKSLASH' = 0xDC;
|
|
544
|
+
'MINUS' = 0xBD; 'PLUS' = 0xBB; 'TILDE' = 0xC0
|
|
545
|
+
}
|
|
546
|
+
if ($names.ContainsKey($token)) { return [int]$names[$token] }
|
|
547
|
+
if ($token -match '^F([1-9]|1[0-2])$') { return 0x6F + [int]$Matches[1] }
|
|
548
|
+
if ($token.Length -eq 1) {
|
|
549
|
+
$scan = [DshClickWin32]::VkKeyScanW([char]$token)
|
|
550
|
+
if ($scan -ne -1) { return [int]($scan -band 0xFF) }
|
|
551
|
+
return [int][char]$token
|
|
552
|
+
}
|
|
553
|
+
throw "unknown key '$token'"
|
|
487
554
|
}
|
|
488
555
|
|
|
489
556
|
function Post-KeyCombo($hwnd, $keysText) {
|
|
490
|
-
$map = Get-KeyMap
|
|
491
557
|
$tokens = @()
|
|
492
558
|
foreach ($token in ($keysText -split '\+')) {
|
|
493
559
|
$trimmed = $token.Trim()
|
|
@@ -495,39 +561,37 @@ function Post-KeyCombo($hwnd, $keysText) {
|
|
|
495
561
|
$tokens += $trimmed.ToUpperInvariant()
|
|
496
562
|
}
|
|
497
563
|
if ($tokens.Count -eq 0) { throw 'empty key combination' }
|
|
564
|
+
$modifierNames = @('CTRL', 'CONTROL', 'SHIFT', 'ALT', 'MENU', 'WIN', 'WINDOWS', 'LWIN', 'RWIN')
|
|
498
565
|
$modifiers = @()
|
|
499
566
|
$mainKeys = @()
|
|
500
567
|
foreach ($token in $tokens) {
|
|
501
|
-
if ($
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
568
|
+
if ($modifierNames -contains $token) { $modifiers += $token } else { $mainKeys += $token }
|
|
569
|
+
}
|
|
570
|
+
$focused = $false
|
|
571
|
+
try { $focused = [DshClickWin32]::BringToFront($hwnd) } catch { $focused = $false }
|
|
572
|
+
if (-not $focused) {
|
|
573
|
+
# Last resort: posted messages (only works for windows that accept posted input).
|
|
574
|
+
foreach ($modifier in $modifiers) {
|
|
575
|
+
[void][DshClickWin32]::PostMessage($hwnd, 0x100, [IntPtr](Get-VirtualKey $modifier), [IntPtr]0)
|
|
509
576
|
}
|
|
577
|
+
foreach ($key in $mainKeys) {
|
|
578
|
+
$vk = Get-VirtualKey $key
|
|
579
|
+
[void][DshClickWin32]::PostMessage($hwnd, 0x100, [IntPtr]$vk, [IntPtr]0)
|
|
580
|
+
[void][DshClickWin32]::PostMessage($hwnd, 0x101, [IntPtr]$vk, [IntPtr]0)
|
|
581
|
+
}
|
|
582
|
+
for ($i = $modifiers.Count - 1; $i -ge 0; $i--) {
|
|
583
|
+
[void][DshClickWin32]::PostMessage($hwnd, 0x101, [IntPtr](Get-VirtualKey $modifiers[$i]), [IntPtr]0)
|
|
584
|
+
}
|
|
585
|
+
return
|
|
510
586
|
}
|
|
511
|
-
foreach ($modifier in $modifiers) {
|
|
512
|
-
$entry = $map[$modifier]
|
|
513
|
-
[void][DshClickWin32]::PostMessage($hwnd, 0x100, [IntPtr]$entry[0], [IntPtr]0)
|
|
514
|
-
}
|
|
587
|
+
foreach ($modifier in $modifiers) { [void][DshClickWin32]::SendVirtualKey([uint16](Get-VirtualKey $modifier), $false) }
|
|
515
588
|
foreach ($key in $mainKeys) {
|
|
516
|
-
$
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
[void][DshClickWin32]::PostMessage($hwnd, 0x100, [IntPtr]$entry[0], [IntPtr]0)
|
|
520
|
-
[void][DshClickWin32]::PostMessage($hwnd, 0x101, [IntPtr]$entry[0], [IntPtr]0)
|
|
521
|
-
} else {
|
|
522
|
-
$charCode = [int][char]$key
|
|
523
|
-
[void][DshClickWin32]::PostMessage($hwnd, 0x100, [IntPtr]$charCode, [IntPtr]0)
|
|
524
|
-
[void][DshClickWin32]::PostMessage($hwnd, 0x102, [IntPtr]$charCode, [IntPtr]0)
|
|
525
|
-
[void][DshClickWin32]::PostMessage($hwnd, 0x101, [IntPtr]$charCode, [IntPtr]0)
|
|
526
|
-
}
|
|
589
|
+
$vk = [uint16](Get-VirtualKey $key)
|
|
590
|
+
[void][DshClickWin32]::SendVirtualKey($vk, $false)
|
|
591
|
+
[void][DshClickWin32]::SendVirtualKey($vk, $true)
|
|
527
592
|
}
|
|
528
593
|
for ($i = $modifiers.Count - 1; $i -ge 0; $i--) {
|
|
529
|
-
|
|
530
|
-
[void][DshClickWin32]::PostMessage($hwnd, 0x101, [IntPtr]$entry[0], [IntPtr]0)
|
|
594
|
+
[void][DshClickWin32]::SendVirtualKey([uint16](Get-VirtualKey $modifiers[$i]), $true)
|
|
531
595
|
}
|
|
532
596
|
}
|
|
533
597
|
|
|
@@ -799,14 +863,26 @@ function Invoke-OpType($opArgs) {
|
|
|
799
863
|
throw "type failed and rollback $(if ($restored) { 'restored the original text' } else { 'could NOT restore the original text' }): $($_.Exception.Message)"
|
|
800
864
|
}
|
|
801
865
|
}
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
866
|
+
# dsh-click input patch: real keyboard input (SendInput) instead of posted WM_CHAR.
|
|
867
|
+
$center = Get-ElementCenter $element
|
|
868
|
+
$windowRect = Get-WindowRectInfo $hwnd
|
|
869
|
+
$client = Get-ClientPoint $hwnd $windowRect $center.x $center.y
|
|
870
|
+
Post-Click $hwnd $client.x $client.y 'left'
|
|
871
|
+
$focused = $false
|
|
872
|
+
try { $focused = [DshClickWin32]::BringToFront($hwnd) } catch { $focused = $false }
|
|
873
|
+
if ($focused) {
|
|
874
|
+
$sent = [DshClickWin32]::SendUnicodeText([string]$request.text)
|
|
875
|
+
if ($sent -gt 0) {
|
|
876
|
+
return (Get-ActionOutcome $hwnd 'type' 'posted' $null "typed $($sent / 2) chars via SendInput")
|
|
806
877
|
}
|
|
807
|
-
return (Get-ActionOutcome $hwnd 'type' 'posted' $null 'typed via posted WM_CHAR (no value pattern on the element)')
|
|
808
878
|
}
|
|
809
|
-
|
|
879
|
+
if (-not $focusFallback) {
|
|
880
|
+
throw "element '$($request.elementId)' exposes no value pattern and the window could not be focused - refusing to type into it"
|
|
881
|
+
}
|
|
882
|
+
foreach ($ch in [char[]][string]$request.text) {
|
|
883
|
+
[void][DshClickWin32]::PostMessage($hwnd, 0x102, [IntPtr][int]$ch, [IntPtr]0)
|
|
884
|
+
}
|
|
885
|
+
return (Get-ActionOutcome $hwnd 'type' 'posted' $null 'typed via posted WM_CHAR (SendInput unavailable)')
|
|
810
886
|
}
|
|
811
887
|
|
|
812
888
|
function Invoke-OpScroll($opArgs) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-click",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.24",
|
|
4
4
|
"description": "Cross-platform native desktop control tools for DeepSeek Harness (Windows first): screen_shot, screen_read (accessibility tree + pixel description for text-only models), click/type/scroll/key, and app_list/app_launch — every mutating action gated by approval, never stealing foreground focus, with stale-observation rejection and process-identity checks",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|