426e9eeabd
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Waiting to run
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Waiting to run
Build Agent Image / build-and-push (push) Waiting to run
Chat shell gestures / Chat shell gesture + parity e2e (push) Waiting to run
Cloud Gateway Discord / Test (push) Waiting to run
Cloud Gateway Webhook / Test (push) Waiting to run
Cloud Tests / lint-and-types (push) Waiting to run
Cloud Tests / unit-tests (push) Waiting to run
Cloud Tests / integration-tests (push) Waiting to run
Cloud Tests / e2e-tests (push) Blocked by required conditions
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
Deploy Apps Worker (Product 2) / Determine environment (push) Waiting to run
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Blocked by required conditions
Deploy Eliza Provisioning Worker / Determine environment (push) Waiting to run
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Blocked by required conditions
Dev Smoke / Classify changed paths (push) Waiting to run
Dev Smoke / bun run dev onboarding chat (push) Blocked by required conditions
Dev Smoke / Vite HMR dependency-level smoke (push) Blocked by required conditions
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Waiting to run
Publish @elizaos/example-code / check_npm (push) Waiting to run
Publish @elizaos/example-code / publish_npm (push) Blocked by required conditions
Publish @elizaos/plugin-elizacloud / verify_version (push) Waiting to run
Publish @elizaos/plugin-elizacloud / publish_npm (push) Blocked by required conditions
Sandbox Live Smoke / Sandbox live smoke (push) Waiting to run
Snap Build & Test / Build Snap (amd64) (push) Waiting to run
Snap Build & Test / Build Snap (arm64) (push) Waiting to run
supply-chain / sbom (push) Waiting to run
supply-chain / vulnerability-scan (push) Waiting to run
Build, Push & Deploy to Phala Cloud / build-and-push (push) Waiting to run
Test Packaging / Validate Packaging Configs (push) Waiting to run
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Waiting to run
Test Packaging / Pack & Test JS Tarballs (push) Waiting to run
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Waiting to run
UI Fixture E2E / ui-fixture-e2e (push) Waiting to run
UI Fixture E2E / fixture-e2e (push) Waiting to run
UI Story Gate / story-gate (push) Waiting to run
vault-ci / test (macos-latest) (push) Waiting to run
vault-ci / test (ubuntu-latest) (push) Waiting to run
vault-ci / test (windows-latest) (push) Waiting to run
vault-ci / app-core wiring tests (push) Waiting to run
verify-patches / verify patches/CHECKSUMS.sha256 (push) Waiting to run
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Waiting to run
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Waiting to run
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Waiting to run
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Waiting to run
Voice Benchmark Smoke / voice bench smoke summary (push) Blocked by required conditions
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Waiting to run
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Waiting to run
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Waiting to run
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Waiting to run
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Waiting to run
Test Packaging / Build & Test PyPI Package (push) Waiting to run
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
555 lines
18 KiB
JavaScript
555 lines
18 KiB
JavaScript
#!/usr/bin/env bun
|
|
/**
|
|
* #9581 — Windows non-disruptive mouse/keyboard *effect* screen RECORDING.
|
|
*
|
|
* The capture harness (`capture-windows-desktop-evidence.mjs`) proves the input
|
|
* lands by reading the typed marker back; this companion produces the moving
|
|
* picture the issue asks for — a real screen recording of CUA input taking
|
|
* effect on a controlled Windows text-input window.
|
|
*
|
|
* gdigrab is blocked in this RDP session (BitBlt error 5), but the computeruse
|
|
* capture path (WinRT/.NET) works, so we capture a dense frame burst through it
|
|
* WHILE driving mouse_move → click → paste (progressive, chunked) → save →
|
|
* select-all, verify the marker via a saved file + clipboard/UIA read-back,
|
|
* then ffmpeg the frames into an mp4 + gif. Non-disruptive: it drives a
|
|
* freshly-launched controlled window and kills it by window id, never the
|
|
* user's apps.
|
|
*
|
|
* Run: bun scripts/record-windows-cua-input.mjs
|
|
*/
|
|
|
|
import { execFileSync } from "node:child_process";
|
|
import { copyFile, mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { readClipboard, writeClipboard } from "../src/platform/clipboard.ts";
|
|
import { ComputerUseService } from "../src/services/computer-use-service.ts";
|
|
|
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
const repoRoot = path.resolve(here, "../../..");
|
|
const outDir = path.join(
|
|
repoRoot,
|
|
"test-results/evidence/9581-windows-cua/input-recording",
|
|
);
|
|
const framesDir = path.join(outDir, "frames");
|
|
|
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
|
|
function createRuntime(settings = {}) {
|
|
return {
|
|
character: {},
|
|
getSetting: (k) => settings[k],
|
|
getService: () => null,
|
|
};
|
|
}
|
|
|
|
function displayForPoint(displays, x, y) {
|
|
return (
|
|
displays.find((d) => {
|
|
const [dx, dy, w, h] = d.bounds;
|
|
return x >= dx && x < dx + w && y >= dy && y < dy + h;
|
|
}) ??
|
|
displays.find((d) => d.primary) ??
|
|
displays[0]
|
|
);
|
|
}
|
|
|
|
function windowHaystack(w) {
|
|
return `${w?.app ?? ""} ${w?.title ?? ""}`.toLowerCase();
|
|
}
|
|
|
|
function looksLikeTargetWindow(w, targetTitle) {
|
|
return windowHaystack(w).includes(targetTitle.toLowerCase());
|
|
}
|
|
|
|
function psSingleQuote(value) {
|
|
return `'${String(value).replace(/'/g, "''")}'`;
|
|
}
|
|
|
|
function createInputTargetScript(title, savePath) {
|
|
const savedTitle = `${title} (saved)`;
|
|
return `
|
|
Add-Type -AssemblyName System.Windows.Forms
|
|
Add-Type -AssemblyName System.Drawing
|
|
$savePath = ${psSingleQuote(savePath)}
|
|
$form = New-Object System.Windows.Forms.Form
|
|
$form.Text = ${psSingleQuote(title)}
|
|
$form.Width = 920
|
|
$form.Height = 680
|
|
$form.StartPosition = 'CenterScreen'
|
|
$form.KeyPreview = $true
|
|
$box = New-Object System.Windows.Forms.TextBox
|
|
$box.Multiline = $true
|
|
$box.AcceptsReturn = $true
|
|
$box.AcceptsTab = $true
|
|
$box.ScrollBars = 'Both'
|
|
$box.WordWrap = $false
|
|
$box.Dock = 'Fill'
|
|
$box.Font = New-Object System.Drawing.Font('Consolas', 16)
|
|
$form.Controls.Add($box)
|
|
$save = {
|
|
[System.IO.File]::WriteAllText($savePath, $box.Text)
|
|
$form.Text = ${psSingleQuote(savedTitle)}
|
|
}
|
|
$box.Add_KeyDown({
|
|
if ($_.Control -and $_.KeyCode -eq [System.Windows.Forms.Keys]::S) {
|
|
& $save
|
|
$_.SuppressKeyPress = $true
|
|
}
|
|
})
|
|
$form.Add_KeyDown({
|
|
if ($_.Control -and $_.KeyCode -eq [System.Windows.Forms.Keys]::S) {
|
|
& $save
|
|
$_.SuppressKeyPress = $true
|
|
}
|
|
})
|
|
$form.Add_Shown({ $box.Focus() })
|
|
[void]$form.ShowDialog()
|
|
`;
|
|
}
|
|
|
|
function readWindowTextViaUia(windowId) {
|
|
const pid = Number.parseInt(String(windowId), 10);
|
|
if (!Number.isFinite(pid) || pid <= 0) return "";
|
|
const ps = `
|
|
Add-Type -AssemblyName UIAutomationClient
|
|
Add-Type -AssemblyName UIAutomationTypes
|
|
$proc = Get-Process -Id ${pid} -ErrorAction Stop
|
|
$root = [System.Windows.Automation.AutomationElement]::FromHandle($proc.MainWindowHandle)
|
|
if ($null -eq $root) { exit 0 }
|
|
$nodes = $root.FindAll([System.Windows.Automation.TreeScope]::Descendants, [System.Windows.Automation.Condition]::TrueCondition)
|
|
$out = New-Object System.Collections.Generic.List[string]
|
|
for ($i = 0; $i -lt $nodes.Count; $i++) {
|
|
$el = $nodes.Item($i)
|
|
try {
|
|
$pattern = $null
|
|
if ($el.TryGetCurrentPattern([System.Windows.Automation.TextPattern]::Pattern, [ref]$pattern)) {
|
|
$text = $pattern.DocumentRange.GetText(-1)
|
|
if ($text) { [void]$out.Add($text) }
|
|
}
|
|
} catch {}
|
|
try {
|
|
$pattern = $null
|
|
if ($el.TryGetCurrentPattern([System.Windows.Automation.ValuePattern]::Pattern, [ref]$pattern)) {
|
|
$value = $pattern.Current.Value
|
|
if ($value) { [void]$out.Add($value) }
|
|
}
|
|
} catch {}
|
|
try {
|
|
$name = $el.Current.Name
|
|
if ($name) { [void]$out.Add($name) }
|
|
} catch {}
|
|
}
|
|
$out -join "\`n"
|
|
`;
|
|
try {
|
|
return execFileSync("powershell", ["-NoProfile", "-Command", ps], {
|
|
encoding: "utf8",
|
|
timeout: 15_000,
|
|
stdio: ["ignore", "pipe", "ignore"],
|
|
});
|
|
} catch {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
let frameIndex = 0;
|
|
async function captureFrame(service, label, frames) {
|
|
const shot = await service.executeCommand("screenshot");
|
|
if (!shot.success || !shot.screenshot) {
|
|
throw new Error(`screenshot failed: ${shot.error ?? "no payload"}`);
|
|
}
|
|
const buf = Buffer.from(shot.screenshot, "base64");
|
|
const name = `frame-${String(frameIndex).padStart(3, "0")}.png`;
|
|
await writeFile(path.join(framesDir, name), buf);
|
|
frames.push({ index: frameIndex, name, label, bytes: buf.length });
|
|
frameIndex++;
|
|
return buf.length;
|
|
}
|
|
|
|
async function main() {
|
|
await rm(outDir, { recursive: true, force: true }).catch(() => {});
|
|
await mkdir(framesDir, { recursive: true });
|
|
|
|
const token = `eliza-win-cua-${Date.now()}`;
|
|
const phrase = token;
|
|
const chunks = phrase.match(/.{1,12}/g) ?? [phrase];
|
|
const targetTitle = `elizaOS CUA Input Target ${process.pid}`;
|
|
const targetTextFile = path.join(outDir, "windows-cua-input-target.txt");
|
|
const targetScript = path.join(outDir, "windows-cua-input-target.ps1");
|
|
await writeFile(targetTextFile, "", "utf8");
|
|
await writeFile(
|
|
targetScript,
|
|
createInputTargetScript(targetTitle, targetTextFile),
|
|
"utf8",
|
|
);
|
|
|
|
const service = await ComputerUseService.start(
|
|
createRuntime({
|
|
COMPUTER_USE_APPROVAL_MODE: "full_control",
|
|
// Explicit captures only — no implicit post-action screenshots (faster,
|
|
// and we control exactly which frames make the recording).
|
|
COMPUTER_USE_SCREENSHOT_AFTER_ACTION: "false",
|
|
COMPUTER_USE_BROWSER_HEADLESS: "true",
|
|
}),
|
|
);
|
|
|
|
const frames = [];
|
|
const originalClipboard = await readClipboard().catch(() => "");
|
|
let targetWindow = null;
|
|
let verified = false;
|
|
let verificationMethod = "none";
|
|
let verificationReadback = "";
|
|
|
|
try {
|
|
const displays = service.getDisplays();
|
|
|
|
const launched = await service.executeCommand("launch", {
|
|
app: "cmd.exe",
|
|
appArgs: [
|
|
"/c",
|
|
"start",
|
|
"",
|
|
"powershell.exe",
|
|
"-NoProfile",
|
|
"-STA",
|
|
"-ExecutionPolicy",
|
|
"Bypass",
|
|
"-File",
|
|
targetScript,
|
|
],
|
|
});
|
|
if (!launched.success) {
|
|
throw new Error(
|
|
`launch input target failed: ${launched.error ?? "unknown"}`,
|
|
);
|
|
}
|
|
|
|
// Resolve the real target window by the generated title. That keeps this
|
|
// run isolated from whatever user apps or restored editor sessions are open.
|
|
for (let attempt = 0; attempt < 20; attempt++) {
|
|
const found = await service.executeWindowAction({ action: "list" });
|
|
targetWindow =
|
|
(found?.windows ?? []).find((window) =>
|
|
looksLikeTargetWindow(window, targetTitle),
|
|
) ?? null;
|
|
if (targetWindow?.id) break;
|
|
await sleep(250);
|
|
}
|
|
if (!targetWindow?.id) {
|
|
throw new Error(
|
|
`could not resolve controlled input window titled ${targetTitle}`,
|
|
);
|
|
}
|
|
await service
|
|
.executeWindowAction({ action: "focus", windowId: targetWindow.id })
|
|
.catch(() => {});
|
|
await sleep(500);
|
|
await captureFrame(
|
|
service,
|
|
"controlled text target launched (empty)",
|
|
frames,
|
|
);
|
|
|
|
// Maximize the target, then read its actual bounds. Windows can preserve a
|
|
// snapped/tiled layout even after a maximize request in this RDP session, so
|
|
// display-center clicks can land in a neighboring Edge window.
|
|
await service
|
|
.executeWindowAction({ action: "maximize", windowId: targetWindow.id })
|
|
.catch(() => {});
|
|
await sleep(700);
|
|
await captureFrame(
|
|
service,
|
|
"controlled text target maximized (empty)",
|
|
frames,
|
|
);
|
|
|
|
const bounds = await service.executeWindowAction({
|
|
action: "get_window_position",
|
|
windowId: targetWindow.id,
|
|
});
|
|
if (!bounds.success || !bounds.bounds) {
|
|
throw new Error(
|
|
`could not read target bounds: ${bounds.error ?? "unknown"}`,
|
|
);
|
|
}
|
|
const b = bounds.bounds;
|
|
// Aim at the center of the multiline text box.
|
|
const globalX = Math.round(b.x + b.width / 2);
|
|
const globalY = Math.round(b.y + b.height / 2);
|
|
const display = displayForPoint(displays, globalX, globalY);
|
|
if (!display) {
|
|
throw new Error("no display available for input target");
|
|
}
|
|
const [displayX, displayY] = display.bounds;
|
|
const coordinate = [globalX - displayX, globalY - displayY];
|
|
|
|
// Click to focus + place the caret; Ctrl+End re-homes the caret so re-clicks
|
|
// between chunks never split the text.
|
|
const focusControlledTarget = async () => {
|
|
if (!targetWindow?.id) {
|
|
throw new Error("Input target window is not resolved");
|
|
}
|
|
await service
|
|
.executeWindowAction({ action: "focus", windowId: targetWindow.id })
|
|
.catch(() => {});
|
|
await sleep(200);
|
|
};
|
|
const clickFocus = async () => {
|
|
await focusControlledTarget();
|
|
await service.executeCommand("click", {
|
|
coordinate,
|
|
displayId: display.id,
|
|
});
|
|
await sleep(250);
|
|
};
|
|
const keyComboInTarget = async (key) => {
|
|
await clickFocus();
|
|
const result = await service.executeCommand("key_combo", { key });
|
|
if (!result.success) {
|
|
throw new Error(`key_combo ${key} failed: ${result.error}`);
|
|
}
|
|
await sleep(250);
|
|
};
|
|
|
|
await service
|
|
.executeCommand("mouse_move", { coordinate, displayId: display.id })
|
|
.catch(() => {});
|
|
await clickFocus();
|
|
await captureFrame(
|
|
service,
|
|
"clicked into the text area (caret active)",
|
|
frames,
|
|
);
|
|
|
|
// Progressive, chunked typing so the recording shows text appearing.
|
|
let typedSoFar = "";
|
|
for (const chunk of chunks) {
|
|
await keyComboInTarget("ctrl+End");
|
|
await writeClipboard(chunk);
|
|
await keyComboInTarget("ctrl+v");
|
|
typedSoFar += chunk;
|
|
await captureFrame(
|
|
service,
|
|
`pasted ${typedSoFar.length}/${phrase.length} chars via ctrl+v`,
|
|
frames,
|
|
);
|
|
}
|
|
await keyComboInTarget("ctrl+s");
|
|
await sleep(700);
|
|
await captureFrame(service, "ctrl+s saved the typed text file", frames);
|
|
|
|
// Ask for select-all (visible when the legacy SendKeys driver manages to
|
|
// keep focus), then verify by clipboard. On this Windows/Edge split-screen
|
|
// session SendKeys can leave focus on the text area without a visible
|
|
// selection, so fall back to UI Automation text readback from the controlled
|
|
// target window. These methods read the real app/window, not a mock.
|
|
await keyComboInTarget("ctrl+a");
|
|
await captureFrame(service, "ctrl+a requested for the typed text", frames);
|
|
await sleep(300);
|
|
await keyComboInTarget("ctrl+a");
|
|
await keyComboInTarget("ctrl+c");
|
|
await sleep(350);
|
|
const fileReadBack = await readFile(targetTextFile, "utf8").catch(() => "");
|
|
const readBack = await readClipboard().catch(() => "");
|
|
const uiaReadBack = readWindowTextViaUia(targetWindow.id);
|
|
const freshWindows = await service.executeCommand("list_windows");
|
|
const titleReadBack = Array.isArray(freshWindows.windows)
|
|
? freshWindows.windows
|
|
.filter((window) => looksLikeTargetWindow(window, targetTitle))
|
|
.map((window) => `${window.title ?? ""} ${window.app ?? ""}`.trim())
|
|
.join("\n")
|
|
: "";
|
|
if (fileReadBack.includes(token)) {
|
|
verificationReadback = fileReadBack;
|
|
verificationMethod = "saved-file";
|
|
} else if (readBack.includes(token)) {
|
|
verificationReadback = readBack;
|
|
verificationMethod = "clipboard";
|
|
} else if (uiaReadBack.includes(token)) {
|
|
verificationReadback = uiaReadBack;
|
|
verificationMethod = "uia";
|
|
} else {
|
|
verificationReadback = titleReadBack;
|
|
verificationMethod = "window-title";
|
|
}
|
|
verified = verificationReadback.includes(token);
|
|
await captureFrame(
|
|
service,
|
|
verified
|
|
? `verified: marker read back from target via ${verificationMethod}`
|
|
: "read-back did NOT contain the marker",
|
|
frames,
|
|
);
|
|
|
|
if (!verified) {
|
|
throw new Error(
|
|
`read-back missing marker; clipboard=${JSON.stringify(
|
|
readBack.slice(0, 80),
|
|
)}; savedFile=${JSON.stringify(
|
|
fileReadBack.slice(0, 120),
|
|
)}; uia=${JSON.stringify(
|
|
uiaReadBack.slice(0, 120),
|
|
)}; title=${JSON.stringify(titleReadBack.slice(0, 120))}`,
|
|
);
|
|
}
|
|
} finally {
|
|
if (targetWindow?.id) {
|
|
const killed = await service
|
|
.executeCommand("kill_app", { target: String(targetWindow.id) })
|
|
.catch(() => ({ success: false }));
|
|
if (!killed.success) {
|
|
await service
|
|
.executeCommand("close_window", { windowId: targetWindow.id })
|
|
.catch(() => {});
|
|
}
|
|
}
|
|
await writeClipboard(originalClipboard).catch(() => {});
|
|
await service.stop().catch(() => {});
|
|
}
|
|
|
|
// Assemble the frame burst into an mp4 + gif (ffmpeg reads PNG files, not the
|
|
// blocked gdigrab device). ~2.5 fps reads as a clear step-by-step recording.
|
|
const fps = "5/2";
|
|
const pattern = path.join(framesDir, "frame-%03d.png");
|
|
const mp4 = path.join(outDir, "windows-cua-input.mp4");
|
|
const gif = path.join(outDir, "windows-cua-input.gif");
|
|
const palette = path.join(outDir, "palette.png");
|
|
|
|
execFileSync(
|
|
"ffmpeg",
|
|
[
|
|
"-y",
|
|
"-framerate",
|
|
fps,
|
|
"-i",
|
|
pattern,
|
|
"-vf",
|
|
"scale=1100:-2:flags=lanczos,format=yuv420p",
|
|
"-r",
|
|
"12",
|
|
mp4,
|
|
],
|
|
{ stdio: "pipe" },
|
|
);
|
|
execFileSync(
|
|
"ffmpeg",
|
|
[
|
|
"-y",
|
|
"-framerate",
|
|
fps,
|
|
"-i",
|
|
pattern,
|
|
"-vf",
|
|
"scale=1000:-1:flags=lanczos,palettegen",
|
|
palette,
|
|
],
|
|
{ stdio: "pipe" },
|
|
);
|
|
execFileSync(
|
|
"ffmpeg",
|
|
[
|
|
"-y",
|
|
"-framerate",
|
|
fps,
|
|
"-i",
|
|
pattern,
|
|
"-i",
|
|
palette,
|
|
"-lavfi",
|
|
"scale=1000:-1:flags=lanczos[x];[x][1:v]paletteuse",
|
|
"-r",
|
|
"6",
|
|
gif,
|
|
],
|
|
{ stdio: "pipe" },
|
|
);
|
|
await rm(palette, { force: true }).catch(() => {});
|
|
|
|
const initialFrame = frames[1] ?? frames[0];
|
|
const finalFrame = frames[frames.length - 1];
|
|
const initialStill = path.join(outDir, "initial-empty-input-window.png");
|
|
const finalStill = path.join(outDir, "final-typed-selected.png");
|
|
if (initialFrame) {
|
|
await copyFile(path.join(framesDir, initialFrame.name), initialStill);
|
|
}
|
|
if (finalFrame) {
|
|
await copyFile(path.join(framesDir, finalFrame.name), finalStill);
|
|
}
|
|
|
|
const readme = `# #9581 — Windows non-disruptive mouse/keyboard effect screen recording
|
|
|
|
The capture harness proves Windows CUA input lands by reading the typed marker
|
|
back. This is the moving-picture companion: a real screen recording of CUA mouse
|
|
and keyboard input taking effect on a controlled Windows text-input window.
|
|
|
|
Captured on a real Windows 11 Pro host (QEMU), 1728x1052, via
|
|
\`plugins/plugin-computeruse/scripts/record-windows-cua-input.mjs\`:
|
|
|
|
1. launch a generated Windows Forms text target through computeruse
|
|
2. click inside the controlled text box bounds
|
|
3. progressively paste a marker with \`Ctrl+V\` while capturing frames
|
|
4. save with \`Ctrl+S\` and verify the marker from the real saved file/window
|
|
|
|
Frames are captured through the computeruse WinRT/.NET capture path and
|
|
assembled into MP4/GIF with ffmpeg.
|
|
|
|
| File | What it is |
|
|
|------|------------|
|
|
| \`windows-cua-input.gif\` | Inline recording of the run. |
|
|
| \`windows-cua-input.mp4\` | Same recording, H.264. |
|
|
| \`final-typed-selected.png\` | Final frame after verification; the typed marker is visible in the target. |
|
|
| \`initial-empty-input-window.png\` | Early frame before typing. |
|
|
| \`windows-cua-input-target.txt\` | The real text file saved by the \`Ctrl+S\` step. |
|
|
| \`windows-cua-input-target.ps1\` | The generated controlled Windows Forms target used for the run. |
|
|
| \`recording-summary.json\` | Run metadata, including \`verified: true\` and verification method. |
|
|
|
|
Verification method for this run: \`${verificationMethod}\`.
|
|
`;
|
|
await writeFile(path.join(outDir, "README.md"), readme);
|
|
|
|
const summary = {
|
|
issue: 9581,
|
|
capturedAt: new Date().toISOString(),
|
|
host: "Windows 11 Pro (QEMU)",
|
|
verified,
|
|
verificationMethod,
|
|
verificationReadbackPreview: verificationReadback.slice(0, 240),
|
|
marker: token,
|
|
phrase,
|
|
frameCount: frames.length,
|
|
frames,
|
|
artifacts: {
|
|
mp4: path.relative(repoRoot, mp4),
|
|
gif: path.relative(repoRoot, gif),
|
|
initialStill: path.relative(repoRoot, initialStill),
|
|
finalStill: path.relative(repoRoot, finalStill),
|
|
savedTextFile: path.relative(repoRoot, targetTextFile),
|
|
targetScript: path.relative(repoRoot, targetScript),
|
|
readme: path.relative(repoRoot, path.join(outDir, "README.md")),
|
|
framesDir: path.relative(repoRoot, framesDir),
|
|
},
|
|
};
|
|
await writeFile(
|
|
path.join(outDir, "recording-summary.json"),
|
|
`${JSON.stringify(summary, null, 2)}\n`,
|
|
);
|
|
|
|
console.log(
|
|
JSON.stringify(
|
|
{
|
|
status: verified ? "passed" : "failed",
|
|
...summary.artifacts,
|
|
frameCount: frames.length,
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
);
|
|
}
|
|
|
|
main().catch((err) => {
|
|
console.error(err);
|
|
process.exit(1);
|
|
});
|