mindweave 3.2.2 → 3.2.3
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/dist/cli/App.js +2 -2
- package/dist/cli/App.js.map +1 -1
- package/dist/cli/agentLines.js +196 -0
- package/dist/cli/agentLines.js.map +1 -0
- package/dist/cli/components/BlockView.js +1 -1
- package/dist/cli/components/BlockView.js.map +1 -1
- package/dist/cli/components/SubagentView.js +43 -74
- package/dist/cli/components/SubagentView.js.map +1 -1
- package/dist/cli/help.js +3 -1
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/transcript.js +13 -3
- package/dist/cli/transcript.js.map +1 -1
- package/dist/core/turnRunner.js +2 -2
- package/dist/core/turnRunner.js.map +1 -1
- package/dist/dynamo/engine.js +6 -3
- package/dist/dynamo/engine.js.map +1 -1
- package/dist/dynamo/guard.js +6 -0
- package/dist/dynamo/guard.js.map +1 -1
- package/dist/dynamo/marathon.js +17 -4
- package/dist/dynamo/marathon.js.map +1 -1
- package/dist/memory/compaction.js +60 -6
- package/dist/memory/compaction.js.map +1 -1
- package/dist/memory/session.js +2 -0
- package/dist/memory/session.js.map +1 -1
- package/dist/tools/guard.js +13 -0
- package/dist/tools/guard.js.map +1 -1
- package/dist/tools/registry.js +1 -1
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/runCommand.js +9 -0
- package/dist/tools/runCommand.js.map +1 -1
- package/dist/tools/screenshot.js +28 -9
- package/dist/tools/screenshot.js.map +1 -1
- package/dist/tools/screenshotWin.js +258 -175
- package/dist/tools/screenshotWin.js.map +1 -1
- package/dist/tools/subagent.js +55 -27
- package/dist/tools/subagent.js.map +1 -1
- package/dist/tools/subagentOutcome.js +24 -0
- package/dist/tools/subagentOutcome.js.map +1 -0
- package/dist/tools/treeGuard.js +164 -0
- package/dist/tools/treeGuard.js.map +1 -0
- package/dist/tools/ui.js +27 -0
- package/dist/tools/ui.js.map +1 -1
- package/dist/tools/uiHide.js +19 -6
- package/dist/tools/uiHide.js.map +1 -1
- package/dist/tools/verifierCommands.js +188 -0
- package/dist/tools/verifierCommands.js.map +1 -0
- package/dist/tools/verifierRun.js +88 -0
- package/dist/tools/verifierRun.js.map +1 -0
- package/dist/tools/verifierTools.js +19 -0
- package/dist/tools/verifierTools.js.map +1 -0
- package/dist/tools/verifyReport.js +79 -9
- package/dist/tools/verifyReport.js.map +1 -1
- package/package.json +3 -3
package/dist/tools/screenshot.js
CHANGED
|
@@ -58,6 +58,11 @@ export function pickWindow(query, windows) {
|
|
|
58
58
|
return { kind: "none", candidates: [] };
|
|
59
59
|
if (!query) {
|
|
60
60
|
const focused = windows.find((w) => w.foreground);
|
|
61
|
+
// The focused window is the one the user is typing into, and while the agent works that is the terminal
|
|
62
|
+
// Mindweave runs in. "Whichever has focus" then photographs the conversation itself, which is never what
|
|
63
|
+
// was meant; an app was asked for by name, or was focused on purpose.
|
|
64
|
+
if (focused?.host)
|
|
65
|
+
return { kind: "host", window: focused, candidates: windows.filter((w) => !w.host) };
|
|
61
66
|
return focused
|
|
62
67
|
? { kind: "match", window: focused, tied: windows.filter((w) => w !== focused) }
|
|
63
68
|
: { kind: "ambiguous", candidates: windows };
|
|
@@ -123,12 +128,22 @@ export function ambiguousMessage(query, candidates) {
|
|
|
123
128
|
return (`"${query}" matches ${candidates.length} windows, so it is not clear which to capture. ` +
|
|
124
129
|
`Name one more precisely:\n${listTitles(candidates)}`);
|
|
125
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* What to say when the focused window is the one Mindweave runs in (pure). It is the answer to a
|
|
133
|
+
* call with no title while the user is looking at the terminal, which is nearly every call: the model
|
|
134
|
+
* is told what the window is, that nothing was captured, and how to name the app instead.
|
|
135
|
+
*/
|
|
136
|
+
export function hostMessage(host, others) {
|
|
137
|
+
return (`The focused window is the one Mindweave itself runs in ("${host.title}"), so nothing was captured: a picture of ` +
|
|
138
|
+
`it cannot tell you anything about the app you are testing. Name the app's window by part of its title. ` +
|
|
139
|
+
`A window with a blank title bar is listed by its process name, such as "electron".\n${listTitles(others)}`);
|
|
140
|
+
}
|
|
126
141
|
/** Render window titles for an error the model can act on without another call. */
|
|
127
142
|
export function listTitles(windows) {
|
|
128
143
|
if (windows.length === 0)
|
|
129
144
|
return "No capturable windows are open.";
|
|
130
145
|
const shown = windows.slice(0, MAX_LISTED);
|
|
131
|
-
const lines = shown.map((w) => ` - ${w.title}${w.foreground ? " (focused)" : ""}`);
|
|
146
|
+
const lines = shown.map((w) => ` - ${w.title}${w.foreground ? " (focused)" : ""}${w.host ? " (where Mindweave runs)" : ""}`);
|
|
132
147
|
const hidden = windows.length - shown.length;
|
|
133
148
|
if (hidden > 0)
|
|
134
149
|
lines.push(` … and ${hidden} more`);
|
|
@@ -156,10 +171,11 @@ export const screenshot = {
|
|
|
156
171
|
description: "Take a picture of one open window and look at it. Use it to check that an app " +
|
|
157
172
|
"actually came up and renders correctly — a running process is not proof of that — " +
|
|
158
173
|
"or to see a layout, a chart, or an error dialog for yourself. " +
|
|
159
|
-
"Pass `window` with part of the window's title
|
|
160
|
-
"
|
|
161
|
-
"
|
|
162
|
-
"
|
|
174
|
+
"Pass `window` with part of the window's title. An app with a custom title bar that shows a blank " +
|
|
175
|
+
"OS title (common for desktop apps) is listed by its process name, such as \"electron\": use that. " +
|
|
176
|
+
"Leave `window` out only to capture the window the user has focused on purpose; the window " +
|
|
177
|
+
"Mindweave itself runs in is never captured that way, because while you work it is almost always the " +
|
|
178
|
+
"one with focus and a picture of it shows you nothing about the app. " +
|
|
163
179
|
"It photographs whatever that window is showing, so call it when looking will " +
|
|
164
180
|
"genuinely tell you something, not as a routine check. " +
|
|
165
181
|
"Windows only, one window at a time — the whole screen is never captured. " +
|
|
@@ -171,8 +187,8 @@ export const screenshot = {
|
|
|
171
187
|
properties: {
|
|
172
188
|
window: {
|
|
173
189
|
type: "string",
|
|
174
|
-
description: "Part of the target window's title, e.g. \"Vite\" or \"localhost\"
|
|
175
|
-
"Omit to capture
|
|
190
|
+
description: "Part of the target window's title, e.g. \"Vite\" or \"localhost\", or the process name for a window " +
|
|
191
|
+
"with a blank title (\"electron\"). Omit only to capture a window the user has focused on purpose.",
|
|
176
192
|
},
|
|
177
193
|
},
|
|
178
194
|
},
|
|
@@ -209,14 +225,17 @@ export const screenshot = {
|
|
|
209
225
|
? `No open window's title contains "${query}". ${listTitles(pick.candidates)}\n` +
|
|
210
226
|
`Three things to try, in order: if you just launched the app, its window may need a ` +
|
|
211
227
|
`moment — call again. If it has a custom title bar (many desktop apps, and Tauri or ` +
|
|
212
|
-
`Electron with window decorations off, show a blank OS title),
|
|
213
|
-
`
|
|
228
|
+
`Electron with window decorations off, show a blank OS title), it is listed by its ` +
|
|
229
|
+
`process name: use that. Or name one of the windows listed above. ` +
|
|
214
230
|
`Do NOT capture a different named window as a stand-in: a picture of something else ` +
|
|
215
231
|
`cannot tell you anything about "${query}".`
|
|
216
232
|
: `There is no window to capture. ${listTitles(pick.candidates)}`);
|
|
217
233
|
}
|
|
218
234
|
if (pick.kind === "ambiguous")
|
|
219
235
|
return fail(ambiguousMessage(query, pick.candidates));
|
|
236
|
+
// Refused before anyone is asked: the user is not asked to approve a picture that was never going to help.
|
|
237
|
+
if (pick.kind === "host")
|
|
238
|
+
return fail(hostMessage(pick.window, pick.candidates));
|
|
220
239
|
const target = pick.window;
|
|
221
240
|
// The window is resolved BEFORE this either way, so the user is never asked about a
|
|
222
241
|
// capture that was never going to happen.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"screenshot.js","sourceRoot":"","sources":["../../src/tools/screenshot.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAC3C,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACxC,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AACjC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AACjE,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,WAAW,EAAmB,MAAM,oBAAoB,CAAC;AAEjF,oFAAoF;AACpF,MAAM,UAAU,GAAG,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"screenshot.js","sourceRoot":"","sources":["../../src/tools/screenshot.ts"],"names":[],"mappings":"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"}
|
|
@@ -42,176 +42,221 @@ import { tmpdir } from "node:os";
|
|
|
42
42
|
import { join } from "node:path";
|
|
43
43
|
import { DESK_CS, HIDDEN_MARK, hiddenDesktopName, windowArgs } from "./hiddenDesktop.js";
|
|
44
44
|
const PS_TIMEOUT_MS = 20_000;
|
|
45
|
-
const SCRIPT = String.raw `
|
|
46
|
-
param([string]$Mode = "list", [string]$Handle = "", [string]$Out = "", [string]$Desktop = "", [string]$Hidden = "")
|
|
47
|
-
$ErrorActionPreference = "Stop"
|
|
48
|
-
Add-Type -AssemblyName System.Drawing
|
|
49
|
-
|
|
50
|
-
Add-Type -ReferencedAssemblies System.Drawing -TypeDefinition @"
|
|
51
|
-
using System;
|
|
52
|
-
using System.Collections.Generic;
|
|
53
|
-
using System.Drawing;
|
|
54
|
-
using System.Drawing.Imaging;
|
|
55
|
-
using System.Runtime.InteropServices;
|
|
56
|
-
using System.Text;
|
|
57
|
-
|
|
58
|
-
public static class MwCapture {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
[DllImport("user32.dll"
|
|
64
|
-
[DllImport("user32.dll"
|
|
65
|
-
[DllImport("user32.dll")] static extern bool
|
|
66
|
-
[DllImport("user32.dll")] static extern
|
|
67
|
-
[DllImport("user32.dll")] static extern
|
|
68
|
-
[DllImport("user32.dll")]
|
|
69
|
-
[DllImport("user32.dll")] static extern
|
|
70
|
-
[DllImport("user32.dll")] static extern
|
|
71
|
-
[DllImport("
|
|
72
|
-
[DllImport("
|
|
73
|
-
|
|
74
|
-
[
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
static
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
//
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
static
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
45
|
+
const SCRIPT = String.raw `
|
|
46
|
+
param([string]$Mode = "list", [string]$Handle = "", [string]$Out = "", [string]$Desktop = "", [string]$Hidden = "", [int]$Owner = 0)
|
|
47
|
+
$ErrorActionPreference = "Stop"
|
|
48
|
+
Add-Type -AssemblyName System.Drawing
|
|
49
|
+
|
|
50
|
+
Add-Type -ReferencedAssemblies System.Drawing -TypeDefinition @"
|
|
51
|
+
using System;
|
|
52
|
+
using System.Collections.Generic;
|
|
53
|
+
using System.Drawing;
|
|
54
|
+
using System.Drawing.Imaging;
|
|
55
|
+
using System.Runtime.InteropServices;
|
|
56
|
+
using System.Text;
|
|
57
|
+
|
|
58
|
+
public static class MwCapture {
|
|
59
|
+
// The processes this one runs inside (the terminal, its console host, the app hosting it).
|
|
60
|
+
// Their windows are marked "host" in the list, so the caller can tell them from the app under test.
|
|
61
|
+
public static HashSet<uint> HostPids = new HashSet<uint>();
|
|
62
|
+
delegate bool EnumProc(IntPtr hWnd, IntPtr lParam);
|
|
63
|
+
[DllImport("user32.dll")] static extern bool EnumWindows(EnumProc cb, IntPtr l);
|
|
64
|
+
[DllImport("user32.dll")] static extern bool IsWindowVisible(IntPtr h);
|
|
65
|
+
[DllImport("user32.dll")] static extern bool IsIconic(IntPtr h);
|
|
66
|
+
[DllImport("user32.dll", CharSet = CharSet.Unicode)] static extern int GetWindowTextLength(IntPtr h);
|
|
67
|
+
[DllImport("user32.dll", CharSet = CharSet.Unicode)] static extern int GetWindowText(IntPtr h, StringBuilder s, int n);
|
|
68
|
+
[DllImport("user32.dll")] static extern bool PrintWindow(IntPtr h, IntPtr hdc, uint flags);
|
|
69
|
+
[DllImport("user32.dll")] static extern bool SetProcessDPIAware();
|
|
70
|
+
[DllImport("user32.dll")] static extern bool GetWindowRect(IntPtr h, out RECT r);
|
|
71
|
+
[DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
|
|
72
|
+
[DllImport("user32.dll")] static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);
|
|
73
|
+
[DllImport("user32.dll")] static extern int GetWindowLong(IntPtr h, int index);
|
|
74
|
+
[DllImport("dwmapi.dll")] static extern int DwmGetWindowAttribute(IntPtr h, int attr, out RECT r, int size);
|
|
75
|
+
[DllImport("dwmapi.dll", EntryPoint = "DwmGetWindowAttribute")] static extern int DwmGetIntAttribute(IntPtr h, int attr, out int v, int size);
|
|
76
|
+
|
|
77
|
+
[StructLayout(LayoutKind.Sequential)] public struct RECT { public int Left, Top, Right, Bottom; }
|
|
78
|
+
|
|
79
|
+
public static void Dpi() { SetProcessDPIAware(); }
|
|
80
|
+
|
|
81
|
+
[DllImport("kernel32.dll")] static extern IntPtr GetConsoleWindow();
|
|
82
|
+
[DllImport("user32.dll")] static extern IntPtr GetAncestor(IntPtr h, uint flags);
|
|
83
|
+
|
|
84
|
+
// The window the console this process is attached to is drawn in. In Windows Terminal the console
|
|
85
|
+
// window is a hidden stand-in owned by the real top-level window, so the owner is what is returned.
|
|
86
|
+
// Empty when there is no console (the desktop app, a service).
|
|
87
|
+
public static string HostWindow() {
|
|
88
|
+
IntPtr c = GetConsoleWindow();
|
|
89
|
+
if (c == IntPtr.Zero) return "";
|
|
90
|
+
IntPtr root = GetAncestor(c, 3 /* GA_ROOTOWNER */);
|
|
91
|
+
return (root == IntPtr.Zero ? c : root).ToInt64().ToString();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// A DWM-cloaked window passes IsWindowVisible but is not actually on screen — the
|
|
95
|
+
// invisible UWP and host windows that would otherwise flood the list. Any failure to
|
|
96
|
+
// read the attribute is treated as not-cloaked, so a window is never dropped on error.
|
|
97
|
+
static bool Cloaked(IntPtr h) {
|
|
98
|
+
int v;
|
|
99
|
+
return DwmGetIntAttribute(h, 14 /* DWMWA_CLOAKED */, out v, sizeof(int)) == 0 && v != 0;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// The Shell's own "is this an Alt-Tab window" test: a tool window is a palette or
|
|
103
|
+
// tray surface and is never what "the app" means, unless it also asks for the taskbar.
|
|
104
|
+
static bool AltTabWorthy(IntPtr h) {
|
|
105
|
+
int ex = GetWindowLong(h, -20 /* GWL_EXSTYLE */);
|
|
106
|
+
bool tool = (ex & 0x00000080) != 0; // WS_EX_TOOLWINDOW
|
|
107
|
+
bool app = (ex & 0x00040000) != 0; // WS_EX_APPWINDOW
|
|
108
|
+
return !tool || app;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// The window's process name, for a window that has no title of its own. Wrapped
|
|
112
|
+
// because the process can exit between the enum and the lookup.
|
|
113
|
+
static string ProcName(IntPtr h) {
|
|
114
|
+
try {
|
|
115
|
+
uint pid;
|
|
116
|
+
GetWindowThreadProcessId(h, out pid);
|
|
117
|
+
if (pid == 0) return "";
|
|
118
|
+
using (var p = System.Diagnostics.Process.GetProcessById((int)pid)) return p.ProcessName;
|
|
119
|
+
} catch { return ""; }
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// DWMWA_EXTENDED_FRAME_BOUNDS. GetWindowRect includes an invisible border on
|
|
123
|
+
// Windows 10+, so it is the fallback rather than the first choice.
|
|
124
|
+
static bool Bounds(IntPtr h, out RECT r) {
|
|
125
|
+
if (DwmGetWindowAttribute(h, 9, out r, Marshal.SizeOf(typeof(RECT))) == 0
|
|
126
|
+
&& r.Right > r.Left && r.Bottom > r.Top) return true;
|
|
127
|
+
return GetWindowRect(h, out r);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// The hidden desktop's windows are marked, so a capture is sent back there.
|
|
131
|
+
public static string ListOn(string desk) {
|
|
132
|
+
return MwDesk.On(desk, () => string.Join("\n", List("${HIDDEN_MARK}").ToArray()));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
public static List<string> List(string mark) {
|
|
136
|
+
var found = new List<string>();
|
|
137
|
+
IntPtr fg = GetForegroundWindow();
|
|
138
|
+
EnumWindows(delegate(IntPtr h, IntPtr l) {
|
|
139
|
+
if (!IsWindowVisible(h) || IsIconic(h)) return true;
|
|
140
|
+
if (Cloaked(h) || !AltTabWorthy(h)) return true;
|
|
141
|
+
RECT r;
|
|
142
|
+
if (!Bounds(h, out r)) return true;
|
|
143
|
+
// Tool windows and tray hosts are real but never what anyone means.
|
|
144
|
+
if (r.Right - r.Left < 120 || r.Bottom - r.Top < 120) return true;
|
|
145
|
+
int len = GetWindowTextLength(h);
|
|
146
|
+
string title = "";
|
|
147
|
+
if (len > 0) {
|
|
148
|
+
var sb = new StringBuilder(len + 1);
|
|
149
|
+
GetWindowText(h, sb, sb.Capacity);
|
|
150
|
+
title = sb.ToString().Trim();
|
|
151
|
+
}
|
|
152
|
+
// An empty title is NOT a reason to drop the window. A custom title bar
|
|
153
|
+
// (Tauri or Electron with decorations off) leaves the OS title blank, as does
|
|
154
|
+
// an app whose web content has not set document.title yet — and dropping it
|
|
155
|
+
// makes a window the user is plainly looking at impossible to capture, by name
|
|
156
|
+
// OR as the foreground. Label it by its process so it stays nameable.
|
|
157
|
+
if (title.Length == 0) {
|
|
158
|
+
string pn = ProcName(h);
|
|
159
|
+
title = pn.Length > 0 ? pn : "(untitled window)";
|
|
160
|
+
}
|
|
161
|
+
// The leading marker is how the caller knows which window the user is
|
|
162
|
+
// actually looking at, so it can name it when asking permission.
|
|
163
|
+
uint wpid; GetWindowThreadProcessId(h, out wpid);
|
|
164
|
+
string hostCol = mark.Length == 0 && HostPids.Contains(wpid) ? "\thost" : "";
|
|
165
|
+
found.Add((h == fg && mark.Length == 0 ? "*" : "-") + mark + h.ToInt64().ToString() + "\t" + title + hostCol);
|
|
166
|
+
return true;
|
|
167
|
+
}, IntPtr.Zero);
|
|
168
|
+
return found;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
public static string CaptureOn(string desk, IntPtr h, string path) {
|
|
172
|
+
return MwDesk.On(desk, () => Capture(h, path, string.IsNullOrEmpty(desk)));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// screen: whether a blank PrintWindow may fall back to copying the screen. Never for a
|
|
176
|
+
// window on the hidden desktop: the screen is the USER's, and that window is not on it.
|
|
177
|
+
public static string Capture(IntPtr h, string path, bool screen) {
|
|
178
|
+
RECT r;
|
|
179
|
+
if (!Bounds(h, out r)) return "ERR could not measure the window";
|
|
180
|
+
int w = r.Right - r.Left, ht = r.Bottom - r.Top;
|
|
181
|
+
if (w <= 0 || ht <= 0) return "ERR the window has no visible area";
|
|
182
|
+
using (var bmp = new Bitmap(w, ht)) {
|
|
183
|
+
bool drew = false;
|
|
184
|
+
using (var g = Graphics.FromImage(bmp)) {
|
|
185
|
+
IntPtr hdc = g.GetHdc();
|
|
186
|
+
drew = PrintWindow(h, hdc, 2); // PW_RENDERFULLCONTENT
|
|
187
|
+
g.ReleaseHdc(hdc);
|
|
188
|
+
}
|
|
189
|
+
if (!drew || Blank(bmp)) {
|
|
190
|
+
if (!screen) return "ERR the window drew nothing to capture";
|
|
191
|
+
using (var g = Graphics.FromImage(bmp)) {
|
|
192
|
+
g.CopyFromScreen(r.Left, r.Top, 0, 0, new Size(w, ht));
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
bmp.Save(path, ImageFormat.Png);
|
|
196
|
+
return "OK " + w + " " + ht;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// PrintWindow can report success and draw nothing. Sampling three corners of a
|
|
201
|
+
// fully transparent bitmap is enough to tell that apart from a real capture.
|
|
202
|
+
static bool Blank(Bitmap b) {
|
|
203
|
+
if (b.Width < 4 || b.Height < 4) return false;
|
|
204
|
+
return b.GetPixel(1, 1).A == 0
|
|
205
|
+
&& b.GetPixel(b.Width / 2, b.Height / 2).A == 0
|
|
206
|
+
&& b.GetPixel(b.Width - 2, b.Height - 2).A == 0;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
${DESK_CS}
|
|
210
|
+
"@
|
|
211
|
+
|
|
212
|
+
# Before any measurement, or every rectangle is in the wrong coordinate space.
|
|
213
|
+
[MwCapture]::Dpi() | Out-Null
|
|
214
|
+
|
|
215
|
+
if ($Mode -eq "host") {
|
|
216
|
+
Write-Output ([MwCapture]::HostWindow())
|
|
217
|
+
exit 0
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if ($Mode -eq "list") {
|
|
221
|
+
# Which windows are the one this process runs in: walk up from it, stopping at the shell that
|
|
222
|
+
# started the whole session (the desktop, services), and take each console host that belongs to
|
|
223
|
+
# one of those on the way. Every process passed may own the window.
|
|
224
|
+
if ($Owner -gt 0) {
|
|
225
|
+
$procs = Get-CimInstance Win32_Process -Property ProcessId, ParentProcessId, Name
|
|
226
|
+
$parent = @{}
|
|
227
|
+
$name = @{}
|
|
228
|
+
foreach ($p in $procs) { $parent[[int]$p.ProcessId] = [int]$p.ParentProcessId; $name[[int]$p.ProcessId] = ([string]$p.Name).ToLower() }
|
|
229
|
+
$stop = @("explorer.exe", "services.exe", "wininit.exe", "winlogon.exe", "csrss.exe", "svchost.exe", "system")
|
|
230
|
+
$seen = @{}
|
|
231
|
+
$at = $Owner
|
|
232
|
+
while ($at -gt 0 -and -not $seen.ContainsKey($at)) {
|
|
233
|
+
$seen[$at] = $true
|
|
234
|
+
if ($stop -contains $name[$at]) { break }
|
|
235
|
+
[void][MwCapture]::HostPids.Add([uint32]$at)
|
|
236
|
+
$at = $parent[$at]
|
|
237
|
+
}
|
|
238
|
+
foreach ($p in $procs) {
|
|
239
|
+
$n = ([string]$p.Name).ToLower()
|
|
240
|
+
if (($n -eq "conhost.exe" -or $n -eq "openconsole.exe") -and [MwCapture]::HostPids.Contains([uint32]$p.ParentProcessId)) { [void][MwCapture]::HostPids.Add([uint32]$p.ProcessId) }
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
[MwCapture]::List("") | ForEach-Object { Write-Output $_ }
|
|
244
|
+
# The windows of apps started for testing, which are never on the screen.
|
|
245
|
+
if ($Hidden -and [MwDesk]::Exists($Hidden)) { Write-Output ([MwCapture]::ListOn($Hidden)) }
|
|
246
|
+
exit 0
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if ($Mode -eq "capture") {
|
|
250
|
+
$h = if ($Handle -eq "foreground") { [MwCapture]::GetForegroundWindow() } else { [IntPtr][Int64]$Handle }
|
|
251
|
+
if ($h -eq [IntPtr]::Zero) { Write-Output "ERR no such window"; exit 1 }
|
|
252
|
+
$r = [MwCapture]::CaptureOn($Desktop, $h, $Out)
|
|
253
|
+
Write-Output $r
|
|
254
|
+
if ($r.StartsWith("ERR")) { exit 1 }
|
|
255
|
+
exit 0
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
Write-Output "ERR unknown mode"
|
|
259
|
+
exit 1
|
|
215
260
|
`;
|
|
216
261
|
let scriptPath = null;
|
|
217
262
|
/** Write the capture script to a temp file once per process. */
|
|
@@ -225,10 +270,12 @@ async function ensureScript() {
|
|
|
225
270
|
return path;
|
|
226
271
|
}
|
|
227
272
|
/** Run the script in one of its two modes. Rejects with the script's own message. */
|
|
228
|
-
async function runScript(args, signal) {
|
|
273
|
+
async function runScript(args, signal, shareConsole = false) {
|
|
229
274
|
const script = await ensureScript();
|
|
230
275
|
return await new Promise((resolve, reject) => {
|
|
231
|
-
const child = spawn("powershell.exe", ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", script, ...args],
|
|
276
|
+
const child = spawn("powershell.exe", ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", script, ...args],
|
|
277
|
+
// Hidden, with a console of its own, unless it must see THIS process's console (see hostWindowHandle).
|
|
278
|
+
shareConsole ? { stdio: ["ignore", "pipe", "pipe"] } : { windowsHide: true });
|
|
232
279
|
let out = "";
|
|
233
280
|
let err = "";
|
|
234
281
|
const timer = setTimeout(() => {
|
|
@@ -269,11 +316,14 @@ export function parseWindowList(stdout) {
|
|
|
269
316
|
if (tab <= 0)
|
|
270
317
|
continue;
|
|
271
318
|
const marked = line.slice(0, tab).trim();
|
|
272
|
-
|
|
319
|
+
// A third column, "host", marks the window this process runs in.
|
|
320
|
+
const rest = line.slice(tab + 1).split("\t");
|
|
321
|
+
const host = rest.length > 1 && rest[rest.length - 1].trim() === "host";
|
|
322
|
+
const title = (host ? rest.slice(0, -1) : rest).join("\t").trim();
|
|
273
323
|
const match = new RegExp(`^([*-])(${HIDDEN_MARK}?\\d+)$`).exec(marked);
|
|
274
324
|
if (!match || !title)
|
|
275
325
|
continue;
|
|
276
|
-
windows.push({ handle: match[2], title, foreground: match[1] === "*" });
|
|
326
|
+
windows.push({ handle: match[2], title, foreground: match[1] === "*", ...(host ? { host: true } : {}) });
|
|
277
327
|
}
|
|
278
328
|
return windows;
|
|
279
329
|
}
|
|
@@ -283,7 +333,40 @@ export function parseWindowList(stdout) {
|
|
|
283
333
|
* title (a custom title bar) kept and labelled by its process rather than dropped.
|
|
284
334
|
*/
|
|
285
335
|
export async function listWindows(signal) {
|
|
286
|
-
|
|
336
|
+
const [raw, host] = await Promise.all([
|
|
337
|
+
runScript(["-Mode", "list", "-Hidden", hiddenDesktopName(), "-Owner", String(process.pid)], signal),
|
|
338
|
+
hostWindowHandle(signal),
|
|
339
|
+
]);
|
|
340
|
+
const windows = parseWindowList(raw);
|
|
341
|
+
// The terminal's own window: found from the console this process is attached to, which the list cannot see
|
|
342
|
+
// (Windows Terminal is not a parent of the shell, only the owner of its window).
|
|
343
|
+
if (host)
|
|
344
|
+
for (const w of windows)
|
|
345
|
+
if (w.handle === host)
|
|
346
|
+
w.host = true;
|
|
347
|
+
return windows;
|
|
348
|
+
}
|
|
349
|
+
let hostHandleCache;
|
|
350
|
+
/**
|
|
351
|
+
* The window of the terminal this process is typed into, as a handle, or null when it has none (the desktop
|
|
352
|
+
* app) or it cannot be told. Asked once and kept: it does not change while the process runs. The question
|
|
353
|
+
* is put by a child that shares this console rather than making one of its own, since a console is what
|
|
354
|
+
* names the window, and that is only done when this process has a terminal at all, so a program with none
|
|
355
|
+
* is never made to open a window.
|
|
356
|
+
*/
|
|
357
|
+
export async function hostWindowHandle(signal) {
|
|
358
|
+
if (hostHandleCache !== undefined)
|
|
359
|
+
return hostHandleCache;
|
|
360
|
+
if (!process.stdout.isTTY && !process.stdin.isTTY)
|
|
361
|
+
return (hostHandleCache = null);
|
|
362
|
+
try {
|
|
363
|
+
const out = (await runScript(["-Mode", "host"], signal, true)).trim().split(/\s+/).pop() ?? "";
|
|
364
|
+
hostHandleCache = /^\d+$/.test(out) ? out : null;
|
|
365
|
+
}
|
|
366
|
+
catch {
|
|
367
|
+
hostHandleCache = null;
|
|
368
|
+
}
|
|
369
|
+
return hostHandleCache;
|
|
287
370
|
}
|
|
288
371
|
/** Capture one window to `outPath`. `handle` may be the literal "foreground". */
|
|
289
372
|
export async function captureWindow(handle, outPath, signal) {
|