dsh-codex-style-computer-use 1.2.3 → 1.3.0
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 +5 -1
- package/README.zh.md +8 -2
- package/lib/batch.js +21 -5
- package/lib/continuation.js +14 -0
- package/lib/core/bin/dsh-computer-use-worker.exe +0 -0
- package/lib/core/bin/dsh-computer-use-worker.exe.json +2 -2
- package/lib/core/index.js +72 -17
- package/lib/core/worker.cs +495 -246
- package/lib/cycle-lifecycle.js +66 -0
- package/lib/cycle.js +99 -9
- package/lib/index.js +51 -618
- package/lib/policy.js +1 -0
- package/lib/skill-gate.js +1 -1
- package/lib/terminal-cleanup.js +57 -0
- package/lib/tools.js +18 -4
- package/mcp/src/execution.js +22 -0
- package/mcp/src/index.js +36 -13
- package/package.json +1 -1
- package/skills/computer-use/SKILL.md +8 -3
- package/skills/computer-use/references/lifecycle.md +23 -12
package/lib/core/worker.cs
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// Safety model: the worker is a faithful actuator. Policy (approvals,
|
|
14
14
|
// blacklists, masking) lives in the DSH plugin / MCP gate layers. Worker-side
|
|
15
15
|
// guards: exact physical Ctrl+Esc for manual pause, event-driven human ownership,
|
|
16
|
-
// and accepted-input cleanup. Physical
|
|
16
|
+
// and accepted-input cleanup. Physical takeover during actuation yields until three seconds quiet.
|
|
17
17
|
|
|
18
18
|
using System;
|
|
19
19
|
using System.Collections.Generic;
|
|
@@ -32,15 +32,14 @@ using System.Windows.Forms; // Clipboard (watch out: bare Point stays our own st
|
|
|
32
32
|
|
|
33
33
|
namespace DshComputerUse.Worker
|
|
34
34
|
{
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
// and no physical event returns control. Ordinary input never creates a STOP file.
|
|
35
|
+
// A live cycle observes physical holds, but only actual actuation can start a handoff.
|
|
36
|
+
// Once yielded, three quiet seconds with no held input returns control to the agent.
|
|
38
37
|
public sealed class HumanInputState
|
|
39
38
|
{
|
|
40
39
|
public const int SustainedMs = 2000, IdleMs = 3000;
|
|
41
40
|
readonly object sync = new object();
|
|
42
41
|
readonly HashSet<int> held = new HashSet<int>();
|
|
43
|
-
bool enabled, seen;
|
|
42
|
+
bool enabled, acting, seen;
|
|
44
43
|
string phase = "idle", lastKind = "none";
|
|
45
44
|
long first, last, revision, burst;
|
|
46
45
|
public string Phase { get { lock (sync) { return phase; } } }
|
|
@@ -67,8 +66,18 @@ namespace DshComputerUse.Worker
|
|
|
67
66
|
{
|
|
68
67
|
if (enabled == on) return;
|
|
69
68
|
enabled = on;
|
|
70
|
-
if (!on) { phase = "idle";
|
|
71
|
-
|
|
69
|
+
if (!on) { phase = "idle"; acting = false; }
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
public void SetActing(bool on, long now)
|
|
73
|
+
{
|
|
74
|
+
lock (sync)
|
|
75
|
+
{
|
|
76
|
+
acting = on;
|
|
77
|
+
// Do not replay earlier ordinary activity. A key/button still physically
|
|
78
|
+
// held does need ownership before we can inject a conflicting chord.
|
|
79
|
+
if (on && enabled && phase == "idle" && held.Count > 0)
|
|
80
|
+
{ last = now; revision++; Start(now); }
|
|
72
81
|
}
|
|
73
82
|
}
|
|
74
83
|
void Start(long now) { phase = "yielding"; first = now; if (!seen) last = now; burst++; }
|
|
@@ -79,8 +88,9 @@ namespace DshComputerUse.Worker
|
|
|
79
88
|
bool continued = held.Count > 0 || (seen && now - last < IdleMs);
|
|
80
89
|
lastKind = key == 0 ? "pointer-or-wheel" : key <= 6 ? "button" : "keyboard";
|
|
81
90
|
if (key != 0) { if (down) held.Add(key); if (up) held.Remove(key); }
|
|
82
|
-
seen = true; last = now;
|
|
83
|
-
if (!enabled) return;
|
|
91
|
+
seen = true; last = now;
|
|
92
|
+
if (!enabled || (phase == "idle" && !acting)) return;
|
|
93
|
+
revision++;
|
|
84
94
|
if (phase == "idle" || !continued) Start(now);
|
|
85
95
|
if (now - first >= SustainedMs) phase = "waiting";
|
|
86
96
|
}
|
|
@@ -103,6 +113,35 @@ namespace DshComputerUse.Worker
|
|
|
103
113
|
}
|
|
104
114
|
}
|
|
105
115
|
|
|
116
|
+
// Sum only physical displacement, rebasing every injected cursor event. Agent
|
|
117
|
+
// motion cannot turn a 1-pixel hand tremor into a takeover. Back-and-forth jitter
|
|
118
|
+
// cancels rather than accumulating path length; a deliberate 16-pixel move yields.
|
|
119
|
+
public sealed class PointerHandoffTolerance
|
|
120
|
+
{
|
|
121
|
+
public const int DistancePx = 16, BurstGapMs = 250;
|
|
122
|
+
readonly object sync = new object();
|
|
123
|
+
bool seen;
|
|
124
|
+
int x, y;
|
|
125
|
+
long dx, dy, lastPhysical;
|
|
126
|
+
public void Reset(int px, int py)
|
|
127
|
+
{ lock (sync) { seen = true; x = px; y = py; dx = dy = 0; lastPhysical = 0; } }
|
|
128
|
+
public bool Record(int px, int py, bool physical, bool acting, bool yielded, long now)
|
|
129
|
+
{
|
|
130
|
+
lock (sync)
|
|
131
|
+
{
|
|
132
|
+
long mx = seen ? (long)px - x : 0, my = seen ? (long)py - y : 0;
|
|
133
|
+
x = px; y = py; seen = true;
|
|
134
|
+
if (!physical) return false;
|
|
135
|
+
if (!acting && !yielded) { dx = dy = 0; lastPhysical = now; return false; }
|
|
136
|
+
if (mx == 0 && my == 0) return false;
|
|
137
|
+
if (yielded) return true;
|
|
138
|
+
if (now - lastPhysical > BurstGapMs) dx = dy = 0;
|
|
139
|
+
lastPhysical = now; dx += mx; dy += my;
|
|
140
|
+
return dx * dx + dy * dy >= DistancePx * DistancePx;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
106
145
|
// A temporary pause is an explicit, single-use claim. No clock grants permission.
|
|
107
146
|
public sealed class RecoveryPermit
|
|
108
147
|
{
|
|
@@ -474,40 +513,45 @@ namespace DshComputerUse.Worker
|
|
|
474
513
|
[DllImport("user32.dll")] static extern bool UpdateLayeredWindow(IntPtr h, IntPtr dst, ref POINTL pptDst, ref SIZEL psize,
|
|
475
514
|
IntPtr src, ref POINTL pptSrc, int colorKey, ref BLENDFUNCTION blend, int flags);
|
|
476
515
|
[DllImport("user32.dll")] static extern int GetSystemMetrics(int i);
|
|
516
|
+
[DllImport("user32.dll")] static extern bool SetWindowDisplayAffinity(IntPtr hwnd, uint affinity);
|
|
477
517
|
|
|
478
518
|
static Thread _thread;
|
|
479
|
-
static volatile
|
|
480
|
-
static volatile
|
|
519
|
+
static volatile bool _acting;
|
|
520
|
+
static volatile bool _preparing;
|
|
521
|
+
static volatile int _preparationStart;
|
|
522
|
+
static int _preparationSerial, _presentedPreparation;
|
|
523
|
+
public const int PreparationMs = 1500;
|
|
481
524
|
static volatile bool _enabled = true;
|
|
482
525
|
static volatile string _label = "AI 正在操控此电脑 · 监听中";
|
|
483
526
|
|
|
484
|
-
///
|
|
527
|
+
/// Legacy indicator requests announce monitoring; only real input can paint deep blue.
|
|
485
528
|
public static void Touch(int holdMs)
|
|
486
529
|
{
|
|
487
530
|
if (!_enabled) return;
|
|
488
|
-
|
|
489
|
-
_lastTouchMs = Environment.TickCount;
|
|
531
|
+
_thinking = true;
|
|
490
532
|
EnsureStarted();
|
|
491
533
|
}
|
|
492
|
-
public static void Off() {
|
|
534
|
+
public static void Off() { _acting = false; _preparing = false; _thinking = false; }
|
|
535
|
+
public static bool Acting { get { return _acting; } }
|
|
536
|
+
public static bool Preparing { get { return _preparing; } }
|
|
537
|
+
public static bool PreparationVisible { get { return _preparing && Volatile.Read(ref _presentedPreparation) == Volatile.Read(ref _preparationSerial); } }
|
|
538
|
+
public static void ActuationBegin() { _acting = true; EnsureStarted(); }
|
|
539
|
+
public static void ActuationEnd() { _acting = false; }
|
|
540
|
+
public static void Prepare(bool on)
|
|
541
|
+
{
|
|
542
|
+
if (!on) { _preparing = false; return; }
|
|
543
|
+
_preparationStart = 0;
|
|
544
|
+
Interlocked.Increment(ref _preparationSerial);
|
|
545
|
+
_preparing = true; _enabled = true; EnsureStarted();
|
|
546
|
+
}
|
|
493
547
|
|
|
494
|
-
//
|
|
495
|
-
//
|
|
496
|
-
// deep-blue ACTING border had already expired at its fixed 1200 ms hold WHILE SendInput steps
|
|
497
|
-
// were still being injected — pixels went blue at 1737 ms, then cyan/green at 2153/3438 ms with
|
|
498
|
-
// moveStillPending:true on all eight frames. Nothing here lengthens the hold: the hold is
|
|
499
|
-
// still 1200 ms. What changes is WHERE IT IS ANCHORED (the last real injection, not the moment
|
|
500
|
-
// the op began) and that an injection SEQUENCE in flight is a state the light can read.
|
|
548
|
+
// Nested injection sequences stay observable across drag/key/typing gaps.
|
|
549
|
+
// The enclosing actual action scope controls ownership and the deep-blue cue.
|
|
501
550
|
static int _injecting = 0; // refcount; Interlocked supplies the barriers
|
|
502
551
|
public static void InputBegin() { Interlocked.Increment(ref _injecting); }
|
|
503
552
|
public static void InputEnd() { if (Interlocked.Decrement(ref _injecting) < 0) _injecting = 0; }
|
|
504
553
|
public static bool Injecting { get { return _injecting > 0; } }
|
|
505
|
-
|
|
506
|
-
/// SendInput goes through, so a multi-step op (type/drag/scroll/key/animated move) stays blue
|
|
507
|
-
/// for as long as its events keep coming, and still fades on the same short tail once they
|
|
508
|
-
/// stop. The old behaviour anchored on the op's START, so any op longer than the hold went
|
|
509
|
-
/// cyan mid-flight — the "not a long think, the input is still running" report.
|
|
510
|
-
public static void KeepActing() { _lastTouchMs = Environment.TickCount; }
|
|
554
|
+
public static void KeepActing() { /* Actual operation scope owns the cue, with no trailing hold. */ }
|
|
511
555
|
|
|
512
556
|
// Emergency stop must LOOK like one. The normal fade is a slow ease (~1.6 s) which,
|
|
513
557
|
// during a panic, reads as "nothing happened" — so this snaps the overlay to zero on
|
|
@@ -515,7 +559,7 @@ namespace DshComputerUse.Worker
|
|
|
515
559
|
static volatile bool _killNow = false;
|
|
516
560
|
public static void Kill()
|
|
517
561
|
{
|
|
518
|
-
|
|
562
|
+
_acting = false; _preparing = false; _thinking = false; _flashStart = 0;
|
|
519
563
|
_killNow = true;
|
|
520
564
|
}
|
|
521
565
|
|
|
@@ -567,7 +611,7 @@ namespace DshComputerUse.Worker
|
|
|
567
611
|
public static int DeriveWant(bool flashOn, int committed)
|
|
568
612
|
{
|
|
569
613
|
if (Panic.Exited) return 0;
|
|
570
|
-
return _askMode ? 5 : (_stoppedMode ? 4 : (Panic.HumanActive ? 6 : (flashOn ? 2 : committed)));
|
|
614
|
+
return _askMode ? 5 : (_stoppedMode ? 4 : (Panic.HumanActive ? 6 : (_preparing ? 7 : (Acting ? 1 : (flashOn ? 2 : committed)))));
|
|
571
615
|
}
|
|
572
616
|
/// The title states the same fact as the light, derived the same way (an ended session must
|
|
573
617
|
/// not advertise "已暂停 · Ctrl+Alt+R 继续" in the window list).
|
|
@@ -575,7 +619,7 @@ namespace DshComputerUse.Worker
|
|
|
575
619
|
{
|
|
576
620
|
if (Panic.Exited) return "AI 已停止操控 · 未监听";
|
|
577
621
|
return _askMode ? "AI 正在提问 · 在 DSH 聊天里回答 · 无倒计时(Ctrl+Alt+R 撤销)"
|
|
578
|
-
: (_stoppedMode ? "已暂停 · Ctrl+Alt+R 继续 · Ctrl+Alt+Q 退出" : Panic.HumanActive ? "已让出键鼠 · 等待人操作 · 静默 3 秒后继续" : label);
|
|
622
|
+
: (_stoppedMode ? "已暂停 · Ctrl+Alt+R 继续 · Ctrl+Alt+Q 退出" : Panic.HumanActive ? "已让出键鼠 · 等待人操作 · 静默 3 秒后继续" : _preparing ? "即将开始操作,请暂停键鼠 · 1.5 秒准备" : label);
|
|
579
623
|
}
|
|
580
624
|
static volatile string _shownTitle = null;
|
|
581
625
|
// What the indicator is ACTUALLY rendering right now, readable from outside via `probe`.
|
|
@@ -678,6 +722,7 @@ namespace DshComputerUse.Worker
|
|
|
678
722
|
WS_EX_LAYERED | WS_EX_TRANSPARENT | WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE | WS_EX_TOPMOST,
|
|
679
723
|
"STATIC", _label, WS_POPUP, vx, vy, vw, vh, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero);
|
|
680
724
|
if (hwnd == IntPtr.Zero) return;
|
|
725
|
+
TryExcludeOverlayFromCapture(hwnd);
|
|
681
726
|
_hwnd = hwnd;
|
|
682
727
|
|
|
683
728
|
IntPtr screenDc = GetDC(IntPtr.Zero);
|
|
@@ -703,37 +748,16 @@ namespace DshComputerUse.Worker
|
|
|
703
748
|
int paintedFlash = 0;
|
|
704
749
|
int alpha = 0;
|
|
705
750
|
bool visible = false;
|
|
706
|
-
int committed = 0;
|
|
707
|
-
int pending = -1;
|
|
708
|
-
int pendingSince = 0;
|
|
751
|
+
int committed = 0;
|
|
709
752
|
while (true)
|
|
710
753
|
{
|
|
711
|
-
//
|
|
712
|
-
//
|
|
713
|
-
bool acting = _enabled &&
|
|
714
|
-
(_lastTouchMs != 0 && unchecked(Environment.TickCount - _lastTouchMs) < _holdMs));
|
|
754
|
+
// Ownership and deep blue use the same actual action scope. There is no
|
|
755
|
+
// delayed onset or lingering tail during reading, thinking or preparation.
|
|
756
|
+
bool acting = _enabled && Acting;
|
|
715
757
|
bool thinking = ThinkingActive;
|
|
716
758
|
bool flashOn = _flashStart != 0 && unchecked(Environment.TickCount - _flashStart) < _flashMs;
|
|
717
759
|
|
|
718
|
-
|
|
719
|
-
// The flash is an OVERRIDE, never a state that lingers.
|
|
720
|
-
//
|
|
721
|
-
// 2026-09-13: the flash used to write `committed = 2`, so once it ended the dwell
|
|
722
|
-
// hysteresis (600 ms) held the yellow bitmap on screen — with the border snapped to
|
|
723
|
-
// full alpha that read as "the yellow frame is permanently lit", and the human lost
|
|
724
|
-
// the ability to tell WHICH step took a screenshot. Now yellow shows only while the
|
|
725
|
-
// flash is actually running (<=400 ms) and the border drops straight back to deep
|
|
726
|
-
// blue when acting, or pale breathing when thinking.
|
|
727
|
-
if (!flashOn)
|
|
728
|
-
{
|
|
729
|
-
if (live != committed)
|
|
730
|
-
{
|
|
731
|
-
// require a state to hold for 600 ms before switching: kills strobe
|
|
732
|
-
if (pending != live) { pending = live; pendingSince = Environment.TickCount; }
|
|
733
|
-
else if (unchecked(Environment.TickCount - pendingSince) >= 600) { committed = live; pending = -1; }
|
|
734
|
-
}
|
|
735
|
-
else { pending = -1; }
|
|
736
|
-
}
|
|
760
|
+
committed = acting ? 1 : (thinking ? 3 : 0);
|
|
737
761
|
|
|
738
762
|
// ONE derivation (Glow.DeriveWant): the exit flag outranks ask/stop/flash/committed,
|
|
739
763
|
// so no later state change on another thread can paint a light on an ended session.
|
|
@@ -760,14 +784,14 @@ namespace DshComputerUse.Worker
|
|
|
760
784
|
_killNow = false;
|
|
761
785
|
alpha = 0;
|
|
762
786
|
if (visible) { ShowWindow(hwnd, SW_HIDE); visible = false; }
|
|
763
|
-
curBitmap = 0; committed = 0;
|
|
787
|
+
curBitmap = 0; committed = 0; paintedFlash = 0;
|
|
764
788
|
MSG mk; while (PeekMessage(out mk, hwnd, 0, 0, PM_REMOVE)) { }
|
|
765
789
|
Thread.Sleep(12);
|
|
766
790
|
continue;
|
|
767
791
|
}
|
|
768
792
|
// want: 0 idle | 1 acting (deep blue) | 3 thinking (pale blue) | 2 flash (yellow)
|
|
769
793
|
// 4 emergency-stopped (RED, steady — the session is dead until re-armed)
|
|
770
|
-
int needBitmap = want == 6 ? 5 : want == 2 ? 2 : (want == 0 ? curBitmap : (want == 1 ? 3 : (want == 4 || want == 5 ? 4 : 1)));
|
|
794
|
+
int needBitmap = want == 7 ? 6 : want == 6 ? 5 : want == 2 ? 2 : (want == 0 ? curBitmap : (want == 1 ? 3 : (want == 4 || want == 5 ? 4 : 1)));
|
|
771
795
|
// A REPAINT MUST REACH THE SCREEN EVEN WHEN ALPHA DOES NOT MOVE.
|
|
772
796
|
//
|
|
773
797
|
// The present below used to be gated on `diff != 0` alone, while the flash snaps
|
|
@@ -778,7 +802,8 @@ namespace DshComputerUse.Worker
|
|
|
778
802
|
// RED frame when the border happened to be at full alpha already — whether the
|
|
779
803
|
// brake was visible was luck. Present when EITHER the bitmap or the alpha changed.
|
|
780
804
|
bool repaint = false;
|
|
781
|
-
if (needBitmap ==
|
|
805
|
+
if (needBitmap == 6) { PaintPreparation(bits, vw, vh, PreparationVisible ? unchecked(Environment.TickCount - _preparationStart) : 0); curBitmap = 6; repaint = true; }
|
|
806
|
+
else if (needBitmap == 2 && paintedFlash != _flashStart) { PaintFlash(bits, vw, vh); paintedFlash = _flashStart; curBitmap = 2; alpha = 204; repaint = true; /* snap ON at 80%: a flash is an event, not a fade */ }
|
|
782
807
|
else if (needBitmap == 5 && curBitmap != 5) { PaintHumanYield(bits, vw, vh); curBitmap = 5; repaint = true; }
|
|
783
808
|
else if (needBitmap == 4 && curBitmap != 4) { PaintStop(bits, vw, vh); curBitmap = 4; repaint = true; }
|
|
784
809
|
else if (needBitmap == 3 && curBitmap != 3) { PaintBlue(bits, vw, vh, true); curBitmap = 3; repaint = true; }
|
|
@@ -790,7 +815,7 @@ namespace DshComputerUse.Worker
|
|
|
790
815
|
int target;
|
|
791
816
|
bool flashFrame = false;
|
|
792
817
|
bool breathFrame = false; // breathing states are ABSOLUTE, never eased
|
|
793
|
-
if (want == 0) target = 0;
|
|
818
|
+
if (want == 0) { target = 0; breathFrame = true; }
|
|
794
819
|
else if (want == 2)
|
|
795
820
|
{
|
|
796
821
|
// The capture cue runs its OWN envelope (wind-up, burst, short fade): an absolute
|
|
@@ -798,7 +823,8 @@ namespace DshComputerUse.Worker
|
|
|
798
823
|
target = FlashAlpha(unchecked(Environment.TickCount - _flashStart));
|
|
799
824
|
flashFrame = true;
|
|
800
825
|
}
|
|
801
|
-
else if (want == 1) target = FULL;
|
|
826
|
+
else if (want == 1) { target = FULL; breathFrame = true; }
|
|
827
|
+
else if (want == 7) { target = 255; breathFrame = true; }
|
|
802
828
|
else if (want == 4) target = FULL; // steady red, no breathing
|
|
803
829
|
else if (want == 6)
|
|
804
830
|
{
|
|
@@ -860,7 +886,11 @@ namespace DshComputerUse.Worker
|
|
|
860
886
|
SetWindowPos(hwnd, new IntPtr(-1), vx, vy, vw, vh, 0x0004 | 0x0010 | 0x0040);
|
|
861
887
|
visible = true;
|
|
862
888
|
}
|
|
863
|
-
UpdateLayeredWindow(hwnd, screenDc, ref dst, ref size, memDc, ref src, 0, ref bf, ULW_ALPHA)
|
|
889
|
+
if (UpdateLayeredWindow(hwnd, screenDc, ref dst, ref size, memDc, ref src, 0, ref bf, ULW_ALPHA) && want == 7 && !PreparationVisible)
|
|
890
|
+
{
|
|
891
|
+
_preparationStart = Environment.TickCount;
|
|
892
|
+
Volatile.Write(ref _presentedPreparation, Volatile.Read(ref _preparationSerial));
|
|
893
|
+
}
|
|
864
894
|
}
|
|
865
895
|
else if (visible) { ShowWindow(hwnd, SW_HIDE); visible = false; }
|
|
866
896
|
}
|
|
@@ -870,146 +900,154 @@ namespace DshComputerUse.Worker
|
|
|
870
900
|
}
|
|
871
901
|
}
|
|
872
902
|
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
/// so the pair reads unambiguously: an outer cyan ring breathing = the agent is thinking
|
|
878
|
-
/// (it has not touched the machine yet), the inner steady blue = it is driving right now.
|
|
879
|
-
/// The old design made them the same shape and the same hue at different densities, which
|
|
880
|
-
/// is exactly the case where "is it doing something?" becomes a guess.
|
|
881
|
-
///
|
|
882
|
-
/// Drawn as N concentric rounded-rect strokes whose alpha follows (1-t)^1.8 — a
|
|
883
|
-
/// hand-rolled gradient guaranteed to reach alpha 0 at the INNER edge (a residual
|
|
884
|
-
/// tint there read as a hard seam against near-black windows).
|
|
885
|
-
static void PaintBlue(IntPtr bits, int w, int h, bool intense)
|
|
903
|
+
// Thin, square screen-edge rails leave the application itself completely clear.
|
|
904
|
+
// Each pixel belongs to exactly one inset ring: corners meet without gaps or
|
|
905
|
+
// overlapping hot spots, and the inward bloom ends after eight physical pixels.
|
|
906
|
+
static void PaintEdgeRail(System.Drawing.Graphics g, Rectangle bounds, Color tint)
|
|
886
907
|
{
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
// The CAP has to carry the width: at 1600p the cap is what binds, so a divisor
|
|
894
|
-
// change alone moves nothing (min(46, 1600/26) == min(46, 1600/32) == 46).
|
|
895
|
-
// acting : 37 px (80% of the original 46) — stays INSIDE the thinking ring
|
|
896
|
-
// thinking: 52 px (~40% wider) — reaches OUTSIDE it
|
|
897
|
-
int band = intense
|
|
898
|
-
? Math.Max(22, Math.Min(37, Math.Min(w, h) / 32))
|
|
899
|
-
: Math.Max(30, Math.Min(52, Math.Min(w, h) / 23));
|
|
900
|
-
int r = band + 10;
|
|
901
|
-
int peak = intense ? 240 : 168;
|
|
902
|
-
System.Drawing.Color col = intense
|
|
903
|
-
? System.Drawing.Color.FromArgb(255, 46, 132, 255) // deep, saturated blue
|
|
904
|
-
: System.Drawing.Color.FromArgb(255, 34, 226, 240); // cyan halo (thinking)
|
|
905
|
-
const int N = 60;
|
|
906
|
-
float step = (float)band / N;
|
|
907
|
-
for (int i = 0; i < N; i++)
|
|
908
|
+
int[] alpha = { 244, 216, 94, 53, 30, 16, 8, 3 };
|
|
909
|
+
var smoothing = g.SmoothingMode;
|
|
910
|
+
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.None;
|
|
911
|
+
try
|
|
912
|
+
{
|
|
913
|
+
for (int i = 0; i < alpha.Length; i++)
|
|
908
914
|
{
|
|
909
|
-
|
|
910
|
-
int
|
|
911
|
-
if (
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
+
int x = bounds.Left + i, y = bounds.Top + i;
|
|
916
|
+
int width = bounds.Width - 2 * i, height = bounds.Height - 2 * i;
|
|
917
|
+
if (width <= 0 || height <= 0) break;
|
|
918
|
+
Color start = Color.FromArgb(alpha[i], Math.Min(255, tint.R + 18), Math.Min(255, tint.G + 14), Math.Min(255, tint.B + 8));
|
|
919
|
+
Color end = Color.FromArgb(alpha[i], tint.R, tint.G, tint.B);
|
|
920
|
+
using (var brush = new System.Drawing.Drawing2D.LinearGradientBrush(
|
|
921
|
+
new RectangleF(bounds.X, bounds.Y, Math.Max(1, bounds.Width), Math.Max(1, bounds.Height)), start, end, 32f))
|
|
915
922
|
{
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
923
|
+
g.FillRectangle(brush, x, y, width, 1);
|
|
924
|
+
if (height > 1) g.FillRectangle(brush, x, y + height - 1, width, 1);
|
|
925
|
+
if (height > 2)
|
|
926
|
+
{
|
|
927
|
+
g.FillRectangle(brush, x, y + 1, 1, height - 2);
|
|
928
|
+
if (width > 1) g.FillRectangle(brush, x + width - 1, y + 1, 1, height - 2);
|
|
929
|
+
}
|
|
920
930
|
}
|
|
921
931
|
}
|
|
922
|
-
|
|
932
|
+
}
|
|
933
|
+
finally { g.SmoothingMode = smoothing; }
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
static Rectangle OverlayDisplay(int w, int h)
|
|
937
|
+
{
|
|
938
|
+
var display = new Rectangle(0, 0, w, h);
|
|
939
|
+
if (w == Native.GetSystemMetrics(Native.SM_CXVIRTUALSCREEN) && h == Native.GetSystemMetrics(Native.SM_CYVIRTUALSCREEN))
|
|
940
|
+
{
|
|
941
|
+
display = Screen.PrimaryScreen.Bounds;
|
|
942
|
+
display.Offset(-Native.GetSystemMetrics(Native.SM_XVIRTUALSCREEN), -Native.GetSystemMetrics(Native.SM_YVIRTUALSCREEN));
|
|
943
|
+
}
|
|
944
|
+
return display;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
// WDA_EXCLUDEFROMCAPTURE must not be sent on older Windows, where its fallback
|
|
948
|
+
// can black out the covered image. Unsupported APIs simply leave the overlay visible.
|
|
949
|
+
static void TryExcludeOverlayFromCapture(IntPtr hwnd)
|
|
950
|
+
{
|
|
951
|
+
try
|
|
952
|
+
{
|
|
953
|
+
object build = Microsoft.Win32.Registry.GetValue(
|
|
954
|
+
@"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion", "CurrentBuildNumber", null);
|
|
955
|
+
int number;
|
|
956
|
+
if (int.TryParse(Convert.ToString(build), out number) && number >= 19041)
|
|
957
|
+
SetWindowDisplayAffinity(hwnd, 0x00000011);
|
|
958
|
+
}
|
|
959
|
+
catch { /* display preference only; never changes control or capture behavior */ }
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
static void PaintBlue(IntPtr bits, int w, int h, bool intense)
|
|
963
|
+
{
|
|
964
|
+
WithGraphics(bits, w, h, delegate(System.Drawing.Graphics g) {
|
|
965
|
+
PaintEdgeRail(g, new Rectangle(0, 0, w, h), intense
|
|
966
|
+
? Color.FromArgb(49, 116, 248) : Color.FromArgb(36, 206, 216));
|
|
923
967
|
});
|
|
924
968
|
}
|
|
925
969
|
|
|
926
|
-
/// Semi-transparent badge in the top-left corner. The human must be able to SEE that a
|
|
927
|
-
/// brake exists (and whether it is currently on) — a brake nobody can see feels exactly
|
|
928
|
-
/// like no brake at all.
|
|
929
970
|
public static int HumanYieldAlpha(int tick)
|
|
930
971
|
{ return 48 + (int)Math.Round(30 * (1 - Math.Cos((unchecked((uint)tick) % 2800) * 2 * Math.PI / 2800.0))); }
|
|
931
972
|
static void PaintHumanYield(IntPtr bits, int w, int h)
|
|
932
973
|
{
|
|
933
974
|
WithGraphics(bits, w, h, delegate(System.Drawing.Graphics g) {
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
float inset = (float)i * band / 48;
|
|
938
|
-
int alpha = (int)(175 * Math.Pow(1 - i / 48.0, 1.8));
|
|
939
|
-
using (var pen = new Pen(Color.FromArgb(alpha, 255, 231, 151), band / 48f + 1.2f))
|
|
940
|
-
using (var shape = new System.Drawing.Drawing2D.GraphicsPath())
|
|
941
|
-
{ AddRound(shape, inset, inset, w - 2 * inset, h - 2 * inset, Math.Max(1, band + 8 - inset)); g.DrawPath(pen, shape); }
|
|
942
|
-
}
|
|
943
|
-
PaintBadge(g, "已让出键鼠 · 静默 3 秒后继续 · Ctrl+Esc 暂停", 175, false);
|
|
975
|
+
Color tint = Color.FromArgb(238, 190, 102);
|
|
976
|
+
PaintEdgeRail(g, new Rectangle(0, 0, w, h), tint);
|
|
977
|
+
PaintStatusChip(g, w, h, "已让出键鼠 · 静默 3 秒后接续", tint);
|
|
944
978
|
});
|
|
945
979
|
}
|
|
946
980
|
|
|
947
|
-
static void
|
|
981
|
+
static void PaintPreparation(IntPtr bits, int w, int h, int elapsed)
|
|
948
982
|
{
|
|
949
|
-
|
|
983
|
+
WithGraphics(bits, w, h, delegate(System.Drawing.Graphics g) {
|
|
984
|
+
Color tint = Color.FromArgb(166, 139, 250);
|
|
985
|
+
PaintEdgeRail(g, new Rectangle(0, 0, w, h), tint);
|
|
986
|
+
Rectangle display = OverlayDisplay(w, h);
|
|
987
|
+
float scale = Math.Max(1f, Math.Min(1.25f, display.Width / 1920f));
|
|
988
|
+
float bw = Math.Min(408 * scale, display.Width - 24), bh = 64 * scale;
|
|
989
|
+
float x = display.Left + (display.Width - bw) / 2, y = display.Top + 10 * scale;
|
|
990
|
+
float progress = Math.Max(0f, Math.Min(1f, elapsed / (float)PreparationMs));
|
|
991
|
+
using (var shape = new System.Drawing.Drawing2D.GraphicsPath())
|
|
992
|
+
using (var fill = new System.Drawing.Drawing2D.LinearGradientBrush(new RectangleF(x, y, bw, bh),
|
|
993
|
+
Color.FromArgb(224, 23, 27, 39), Color.FromArgb(224, 35, 31, 53), 0f))
|
|
994
|
+
using (var outline = new Pen(Color.FromArgb(110, tint), 1f))
|
|
995
|
+
using (var title = new Font("Microsoft YaHei UI", 12.5f * scale, FontStyle.Bold))
|
|
996
|
+
using (var detail = new Font("Microsoft YaHei UI", 9f * scale))
|
|
997
|
+
using (var white = new SolidBrush(Color.FromArgb(255, 246, 247, 253)))
|
|
998
|
+
using (var soft = new SolidBrush(Color.FromArgb(238, 183, 188, 207)))
|
|
999
|
+
using (var ring = new Pen(Color.FromArgb(70, tint), 2 * scale))
|
|
1000
|
+
using (var accent = new Pen(Color.FromArgb(245, tint), 2 * scale))
|
|
1001
|
+
using (var track = new SolidBrush(Color.FromArgb(52, tint)))
|
|
1002
|
+
using (var bar = new System.Drawing.Drawing2D.LinearGradientBrush(
|
|
1003
|
+
new RectangleF(x + 48 * scale, y + 56 * scale, bw - 64 * scale, 2 * scale),
|
|
1004
|
+
Color.FromArgb(220, 167, 139, 250), Color.FromArgb(235, 96, 165, 250), 0f))
|
|
1005
|
+
{
|
|
1006
|
+
AddRound(shape, x, y, bw, bh, 14 * scale); g.FillPath(fill, shape); g.DrawPath(outline, shape);
|
|
1007
|
+
g.DrawEllipse(ring, x + 16 * scale, y + 20 * scale, 20 * scale, 20 * scale);
|
|
1008
|
+
if (progress > 0) g.DrawArc(accent, x + 16 * scale, y + 20 * scale, 20 * scale, 20 * scale, -90, 360 * progress);
|
|
1009
|
+
g.DrawString("即将开始操作,请暂停键鼠", title, white, x + 48 * scale, y + 9 * scale);
|
|
1010
|
+
g.DrawString("Ctrl+Esc 暂停", detail, soft, x + 48 * scale, y + 34 * scale);
|
|
1011
|
+
string remaining = (Math.Max(0, PreparationMs - elapsed) / 1000.0).ToString("0.0") + " 秒后开始";
|
|
1012
|
+
SizeF size = g.MeasureString(remaining, detail);
|
|
1013
|
+
g.DrawString(remaining, detail, soft, x + bw - 16 * scale - size.Width, y + 34 * scale);
|
|
1014
|
+
g.FillRectangle(track, x + 48 * scale, y + 56 * scale, bw - 64 * scale, 2 * scale);
|
|
1015
|
+
if (progress > 0) g.FillRectangle(bar, x + 48 * scale, y + 56 * scale, (bw - 64 * scale) * progress, 2 * scale);
|
|
1016
|
+
}
|
|
1017
|
+
});
|
|
950
1018
|
}
|
|
951
1019
|
|
|
952
|
-
static void
|
|
1020
|
+
static void PaintStatusChip(System.Drawing.Graphics g, int w, int h, string text, Color tint)
|
|
953
1021
|
{
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
int o = Math.Max(40, Math.Min(255, opacity));
|
|
958
|
-
using (System.Drawing.Font f = new System.Drawing.Font("Microsoft YaHei", 11.25f, System.Drawing.FontStyle.Bold))
|
|
1022
|
+
Rectangle display = OverlayDisplay(w, h);
|
|
1023
|
+
float scale = Math.Max(1f, Math.Min(1.25f, display.Width / 1920f));
|
|
1024
|
+
using (var font = new Font("Microsoft YaHei UI", 9f * scale))
|
|
959
1025
|
{
|
|
960
|
-
|
|
961
|
-
float bw =
|
|
962
|
-
float x =
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
g.
|
|
972
|
-
|
|
973
|
-
g.DrawPath(pen, p);
|
|
974
|
-
p.Dispose();
|
|
975
|
-
using (System.Drawing.SolidBrush tb = new System.Drawing.SolidBrush(
|
|
976
|
-
red ? System.Drawing.Color.FromArgb((int)(o * 0.70), 255, 226, 226)
|
|
977
|
-
: System.Drawing.Color.FromArgb((int)(o * 0.62), 236, 245, 255)))
|
|
978
|
-
g.DrawString(txt, f, tb, x + 12f, y + 6f);
|
|
1026
|
+
SizeF measured = g.MeasureString(text, font);
|
|
1027
|
+
float bw = Math.Min(measured.Width + 38 * scale, display.Width - 24), bh = 28 * scale;
|
|
1028
|
+
float x = display.Left + (display.Width - bw) / 2, y = display.Top + 8 * scale;
|
|
1029
|
+
using (var shape = new System.Drawing.Drawing2D.GraphicsPath())
|
|
1030
|
+
using (var fill = new SolidBrush(Color.FromArgb(222, 22, 26, 36)))
|
|
1031
|
+
using (var outline = new Pen(Color.FromArgb(90, tint), 1f))
|
|
1032
|
+
using (var accent = new SolidBrush(Color.FromArgb(255, tint)))
|
|
1033
|
+
using (var ink = new SolidBrush(Color.FromArgb(250, 235, 239, 248)))
|
|
1034
|
+
{
|
|
1035
|
+
AddRound(shape, x, y, bw, bh, 12 * scale); g.FillPath(fill, shape); g.DrawPath(outline, shape);
|
|
1036
|
+
g.FillEllipse(accent, x + 12 * scale, y + 11 * scale, 6 * scale, 6 * scale);
|
|
1037
|
+
g.DrawString(text, font, ink, x + 24 * scale, y + (bh - measured.Height) / 2);
|
|
1038
|
+
}
|
|
979
1039
|
}
|
|
980
1040
|
}
|
|
981
1041
|
|
|
982
|
-
/// The stopped state: a RED border plus a red badge, steady (no breathing) — the
|
|
983
|
-
/// computer-use session is dead until a human re-arms it.
|
|
984
1042
|
static void PaintStop(IntPtr bits, int w, int h)
|
|
985
1043
|
{
|
|
986
|
-
WithGraphics(bits, w, h, delegate
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
const int N = 60;
|
|
991
|
-
float step = (float)band / N;
|
|
992
|
-
for (int i = 0; i < N; i++)
|
|
993
|
-
{
|
|
994
|
-
double t = (double)i / N;
|
|
995
|
-
int a = (int)Math.Round(248 * Math.Pow(1 - t, 2.0));
|
|
996
|
-
if (a <= 0) continue;
|
|
997
|
-
float inset = i * step;
|
|
998
|
-
using (System.Drawing.Pen pen = new System.Drawing.Pen(
|
|
999
|
-
System.Drawing.Color.FromArgb(a, 238, 66, 74), step + 1.2f))
|
|
1000
|
-
{
|
|
1001
|
-
System.Drawing.Drawing2D.GraphicsPath path = new System.Drawing.Drawing2D.GraphicsPath();
|
|
1002
|
-
AddRound(path, inset, inset, w - inset * 2, h - inset * 2, Math.Max(1, r - inset));
|
|
1003
|
-
g.DrawPath(pen, path);
|
|
1004
|
-
path.Dispose();
|
|
1005
|
-
}
|
|
1006
|
-
}
|
|
1007
|
-
PaintBadge(g, "已暂停 · Ctrl+Alt+R 继续 · Ctrl+Alt+Q 退出", 252, true);
|
|
1044
|
+
WithGraphics(bits, w, h, delegate(System.Drawing.Graphics g) {
|
|
1045
|
+
Color tint = Color.FromArgb(240, 84, 102);
|
|
1046
|
+
PaintEdgeRail(g, new Rectangle(0, 0, w, h), tint);
|
|
1047
|
+
PaintStatusChip(g, w, h, "操作已暂停 · Ctrl+Alt+R 继续", tint);
|
|
1008
1048
|
});
|
|
1009
1049
|
}
|
|
1010
1050
|
|
|
1011
|
-
/// Yellow "a screenshot was just taken" rectangle around the captured region —
|
|
1012
|
-
/// same stepped-gradient treatment, no strokes, no hard edges.
|
|
1013
1051
|
static void PaintFlash(IntPtr bits, int w, int h)
|
|
1014
1052
|
{
|
|
1015
1053
|
int fx = _fx, fy = _fy, fw = _fw, fh = _fh;
|
|
@@ -1019,32 +1057,8 @@ namespace DshComputerUse.Worker
|
|
|
1019
1057
|
if (fx + fw > w) fw = w - fx;
|
|
1020
1058
|
if (fy + fh > h) fh = h - fy;
|
|
1021
1059
|
if (fw < 8 || fh < 8) { fx = 0; fy = 0; fw = w; fh = h; }
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
{
|
|
1025
|
-
// 2026-09-12 (human): the capture cue on its own, 40% narrower again — 32 -> 19.
|
|
1026
|
-
// The corner radius scales with it (13 -> 8) so the strip still reads as a thin
|
|
1027
|
-
// glow instead of a fat rounded rectangle. Width only: the alpha profile is
|
|
1028
|
-
// untouched, so the cue keeps its brightness.
|
|
1029
|
-
int band = 19;
|
|
1030
|
-
int r = 8;
|
|
1031
|
-
const int N = 40;
|
|
1032
|
-
float step = (float)band / N;
|
|
1033
|
-
for (int i = 0; i < N; i++)
|
|
1034
|
-
{
|
|
1035
|
-
double t = (double)i / N;
|
|
1036
|
-
int a = (int)Math.Round(200 * Math.Pow(1 - t, 1.9));
|
|
1037
|
-
if (a <= 0) continue;
|
|
1038
|
-
float inset = i * step;
|
|
1039
|
-
using (System.Drawing.Pen pen = new System.Drawing.Pen(
|
|
1040
|
-
System.Drawing.Color.FromArgb(a, 255, 186, 20), step + 1.2f))
|
|
1041
|
-
{
|
|
1042
|
-
System.Drawing.Drawing2D.GraphicsPath path = new System.Drawing.Drawing2D.GraphicsPath();
|
|
1043
|
-
AddRound(path, fx + inset, fy + inset, fw - inset * 2, fh - inset * 2, Math.Max(1, r - inset));
|
|
1044
|
-
g.DrawPath(pen, path);
|
|
1045
|
-
path.Dispose();
|
|
1046
|
-
}
|
|
1047
|
-
}
|
|
1060
|
+
WithGraphics(bits, w, h, delegate(System.Drawing.Graphics g) {
|
|
1061
|
+
PaintEdgeRail(g, new Rectangle(fx, fy, fw, fh), Color.FromArgb(247, 193, 62));
|
|
1048
1062
|
});
|
|
1049
1063
|
}
|
|
1050
1064
|
|
|
@@ -1056,6 +1070,7 @@ namespace DshComputerUse.Worker
|
|
|
1056
1070
|
using (System.Drawing.Graphics g = System.Drawing.Graphics.FromImage(bmp))
|
|
1057
1071
|
{
|
|
1058
1072
|
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
|
|
1073
|
+
g.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAliasGridFit;
|
|
1059
1074
|
g.Clear(System.Drawing.Color.Transparent);
|
|
1060
1075
|
draw(g);
|
|
1061
1076
|
}
|
|
@@ -1232,6 +1247,7 @@ namespace DshComputerUse.Worker
|
|
|
1232
1247
|
// separate statements, so a Q between the check and the write re-lit a dead cycle.
|
|
1233
1248
|
lock (SyncLock)
|
|
1234
1249
|
{
|
|
1250
|
+
Program.InvalidateActiveInput();
|
|
1235
1251
|
_temporary = null;
|
|
1236
1252
|
_engaged = false;
|
|
1237
1253
|
_stopped = false;
|
|
@@ -1258,6 +1274,7 @@ namespace DshComputerUse.Worker
|
|
|
1258
1274
|
{
|
|
1259
1275
|
if (_exited) return false;
|
|
1260
1276
|
if (!CycleLit) Program.ResetKickoffFocus();
|
|
1277
|
+
_controlsReleased = false;
|
|
1261
1278
|
CycleLit = true; // an agent cycle is live: cyan is on (R1)
|
|
1262
1279
|
Arm(); // "Computer use 一旦启用,立即挂打断监听"
|
|
1263
1280
|
// A BRAKE OUTRANKS "THINKING" (cold-start bug, 2026-09-13): if a stop is already
|
|
@@ -1281,6 +1298,19 @@ namespace DshComputerUse.Worker
|
|
|
1281
1298
|
return true;
|
|
1282
1299
|
}
|
|
1283
1300
|
}
|
|
1301
|
+
public static bool TryEndCycle()
|
|
1302
|
+
{
|
|
1303
|
+
SyncExitedFromFile(); SyncFromFile();
|
|
1304
|
+
lock (SyncLock)
|
|
1305
|
+
{
|
|
1306
|
+
if (_engaged || _stopped || PauseRequested) return false;
|
|
1307
|
+
_controlsReleased = true;
|
|
1308
|
+
CycleLit = false;
|
|
1309
|
+
Disarm(); Glow.Kill();
|
|
1310
|
+
Program.ReleaseOwnedInputs(); Program.ReleaseDesktopOwnership();
|
|
1311
|
+
return true;
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1284
1314
|
|
|
1285
1315
|
/// Was the record written after this process started? Only then is it news rather than an
|
|
1286
1316
|
/// inherited fact. (The record carries its own `at=` line precisely so this is answerable
|
|
@@ -1355,7 +1385,7 @@ namespace DshComputerUse.Worker
|
|
|
1355
1385
|
}
|
|
1356
1386
|
// OUTSIDE the lock: Notify writes to stdout, and a full pipe must never hold the lock the
|
|
1357
1387
|
// exit and the cycle-opener share.
|
|
1358
|
-
if (adopted) ReleaseStuck();
|
|
1388
|
+
if (adopted) { ReleaseStuck(); Program.ReleaseDesktopOwnership(); }
|
|
1359
1389
|
if (announce) Notify("exit", ExitSummary());
|
|
1360
1390
|
}
|
|
1361
1391
|
|
|
@@ -1751,6 +1781,20 @@ namespace DshComputerUse.Worker
|
|
|
1751
1781
|
} while (!ControlQueue.IsEmpty && Interlocked.CompareExchange(ref _controlWorkQueued, 1, 0) == 0);
|
|
1752
1782
|
});
|
|
1753
1783
|
}
|
|
1784
|
+
static void QueueDesktopControl(Action action, Action skipped = null)
|
|
1785
|
+
{
|
|
1786
|
+
QueueControl(delegate {
|
|
1787
|
+
// Serialize with graceful release: a queued physical hotkey may not
|
|
1788
|
+
// reopen a completed cycle or steer an exit into its continuing chat.
|
|
1789
|
+
lock (SyncLock)
|
|
1790
|
+
{
|
|
1791
|
+
if (!ControlListenersEnabled || !Program.TryClaimDesktop("physical control"))
|
|
1792
|
+
{ if (skipped != null) skipped(); return; }
|
|
1793
|
+
try { action(); }
|
|
1794
|
+
finally { if (!CycleLit) Program.ReleaseDesktopOwnership(); }
|
|
1795
|
+
}
|
|
1796
|
+
});
|
|
1797
|
+
}
|
|
1754
1798
|
public static string ControlHotkey(uint vk, bool down, bool human, bool ctrl, bool alt, bool shift, bool win, bool live)
|
|
1755
1799
|
{
|
|
1756
1800
|
if (!down || !human) return "";
|
|
@@ -1760,8 +1804,7 @@ namespace DshComputerUse.Worker
|
|
|
1760
1804
|
return "";
|
|
1761
1805
|
}
|
|
1762
1806
|
static bool _suppressPauseEsc;
|
|
1763
|
-
static
|
|
1764
|
-
static POINT _physicalPoint;
|
|
1807
|
+
static readonly PointerHandoffTolerance PointerMotion = new PointerHandoffTolerance();
|
|
1765
1808
|
public static bool PhysicalEvent(IntPtr extra, uint flags, uint injectedMask)
|
|
1766
1809
|
{ return extra != OwnMagic && (flags & injectedMask) == 0; }
|
|
1767
1810
|
|
|
@@ -1773,15 +1816,18 @@ namespace DshComputerUse.Worker
|
|
|
1773
1816
|
{
|
|
1774
1817
|
var m = (Native.MSLLHOOKSTRUCT)Marshal.PtrToStructure(lParam, typeof(Native.MSLLHOOKSTRUCT));
|
|
1775
1818
|
if (m.dwExtraInfo != OwnMagic) Interlocked.Increment(ref _externalInput);
|
|
1776
|
-
|
|
1819
|
+
bool physical = PhysicalEvent(m.dwExtraInfo, m.flags, 3);
|
|
1820
|
+
int message = wParam.ToInt32();
|
|
1821
|
+
// Significant idle motion invalidates observations, but does not yield.
|
|
1822
|
+
bool movement = message == 0x200 && PointerMotion.Record(m.pt.X, m.pt.Y, physical,
|
|
1823
|
+
true, Handoff.Active, HumanClock.ElapsedMilliseconds);
|
|
1824
|
+
if (physical)
|
|
1777
1825
|
{
|
|
1778
|
-
int
|
|
1826
|
+
int key = 0;
|
|
1779
1827
|
bool down = false, up = false, activity = true;
|
|
1780
1828
|
switch (message)
|
|
1781
1829
|
{
|
|
1782
|
-
case 0x200:
|
|
1783
|
-
activity = !_havePhysicalPoint || m.pt.X != _physicalPoint.X || m.pt.Y != _physicalPoint.Y;
|
|
1784
|
-
_physicalPoint = m.pt; _havePhysicalPoint = true; break;
|
|
1830
|
+
case 0x200: activity = movement; break;
|
|
1785
1831
|
case 0x201: key = 1; down = true; break;
|
|
1786
1832
|
case 0x202: key = 1; up = true; break;
|
|
1787
1833
|
case 0x204: key = 2; down = true; break;
|
|
@@ -1820,8 +1866,8 @@ namespace DshComputerUse.Worker
|
|
|
1820
1866
|
{ if (up) _suppressPauseEsc = false; return new IntPtr(1); }
|
|
1821
1867
|
bool ctrl = _humanCtrl && (Native.GetAsyncKeyState(Native.VK_CONTROL) & 0x8000) != 0;
|
|
1822
1868
|
bool alt = _humanAlt && (Native.GetAsyncKeyState(Native.VK_MENU) & 0x8000) != 0;
|
|
1823
|
-
string hotkey = ControlHotkey(k.vkCode, down, human, ctrl, alt,
|
|
1824
|
-
Handoff.Held(0x10), Handoff.Held(0x5B) || Handoff.Held(0x5C), CycleLit && !_exited);
|
|
1869
|
+
string hotkey = ControlListenersEnabled ? ControlHotkey(k.vkCode, down, human, ctrl, alt,
|
|
1870
|
+
Handoff.Held(0x10), Handoff.Held(0x5B) || Handoff.Held(0x5C), CycleLit && !_exited) : "";
|
|
1825
1871
|
if (hotkey == "pause")
|
|
1826
1872
|
{
|
|
1827
1873
|
_suppressPauseEsc = true;
|
|
@@ -1829,19 +1875,19 @@ namespace DshComputerUse.Worker
|
|
|
1829
1875
|
// The hook marks ownership immediately; do not perform SendInput or
|
|
1830
1876
|
// filesystem work on this OS hook thread.
|
|
1831
1877
|
string evidence = ChordEvidence(k, ctrl, alt);
|
|
1832
|
-
|
|
1878
|
+
QueueDesktopControl(delegate {
|
|
1833
1879
|
try { Engage("Ctrl+Esc chord detected", false, "hook", evidence); }
|
|
1834
1880
|
finally { Interlocked.Decrement(ref _pendingPauses); }
|
|
1835
|
-
});
|
|
1881
|
+
}, delegate { Interlocked.Decrement(ref _pendingPauses); });
|
|
1836
1882
|
return new IntPtr(1); // Ctrl+Esc must not also open the Windows Start menu.
|
|
1837
1883
|
}
|
|
1838
1884
|
if (hotkey == "exit")
|
|
1839
1885
|
{
|
|
1840
1886
|
string evidence = ChordEvidence(k, ctrl, alt);
|
|
1841
|
-
|
|
1887
|
+
QueueDesktopControl(delegate { Exit("Ctrl+Alt+Q chord detected", "hook", evidence); });
|
|
1842
1888
|
}
|
|
1843
1889
|
else if (hotkey == "resume")
|
|
1844
|
-
|
|
1890
|
+
QueueDesktopControl(delegate { Clear(); });
|
|
1845
1891
|
}
|
|
1846
1892
|
}
|
|
1847
1893
|
}
|
|
@@ -1886,6 +1932,7 @@ namespace DshComputerUse.Worker
|
|
|
1886
1932
|
// SendInputChecked (which now refuses after an exit): releasing what is stuck is exactly
|
|
1887
1933
|
// the cleanup that must still be allowed.
|
|
1888
1934
|
ReleaseStuck();
|
|
1935
|
+
Program.ReleaseDesktopOwnership();
|
|
1889
1936
|
Program.AuditRecord("EXIT", _exitSource + "\t" + why + "\t" + _exitEvidence);
|
|
1890
1937
|
Notify("exit", ExitSummary());
|
|
1891
1938
|
}
|
|
@@ -1963,12 +2010,12 @@ namespace DshComputerUse.Worker
|
|
|
1963
2010
|
bool r = (Native.GetAsyncKeyState(Native.VK_R) & 1) != 0;
|
|
1964
2011
|
bool agentQuiet = _agentActAt == 0 || unchecked(Environment.TickCount - _agentActAt) > 400;
|
|
1965
2012
|
if (warm < 3) warm++;
|
|
1966
|
-
else if (_humanCtrl && _humanAlt && asyncCtrl && asyncAlt && agentQuiet)
|
|
2013
|
+
else if (ControlListenersEnabled && _humanCtrl && _humanAlt && asyncCtrl && asyncAlt && agentQuiet)
|
|
1967
2014
|
{
|
|
1968
2015
|
// R FIRST: Ctrl+Alt+R is the human's explicit re-arm, and a stale Q bit must
|
|
1969
2016
|
// never beat it — pressing the key that brings computer use BACK used to be
|
|
1970
2017
|
// able to END it instead.
|
|
1971
|
-
if (r && !prevR)
|
|
2018
|
+
if (r && !prevR) QueueDesktopControl(delegate { Clear(); });
|
|
1972
2019
|
else if (q && !prevQ)
|
|
1973
2020
|
{
|
|
1974
2021
|
Native.KBDLLHOOKSTRUCT kk = new Native.KBDLLHOOKSTRUCT();
|
|
@@ -1977,7 +2024,7 @@ namespace DshComputerUse.Worker
|
|
|
1977
2024
|
"q-latch + hook-tracked modifiers + measured async key state " +
|
|
1978
2025
|
"(no keydown was seen by the hook); " +
|
|
1979
2026
|
ChordEvidence(kk, asyncCtrl, asyncAlt);
|
|
1980
|
-
|
|
2027
|
+
QueueDesktopControl(delegate { Exit("Ctrl+Alt+Q chord detected", "poll", evidence); });
|
|
1981
2028
|
}
|
|
1982
2029
|
}
|
|
1983
2030
|
prevQ = q; prevR = r;
|
|
@@ -1993,6 +2040,8 @@ namespace DshComputerUse.Worker
|
|
|
1993
2040
|
// durable manual brake and never creates a STOP record.
|
|
1994
2041
|
static int _agentActAt;
|
|
1995
2042
|
static volatile bool _armed = false;
|
|
2043
|
+
static volatile bool _controlsReleased = true;
|
|
2044
|
+
public static bool ControlListenersEnabled { get { return !_controlsReleased; } }
|
|
1996
2045
|
public static volatile bool CycleLit;
|
|
1997
2046
|
public static bool Armed { get { return _armed; } }
|
|
1998
2047
|
public static bool Exited { get { return _exited; } }
|
|
@@ -2000,15 +2049,33 @@ namespace DshComputerUse.Worker
|
|
|
2000
2049
|
static readonly HumanInputState Handoff = new HumanInputState();
|
|
2001
2050
|
static readonly object HandoffPublishLock = new object();
|
|
2002
2051
|
static string _publishedHandoff = "idle";
|
|
2052
|
+
static long _physicalInputEpoch, _publishedInputEpoch;
|
|
2003
2053
|
static int _handoffWorkQueued;
|
|
2004
2054
|
public static bool HumanActive { get { return Handoff.Active; } }
|
|
2005
2055
|
public static long HumanRevision { get { return Handoff.Revision; } }
|
|
2006
2056
|
public static bool HumanHolds(int vk) { return Handoff.Held(vk); }
|
|
2007
|
-
public static
|
|
2057
|
+
public static long InputEpoch { get { return Interlocked.Read(ref _physicalInputEpoch); } }
|
|
2058
|
+
public static object CooperationState()
|
|
2059
|
+
{
|
|
2060
|
+
var state = (Dictionary<string, object>)Handoff.Snapshot(HumanClock.ElapsedMilliseconds);
|
|
2061
|
+
state["inputEpoch"] = InputEpoch; state["mouseTolerancePx"] = PointerHandoffTolerance.DistancePx;
|
|
2062
|
+
return state;
|
|
2063
|
+
}
|
|
2008
2064
|
static void ResetMonitorRun() { /* Manual recovery must not erase physical input history. */ }
|
|
2009
2065
|
public static void Arm() { _armed = true; Handoff.Enable(CycleLit && !_exited, HumanClock.ElapsedMilliseconds); }
|
|
2010
2066
|
public static void Disarm() { _armed = false; Handoff.Enable(false, HumanClock.ElapsedMilliseconds); PublishHandoff(); }
|
|
2011
2067
|
public static void AgentActed() { _agentActAt = Environment.TickCount; }
|
|
2068
|
+
public static void BeginActuation()
|
|
2069
|
+
{
|
|
2070
|
+
POINT point; if (Native.GetCursorPos(out point)) PointerMotion.Reset(point.X, point.Y);
|
|
2071
|
+
Glow.ActuationBegin();
|
|
2072
|
+
Handoff.SetActing(true, HumanClock.ElapsedMilliseconds);
|
|
2073
|
+
}
|
|
2074
|
+
public static void EndActuation()
|
|
2075
|
+
{
|
|
2076
|
+
Handoff.SetActing(false, HumanClock.ElapsedMilliseconds);
|
|
2077
|
+
Glow.ActuationEnd();
|
|
2078
|
+
}
|
|
2012
2079
|
static bool IsBareModifier(uint vk)
|
|
2013
2080
|
{
|
|
2014
2081
|
return vk == Native.VK_SHIFT || vk == Native.VK_CONTROL || vk == Native.VK_MENU ||
|
|
@@ -2017,6 +2084,7 @@ namespace DshComputerUse.Worker
|
|
|
2017
2084
|
}
|
|
2018
2085
|
public static void RecordHumanInput(int key, bool down, bool up)
|
|
2019
2086
|
{
|
|
2087
|
+
Interlocked.Increment(ref _physicalInputEpoch);
|
|
2020
2088
|
long now = HumanClock.ElapsedMilliseconds;
|
|
2021
2089
|
bool was = Handoff.Active;
|
|
2022
2090
|
Handoff.Enable(_armed && CycleLit && !_exited, now);
|
|
@@ -2032,15 +2100,20 @@ namespace DshComputerUse.Worker
|
|
|
2032
2100
|
lock (HandoffPublishLock)
|
|
2033
2101
|
{
|
|
2034
2102
|
string phase = Handoff.Phase;
|
|
2035
|
-
|
|
2103
|
+
long epoch = InputEpoch;
|
|
2104
|
+
if (phase == _publishedHandoff && epoch == _publishedInputEpoch) return;
|
|
2105
|
+
bool changed = phase != _publishedHandoff;
|
|
2036
2106
|
_publishedHandoff = phase;
|
|
2107
|
+
_publishedInputEpoch = epoch;
|
|
2108
|
+
if (!ControlListenersEnabled && !CycleLit && phase == "idle") return;
|
|
2037
2109
|
if (phase != "idle") Glow.HumanYield();
|
|
2038
|
-
Program.AuditRecord("HUMAN-HANDOFF", "phase=" + phase + "; revision=" + HumanRevision + "; kind=" + Handoff.LastKind);
|
|
2110
|
+
if (changed) Program.AuditRecord("HUMAN-HANDOFF", "phase=" + phase + "; revision=" + HumanRevision + "; kind=" + Handoff.LastKind);
|
|
2039
2111
|
Notify("handoff", phase);
|
|
2040
2112
|
}
|
|
2041
2113
|
}
|
|
2042
2114
|
static void MonitorHuman()
|
|
2043
2115
|
{
|
|
2116
|
+
if (!ControlListenersEnabled) return;
|
|
2044
2117
|
SyncExitedFromFile();
|
|
2045
2118
|
long now = HumanClock.ElapsedMilliseconds;
|
|
2046
2119
|
Handoff.Enable(_armed && CycleLit && !_exited, now);
|
|
@@ -2822,8 +2895,8 @@ namespace DshComputerUse.Worker
|
|
|
2822
2895
|
static readonly object OutLock = new object(); // responses AND unsolicited events share stdout
|
|
2823
2896
|
static readonly DateTime Epoch = new DateTime(1970, 1, 1);
|
|
2824
2897
|
|
|
2825
|
-
// Position correction is diagnostic only.
|
|
2826
|
-
//
|
|
2898
|
+
// Position correction is diagnostic only. Deliberate physical takeover during
|
|
2899
|
+
// actual input yields; incidental mouse jitter and idle use do not pause the task.
|
|
2827
2900
|
static int _positionCorrections;
|
|
2828
2901
|
static string _dpiNote = "unset";
|
|
2829
2902
|
static readonly HashSet<string> ObsOps = new HashSet<string>
|
|
@@ -2841,16 +2914,44 @@ namespace DshComputerUse.Worker
|
|
|
2841
2914
|
"activate", "windowOp", "calibrate", "selectRange", "uiaAct"
|
|
2842
2915
|
};
|
|
2843
2916
|
static readonly object InputLock = new object();
|
|
2917
|
+
static readonly DesktopOwnershipLease DesktopLease = new DesktopOwnershipLease(DesktopOwnershipLease.DefaultPath());
|
|
2918
|
+
static bool _releaseDesktopPending;
|
|
2919
|
+
public static bool DesktopOwned { get { return DesktopLease.Owned; } }
|
|
2920
|
+
public static bool TryClaimDesktop(string owner)
|
|
2921
|
+
{ string occupiedBy; return DesktopLease.TryAcquire(owner, out occupiedBy); }
|
|
2922
|
+
static void EnsureDesktopOwnership(string op)
|
|
2923
|
+
{
|
|
2924
|
+
if (op == "ping" || op == "echoargs" || op == "keyClass" || op == "stuckPlan" || op == "probe") return;
|
|
2925
|
+
string occupiedBy;
|
|
2926
|
+
if (!DesktopLease.TryAcquire("native cycle: " + op, out occupiedBy))
|
|
2927
|
+
{
|
|
2928
|
+
var error = InputInterrupted("MACHINE_OWNED", "Another agent owns this desktop's computer-use cycle (" + occupiedBy + "). This operation was not dispatched; keep the task pending until that cycle releases control.");
|
|
2929
|
+
error.Data["outcome"] = "not-dispatched"; throw error;
|
|
2930
|
+
}
|
|
2931
|
+
}
|
|
2932
|
+
public static void ReleaseDesktopOwnership()
|
|
2933
|
+
{
|
|
2934
|
+
lock (InputLock)
|
|
2935
|
+
{
|
|
2936
|
+
_releaseDesktopPending = true;
|
|
2937
|
+
if (!_operationLive && OwnedUps.Count == 0) { DesktopLease.Dispose(); _releaseDesktopPending = false; }
|
|
2938
|
+
}
|
|
2939
|
+
}
|
|
2844
2940
|
static readonly Dictionary<string, INPUT> OwnedUps = new Dictionary<string, INPUT>();
|
|
2845
2941
|
static readonly ConcurrentDictionary<int, bool> CancelledRequests = new ConcurrentDictionary<int, bool>();
|
|
2846
2942
|
static long _cancelGeneration;
|
|
2943
|
+
public static void InvalidateActiveInput() { Interlocked.Increment(ref _cancelGeneration); }
|
|
2847
2944
|
static long _operationCancelGeneration, _operationHumanRevision;
|
|
2848
2945
|
static volatile bool _operationLive, _operationDispatched;
|
|
2946
|
+
static bool _operationActing;
|
|
2947
|
+
static string _lastAnnouncedSeries;
|
|
2948
|
+
static long _announcedHumanRevision, _announcedInputEpoch, _preparedInputEpoch;
|
|
2849
2949
|
static volatile int _activeRequestId, _lastCompletedRequestId;
|
|
2850
2950
|
// The seam is internal to the compiled program; no tool can replace it.
|
|
2851
2951
|
internal static Func<INPUT[], uint> InputSender = delegate(INPUT[] inputs) {
|
|
2852
2952
|
return Native.SendInput((uint)inputs.Length, inputs, InputCbSize);
|
|
2853
2953
|
};
|
|
2954
|
+
internal static Func<IntPtr> PreparationForeground = delegate { return Native.GetForegroundWindow(); };
|
|
2854
2955
|
|
|
2855
2956
|
static Exception InputInterrupted(string code, string message)
|
|
2856
2957
|
{
|
|
@@ -2866,11 +2967,74 @@ namespace DshComputerUse.Worker
|
|
|
2866
2967
|
if (_operationLive && _operationCancelGeneration != Interlocked.Read(ref _cancelGeneration))
|
|
2867
2968
|
throw InputInterrupted("INPUT_CANCELLED", "the owning tool was cancelled; no further input will be sent.");
|
|
2868
2969
|
if (Panic.HumanActive || (_operationLive && _operationHumanRevision != Panic.HumanRevision))
|
|
2869
|
-
throw InputInterrupted("HUMAN_YIELD", "
|
|
2970
|
+
throw InputInterrupted("HUMAN_YIELD", "Temporary interruption only: the user currently owns input and the original task remains active. Wait locally until control returns after three quiet seconds, then immediately obtain fresh target evidence and continue the remaining authorized task. Do not abandon, cancel or declare completion; never replay a partial write.");
|
|
2870
2971
|
}
|
|
2871
2972
|
static void MarkMutation()
|
|
2872
2973
|
{
|
|
2873
|
-
Panic.Check("actuation"); CheckHumanInput(); _operationDispatched = true;
|
|
2974
|
+
Panic.Check("actuation"); BeginActuation(); CheckHumanInput(); _operationDispatched = true;
|
|
2975
|
+
}
|
|
2976
|
+
static void BeginActuation()
|
|
2977
|
+
{
|
|
2978
|
+
if (_operationActing) return;
|
|
2979
|
+
if (_operationLive && _preparedInputEpoch != Panic.InputEpoch)
|
|
2980
|
+
throw InputInterrupted("HUMAN_REOBSERVE", "The user changed the desktop before input began. No action was dispatched. Obtain fresh target evidence and continue the original task; this is a temporary checkpoint, not cancellation.");
|
|
2981
|
+
_operationActing = true;
|
|
2982
|
+
Panic.BeginActuation();
|
|
2983
|
+
CheckHumanInput();
|
|
2984
|
+
if (_operationLive && _preparedInputEpoch != Panic.InputEpoch)
|
|
2985
|
+
throw InputInterrupted("HUMAN_REOBSERVE", "The desktop changed at the input boundary. No action was dispatched. Read the current target and continue the pending task.");
|
|
2986
|
+
}
|
|
2987
|
+
static void EndActuationForReadback()
|
|
2988
|
+
{
|
|
2989
|
+
lock (InputLock)
|
|
2990
|
+
{
|
|
2991
|
+
// Nested selection/chord helpers may still own a modifier or button.
|
|
2992
|
+
// Keep their sequence protected until every accepted down is released.
|
|
2993
|
+
if (OwnedUps.Count != 0) return;
|
|
2994
|
+
_operationActing = false;
|
|
2995
|
+
Panic.EndActuation();
|
|
2996
|
+
}
|
|
2997
|
+
}
|
|
2998
|
+
static void PrepareActuation(string op, Dictionary<string, object> request)
|
|
2999
|
+
{
|
|
3000
|
+
object raw;
|
|
3001
|
+
var args = request.TryGetValue("args", out raw) ? raw as Dictionary<string, object> : null;
|
|
3002
|
+
string series = args == null ? "" : GetS(args, "__inputSeries", "");
|
|
3003
|
+
if (!string.IsNullOrEmpty(series) && series == _lastAnnouncedSeries &&
|
|
3004
|
+
_announcedHumanRevision == Panic.HumanRevision)
|
|
3005
|
+
{
|
|
3006
|
+
if (_announcedInputEpoch != Panic.InputEpoch)
|
|
3007
|
+
throw InputInterrupted("HUMAN_REOBSERVE", "The user changed the desktop between batch steps. Obtain fresh target evidence and continue the remaining task without replaying completed input.");
|
|
3008
|
+
_preparedInputEpoch = Panic.InputEpoch; return;
|
|
3009
|
+
}
|
|
3010
|
+
Panic.Check(op); CheckHumanInput();
|
|
3011
|
+
long inputEpoch = Panic.InputEpoch;
|
|
3012
|
+
IntPtr foreground = PreparationForeground();
|
|
3013
|
+
Glow.Prepare(true);
|
|
3014
|
+
try
|
|
3015
|
+
{
|
|
3016
|
+
var startup = Stopwatch.StartNew();
|
|
3017
|
+
while (!Glow.PreparationVisible)
|
|
3018
|
+
{
|
|
3019
|
+
Panic.Check(op); CheckHumanInput();
|
|
3020
|
+
if (startup.ElapsedMilliseconds >= 5000)
|
|
3021
|
+
throw InputInterrupted("PREPARATION_UNAVAILABLE", "The preparation notice could not be displayed; no input was dispatched.");
|
|
3022
|
+
Thread.Sleep(10);
|
|
3023
|
+
}
|
|
3024
|
+
var timer = Stopwatch.StartNew();
|
|
3025
|
+
while (timer.ElapsedMilliseconds < Glow.PreparationMs)
|
|
3026
|
+
{
|
|
3027
|
+
Panic.Check(op); CheckHumanInput();
|
|
3028
|
+
int remaining = Glow.PreparationMs - (int)timer.ElapsedMilliseconds;
|
|
3029
|
+
if (remaining > 0) Thread.Sleep(Math.Min(25, remaining));
|
|
3030
|
+
}
|
|
3031
|
+
Panic.Check(op); CheckHumanInput();
|
|
3032
|
+
if (inputEpoch != Panic.InputEpoch || foreground != PreparationForeground())
|
|
3033
|
+
throw InputInterrupted("HUMAN_REOBSERVE", "The user changed the desktop during the preparation cue. No action was dispatched. Preserve the task, obtain fresh target evidence, and continue the authorized task immediately; this is not cancellation or abandonment.");
|
|
3034
|
+
_lastAnnouncedSeries = series; _announcedHumanRevision = Panic.HumanRevision;
|
|
3035
|
+
_announcedInputEpoch = _preparedInputEpoch = inputEpoch;
|
|
3036
|
+
}
|
|
3037
|
+
finally { Glow.Prepare(false); }
|
|
2874
3038
|
}
|
|
2875
3039
|
public static bool CancelInput(int requestId)
|
|
2876
3040
|
{
|
|
@@ -2912,6 +3076,7 @@ namespace DshComputerUse.Worker
|
|
|
2912
3076
|
// cross-process STOP may hold SyncLock while releasing owned input.
|
|
2913
3077
|
if (Panic.Engaged || Panic.Stopped || Panic.Exited)
|
|
2914
3078
|
throw InputInterrupted("ABORTED", "manual pause or exit owns the desktop; no further input will be sent.");
|
|
3079
|
+
BeginActuation();
|
|
2915
3080
|
if (inputs.Length == 1)
|
|
2916
3081
|
{
|
|
2917
3082
|
bool isUp; INPUT release; int physical;
|
|
@@ -3218,7 +3383,11 @@ namespace DshComputerUse.Worker
|
|
|
3218
3383
|
{
|
|
3219
3384
|
var request = Json.Deserialize<Dictionary<string, object>>(line);
|
|
3220
3385
|
string op = GetS(request, "op", "");
|
|
3221
|
-
|
|
3386
|
+
object rawArgs;
|
|
3387
|
+
var args = request.TryGetValue("args", out rawArgs) ? rawArgs as Dictionary<string, object> : null;
|
|
3388
|
+
bool input = ActOps.Contains(op) || op == "shiftClick" || op == "selftest" ||
|
|
3389
|
+
(op == "focus" && args != null && GetB(args, "refocus", false));
|
|
3390
|
+
EnsureDesktopOwnership(op);
|
|
3222
3391
|
int requestId = GetI(request, "id", 0);
|
|
3223
3392
|
bool ignored;
|
|
3224
3393
|
_activeRequestId = requestId;
|
|
@@ -3240,8 +3409,13 @@ namespace DshComputerUse.Worker
|
|
|
3240
3409
|
}
|
|
3241
3410
|
finally
|
|
3242
3411
|
{
|
|
3243
|
-
if (input)
|
|
3412
|
+
if (input)
|
|
3413
|
+
{
|
|
3414
|
+
try { ReleaseOwnedInputs(); }
|
|
3415
|
+
finally { _operationActing = false; Panic.EndActuation(); Glow.Prepare(false); }
|
|
3416
|
+
}
|
|
3244
3417
|
_operationLive = false; _activeRequestId = 0;
|
|
3418
|
+
if (_releaseDesktopPending) ReleaseDesktopOwnership();
|
|
3245
3419
|
_lastCompletedRequestId = Math.Max(_lastCompletedRequestId, requestId);
|
|
3246
3420
|
CancelledRequests.TryRemove(requestId, out ignored);
|
|
3247
3421
|
}
|
|
@@ -3257,6 +3431,7 @@ namespace DshComputerUse.Worker
|
|
|
3257
3431
|
Dictionary<string, object> a = (req.TryGetValue("args", out av) && av is Dictionary<string, object>) ? (Dictionary<string, object>)av : new Dictionary<string, object>();
|
|
3258
3432
|
bool cycleOp = GetB(a, "__cycle", false) && op != null &&
|
|
3259
3433
|
op != "abort" && op != "ping" && op != "echoargs" &&
|
|
3434
|
+
op != "probe" && op != "keyClass" && op != "stuckPlan" &&
|
|
3260
3435
|
op != "indicator" && op != "calm" && op != "panic" && op != "resume" && op != "recover" &&
|
|
3261
3436
|
// ... AND THE CYCLE HAS TO BE ALIVE TO BE LIT (2026-09-13). "Lit by any call" is how
|
|
3262
3437
|
// a DEAD cycle came back as a cyan breathing border: after Ctrl+Alt+Q ended the
|
|
@@ -3281,6 +3456,10 @@ namespace DshComputerUse.Worker
|
|
|
3281
3456
|
// and is therefore treated as the observation it really is.
|
|
3282
3457
|
Panic.CheckOp((op == "click" || op == "move" || op == "type" || op == "key") && GetB(a, "dryRun", false) ? "probe" : op);
|
|
3283
3458
|
|
|
3459
|
+
if ((ActOps.Contains(op) || op == "shiftClick" || op == "selftest" ||
|
|
3460
|
+
(op == "focus" && GetB(a, "refocus", false))) && !GetB(a, "dryRun", false))
|
|
3461
|
+
PrepareActuation(op, req);
|
|
3462
|
+
|
|
3284
3463
|
switch (op)
|
|
3285
3464
|
{
|
|
3286
3465
|
// THE QUESTION PATH IS TWO OPS NOW (merge-design.md §2.3): `beginAsk` engages the
|
|
@@ -3399,8 +3578,9 @@ namespace DshComputerUse.Worker
|
|
|
3399
3578
|
pr["glow"] = Dict(
|
|
3400
3579
|
"want", Glow.Want,
|
|
3401
3580
|
// Is a real injection sequence in flight RIGHT NOW? The ACTING light is driven
|
|
3402
|
-
// by
|
|
3581
|
+
// by the actual action scope, so both are reported as observable facts.
|
|
3403
3582
|
"injecting", Glow.Injecting,
|
|
3583
|
+
"acting", Glow.Acting, "preparing", Glow.Preparing, "preparationMs", Glow.PreparationMs,
|
|
3404
3584
|
"title", Glow.ShownTitle == null ? "" : Glow.ShownTitle);
|
|
3405
3585
|
// EVENTS THE OS ACTUALLY INSERTED (not the plan): 0 after a plain pause with
|
|
3406
3586
|
// nothing held is the observable form of "no ghost right-click / Alt menu".
|
|
@@ -3417,9 +3597,9 @@ namespace DshComputerUse.Worker
|
|
|
3417
3597
|
// like the emergency brake. Called when the agent's turn ends, so a lingering blue
|
|
3418
3598
|
// border can never claim the machine is still being driven after control stopped.
|
|
3419
3599
|
case "calm":
|
|
3420
|
-
Panic.
|
|
3421
|
-
|
|
3422
|
-
|
|
3600
|
+
if (!Panic.TryEndCycle())
|
|
3601
|
+
return Ok(Dict("released", false, "reason", "A manual pause or question still owns this cycle; its brake and ownership were preserved."));
|
|
3602
|
+
return Ok(Dict("released", true, "indicator", "off", "hard", true));
|
|
3423
3603
|
case "focus": return Ok(FocusReport(a, GetB(a, "refocus", false)));
|
|
3424
3604
|
case "frameSig": return Ok(FrameSigOp(a));
|
|
3425
3605
|
// `source=agent` (not "unknown"): this op exists ONLY for the agent's own `stop`, and a
|
|
@@ -3706,6 +3886,7 @@ namespace DshComputerUse.Worker
|
|
|
3706
3886
|
// ACTING light on it, and clear the mark in a finally so the blue can never outlive the
|
|
3707
3887
|
// input it is describing.
|
|
3708
3888
|
CheckHumanInput();
|
|
3889
|
+
BeginActuation();
|
|
3709
3890
|
Glow.InputBegin();
|
|
3710
3891
|
try
|
|
3711
3892
|
{
|
|
@@ -3921,6 +4102,7 @@ namespace DshComputerUse.Worker
|
|
|
3921
4102
|
SendMouse(down, 0, 0, 0); Thread.Sleep(30); SendMouse(up, 0, 0, 0);
|
|
3922
4103
|
if (i < clicks - 1) Thread.Sleep(gap);
|
|
3923
4104
|
}
|
|
4105
|
+
EndActuationForReadback();
|
|
3924
4106
|
double changedPct = -1;
|
|
3925
4107
|
if (verify && before != null)
|
|
3926
4108
|
{
|
|
@@ -4047,16 +4229,8 @@ namespace DshComputerUse.Worker
|
|
|
4047
4229
|
{
|
|
4048
4230
|
_lastRefocus = EnsureTargetForeground();
|
|
4049
4231
|
}
|
|
4050
|
-
//
|
|
4051
|
-
//
|
|
4052
|
-
// 1200 ms, not 30000 (2026-09-13). The hold is how long the DEEP BLUE "acting" border
|
|
4053
|
-
// stays after an input injection — and 30 s meant a 50 ms click painted half a minute of
|
|
4054
|
-
// "I am driving the machine", during which the cyan "thinking" state could never show
|
|
4055
|
-
// (the render loop shows acting in preference to thinking). The human watched the blue
|
|
4056
|
-
// light sit still through a whole page read and asked, correctly, why. Blue now means
|
|
4057
|
-
// "input, right now, plus a short tail"; cyan is what a reading agent looks like.
|
|
4058
|
-
int hold = GetI(a, "indicatorMs", 1200);
|
|
4059
|
-
Glow.Touch(hold);
|
|
4232
|
+
// Validation and focus readback do not own input. The first real mutation
|
|
4233
|
+
// or SendInput opens the actual scope and its deep-blue cue.
|
|
4060
4234
|
}
|
|
4061
4235
|
|
|
4062
4236
|
static Dictionary<string, object> Indicator(Dictionary<string, object> a)
|
|
@@ -4665,6 +4839,7 @@ namespace DshComputerUse.Worker
|
|
|
4665
4839
|
KeyboardChord.Run(vks, SendKey, ReleaseKeyForCleanup,
|
|
4666
4840
|
delegate(ushort vk) { return (Native.GetAsyncKeyState(vk) & 0x8000) != 0; },
|
|
4667
4841
|
delegate { WaitForKeyHold(Math.Max(20, GetI(a, "holdMs", 60))); });
|
|
4842
|
+
EndActuationForReadback();
|
|
4668
4843
|
Thread.Sleep(30);
|
|
4669
4844
|
Dictionary<string, object> keyOut = Dict("combo", combo, "focus", focus);
|
|
4670
4845
|
if (_lastRefocus != null) keyOut["refocused"] = _lastRefocus;
|
|
@@ -5550,8 +5725,8 @@ namespace DshComputerUse.Worker
|
|
|
5550
5725
|
{
|
|
5551
5726
|
CheckHumanInput();
|
|
5552
5727
|
if (h == IntPtr.Zero || !Native.IsWindow(h)) return null;
|
|
5553
|
-
if (!Native.IsWindowVisible(h)) Native.ShowWindow(h, Native.SW_SHOWNOACTIVATE);
|
|
5554
|
-
if (Native.IsIconic(h)) Native.ShowWindow(h, Native.SW_RESTORE);
|
|
5728
|
+
if (!Native.IsWindowVisible(h)) { MarkMutation(); Native.ShowWindow(h, Native.SW_SHOWNOACTIVATE); }
|
|
5729
|
+
if (Native.IsIconic(h)) { MarkMutation(); Native.ShowWindow(h, Native.SW_RESTORE); }
|
|
5555
5730
|
Thread.Sleep(40);
|
|
5556
5731
|
CheckHumanInput();
|
|
5557
5732
|
if (IsForeground(h)) return "already-foreground";
|
|
@@ -6018,8 +6193,7 @@ namespace DshComputerUse.Worker
|
|
|
6018
6193
|
", automationId=" + (autoId ?? "-") + ", role=" + (role ?? "-") +
|
|
6019
6194
|
", index=" + index + ") among " + seen + " elements");
|
|
6020
6195
|
|
|
6021
|
-
//
|
|
6022
|
-
// fight, so the quiet gate is skipped on purpose.
|
|
6196
|
+
// A UIA mutation shares the same actual-action boundary and takeover checks.
|
|
6023
6197
|
// A selector WITHOUT hwnd searches the whole desktop, so there the element that
|
|
6024
6198
|
// actually matched decides the victim - and a host control is never driven through
|
|
6025
6199
|
// its automation pattern either.
|
|
@@ -6028,11 +6202,9 @@ namespace DshComputerUse.Worker
|
|
|
6028
6202
|
HostGuardPid(foundPid, "uiaAct");
|
|
6029
6203
|
Panic.Check("uiaAct");
|
|
6030
6204
|
CheckHumanInput();
|
|
6031
|
-
Glow.Touch(GetI(a, "indicatorMs", 1200)); // same reason: see AgentActing
|
|
6032
6205
|
|
|
6033
6206
|
object pat = null;
|
|
6034
6207
|
string via = null;
|
|
6035
|
-
MarkMutation();
|
|
6036
6208
|
switch (action)
|
|
6037
6209
|
{
|
|
6038
6210
|
case "invoke":
|
|
@@ -6064,6 +6236,7 @@ namespace DshComputerUse.Worker
|
|
|
6064
6236
|
default:
|
|
6065
6237
|
throw new Exception("action must be invoke|select|expand|collapse|toggle|setValue|focus|scrollIntoView");
|
|
6066
6238
|
}
|
|
6239
|
+
EndActuationForReadback();
|
|
6067
6240
|
if (via == null)
|
|
6068
6241
|
throw new Exception("element does not support '" + action + "': " + ElemDict(found)["name"]);
|
|
6069
6242
|
|
|
@@ -6373,3 +6546,79 @@ namespace DshComputerUse.Worker
|
|
|
6373
6546
|
static void WriteLine(string s) { lock (OutLock) { Console.Out.WriteLine(s); Console.Out.Flush(); } }
|
|
6374
6547
|
}
|
|
6375
6548
|
}
|
|
6549
|
+
|
|
6550
|
+
// Integrated verbatim into worker.cs by the native worker build. Tests compile this
|
|
6551
|
+
// production class without starting hooks, overlays or desktop input.
|
|
6552
|
+
namespace DshComputerUse.Worker
|
|
6553
|
+
{
|
|
6554
|
+
public sealed class DesktopOwnershipLease : System.IDisposable
|
|
6555
|
+
{
|
|
6556
|
+
readonly object sync = new object();
|
|
6557
|
+
readonly string path;
|
|
6558
|
+
System.IO.FileStream stream;
|
|
6559
|
+
public DesktopOwnershipLease(string file) { path = file; }
|
|
6560
|
+
public bool Owned { get { lock (sync) { return stream != null; } } }
|
|
6561
|
+
public static string DefaultPath()
|
|
6562
|
+
{
|
|
6563
|
+
string isolated = System.Environment.GetEnvironmentVariable("DSH_COMPUTER_USE_OWNER_FILE");
|
|
6564
|
+
if (!string.IsNullOrEmpty(isolated)) return isolated;
|
|
6565
|
+
return System.IO.Path.Combine(System.Environment.GetFolderPath(System.Environment.SpecialFolder.LocalApplicationData),
|
|
6566
|
+
"dsh-computer-use", "desktop-" + System.Diagnostics.Process.GetCurrentProcess().SessionId + ".owner");
|
|
6567
|
+
}
|
|
6568
|
+
public bool TryAcquire(string owner, out string occupiedBy)
|
|
6569
|
+
{
|
|
6570
|
+
lock (sync)
|
|
6571
|
+
{
|
|
6572
|
+
occupiedBy = "";
|
|
6573
|
+
if (stream != null) return true;
|
|
6574
|
+
System.IO.Directory.CreateDirectory(System.IO.Path.GetDirectoryName(System.IO.Path.GetFullPath(path)));
|
|
6575
|
+
System.IO.FileStream opened;
|
|
6576
|
+
try
|
|
6577
|
+
{
|
|
6578
|
+
// A persistent handle is the lock. Never delete the pathname, use a
|
|
6579
|
+
// timer, or steal based on PID: Windows releases it when the process dies.
|
|
6580
|
+
opened = new System.IO.FileStream(path, System.IO.FileMode.OpenOrCreate,
|
|
6581
|
+
System.IO.FileAccess.ReadWrite, System.IO.FileShare.Read);
|
|
6582
|
+
}
|
|
6583
|
+
catch (System.IO.IOException error)
|
|
6584
|
+
{
|
|
6585
|
+
int code = System.Runtime.InteropServices.Marshal.GetHRForException(error) & 0xFFFF;
|
|
6586
|
+
if (code != 32 && code != 33) throw;
|
|
6587
|
+
occupiedBy = ReadOwner();
|
|
6588
|
+
return false;
|
|
6589
|
+
}
|
|
6590
|
+
try
|
|
6591
|
+
{
|
|
6592
|
+
byte[] record = System.Text.Encoding.UTF8.GetBytes("pid=" + System.Diagnostics.Process.GetCurrentProcess().Id +
|
|
6593
|
+
"\nowner=" + (owner ?? "worker").Replace("\r", " ").Replace("\n", " ") + "\n");
|
|
6594
|
+
opened.SetLength(0); opened.Write(record, 0, record.Length); opened.Flush();
|
|
6595
|
+
stream = opened;
|
|
6596
|
+
return true;
|
|
6597
|
+
}
|
|
6598
|
+
catch { opened.Dispose(); throw; }
|
|
6599
|
+
}
|
|
6600
|
+
}
|
|
6601
|
+
string ReadOwner()
|
|
6602
|
+
{
|
|
6603
|
+
try
|
|
6604
|
+
{
|
|
6605
|
+
using (var input = new System.IO.FileStream(path, System.IO.FileMode.Open,
|
|
6606
|
+
System.IO.FileAccess.Read, System.IO.FileShare.ReadWrite))
|
|
6607
|
+
using (var reader = new System.IO.StreamReader(input))
|
|
6608
|
+
{
|
|
6609
|
+
char[] buffer = new char[512];
|
|
6610
|
+
return new string(buffer, 0, reader.Read(buffer, 0, buffer.Length)).Trim();
|
|
6611
|
+
}
|
|
6612
|
+
}
|
|
6613
|
+
catch { return "another live computer-use worker"; }
|
|
6614
|
+
}
|
|
6615
|
+
public void Dispose()
|
|
6616
|
+
{
|
|
6617
|
+
lock (sync)
|
|
6618
|
+
{
|
|
6619
|
+
if (stream == null) return;
|
|
6620
|
+
stream.Dispose(); stream = null;
|
|
6621
|
+
}
|
|
6622
|
+
}
|
|
6623
|
+
}
|
|
6624
|
+
}
|