/** * Screen & Window Tools * * Two Windows-specific tools: * * 1. `take_screenshot` - Capture one monitor (or all monitors stitched) to a PNG. * Supports region cropping (x/y/width/height), rescaling, and * selecting the target monitor by 0-based index, "primary", or "all". * 2. `hide_window` - Hide (or show) a window by title, or the foreground window. * * Both shell out to PowerShell (win32 / System.Drawing). On non-Windows platforms * they report they are unavailable. */ import type { AgentToolResult, ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { Type } from "typebox"; import { writeFile, unlink } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; const IS_WINDOWS = process.platform === "win32"; const HIDE_WINDOW_SCRIPT = ` Add-Type @" using System; using System.Runtime.InteropServices; public class Win { [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow(); [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow); [DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr hWnd); [DllImport("user32.dll", CharSet=CharSet.Unicode)] public static extern int GetWindowText(IntPtr hWnd, System.Text.StringBuilder lpString, int nMaxCount); [DllImport("user32.dll", CharSet=CharSet.Unicode)] public static extern IntPtr FindWindow(string lpClassName, string lpWindowName); } "@ $Target = $args[0] $Show = $args[1] -eq 'show' $h = $null if ($Target -and $Target -ne 'foreground') { $h = [Win]::FindWindow($null, $Target) } else { $h = [Win]::GetForegroundWindow() } if ($h -eq [IntPtr]::Zero) { Write-Output "NOT_FOUND" exit 1 } if ($Show) { [Win]::ShowWindow($h, 9) | Out-Null # SW_RESTORE Write-Output "SHOWN" } else { [Win]::ShowWindow($h, 0) | Out-Null # SW_HIDE Write-Output "HIDDEN" } `; // args: out x y w h scaleWidth scaleHeight monitorIndex // monitorIndex: 0-based int, "primary", "all", or empty (defaults to primary) const SCREENSHOT_SCRIPT = ` Add-Type -AssemblyName System.Windows.Forms,System.Drawing $Out = $args[0] if ([string]::IsNullOrEmpty($Out) -or $Out -eq 'default') { $Out = Join-Path $env:TEMP ("pi-screen-" + [DateTime]::Now.Ticks + ".png") } $ScaleW = if ($args[5]) { [int]$args[5] } else { 0 } $ScaleH = if ($args[6]) { [int]$args[6] } else { 0 } $MonSel = $args[7] $screens = [System.Windows.Forms.Screen]::AllScreens # Determine the physical region to capture. $target = $null $isAll = $false if ($MonSel -eq 'all') { $isAll = $true } elseif ($MonSel -eq 'primary' -or [string]::IsNullOrEmpty($MonSel)) { $target = [System.Windows.Forms.Screen]::PrimaryScreen } else { $idx = [int]$MonSel if ($idx -lt 0 -or $idx -ge $screens.Count) { Write-Output ("MONITOR_NOT_FOUND: index " + $idx + " out of range (0.." + ($screens.Count - 1) + ")") exit 1 } $target = $screens[$idx] } if ($isAll) { # Stitch every monitor. Compute the union rectangle; split capture per screen to # stay under System.Drawing bitmap size limits, then compose onto a canvas. $left = ($screens | ForEach-Object { $_.Bounds.Left } | Measure-Object -Minimum).Minimum $top = ($screens | ForEach-Object { $_.Bounds.Top } | Measure-Object -Minimum).Minimum $right = ($screens | ForEach-Object { $_.Bounds.Right } | Measure-Object -Maximum).Maximum $bottom= ($screens | ForEach-Object { $_.Bounds.Bottom} | Measure-Object -Maximum).Maximum $W = $right - $left $H = $bottom - $top if ($W -le 0 -or $H -le 0) { Write-Output ("LRG: empty virtual screen " + $W + "x" + $H) exit 1 } # System.Drawing bitmaps cannot exceed ~4095 on a dimension. If the stitched # canvas would exceed that, scale the final output to fit within 4095px width. $AutoScaleW = $W $AutoScaleH = $H if ($W -gt 4095) { $ratio = 4095 / $W $AutoScaleW = 4095 $AutoScaleH = [int][math]::Round($H * $ratio) } $canvas = New-Object System.Drawing.Bitmap($AutoScaleW, $AutoScaleH) $cg = [System.Drawing.Graphics]::FromImage($canvas) for ($i = 0; $i -lt $screens.Count; $i++) { $b = $screens[$i].Bounds $bmp = New-Object System.Drawing.Bitmap($b.Width, $b.Height) $g = [System.Drawing.Graphics]::FromImage($bmp) $g.CopyFromScreen($b.Location, [System.Drawing.Point]::Empty, $b.Size) $relX = [int]($b.Left - $left) * ($AutoScaleW / $W) $relY = [int]($b.Top - $top) * ($AutoScaleH / $H) $dw = [int][math]::Round($b.Width * ($AutoScaleW / $W)) $dh = [int][math]::Round($b.Height * ($AutoScaleH / $H)) $cg.DrawImage($bmp, $relX, $relY, $dw, $dh) $g.Dispose(); $bmp.Dispose() } $cg.Dispose() # (scale handled below via $canvas) $bmp = $canvas } else { $bounds = $target.Bounds $RectX = $bounds.Left $RectY = $bounds.Top $RectW = $bounds.Width $RectH = $bounds.Height if ($args[1] -ne '') { $RectX = [int]$bounds.Left + [int]$args[1] } if ($args[2] -ne '') { $RectY = [int]$bounds.Top + [int]$args[2] } if ($args[3] -ne '') { $RectW = [int]$args[3] } if ($args[4] -ne '') { $RectH = [int]$args[4] } $rect = New-Object System.Drawing.Rectangle($RectX, $RectY, $RectW, $RectH) $bmp = New-Object System.Drawing.Bitmap($rect.Width, $rect.Height) $g = [System.Drawing.Graphics]::FromImage($bmp) $g.CopyFromScreen($rect.Location, [System.Drawing.Point]::Empty, $rect.Size) $g.Dispose() } if ($ScaleW -gt 0 -and $ScaleH -gt 0) { $resized = New-Object System.Drawing.Bitmap($ScaleW, $ScaleH) $g2 = [System.Drawing.Graphics]::FromImage($resized) $g2.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic $g2.DrawImage($bmp, 0, 0, $ScaleW, $ScaleH) $bmp.Dispose() $resized.Save($Out, [System.Drawing.Imaging.ImageFormat]::Png) $g2.Dispose(); $resized.Dispose() } else { $bmp.Save($Out, [System.Drawing.Imaging.ImageFormat]::Png) $bmp.Dispose() } Write-Output $Out `; async function runPs(pi: ExtensionAPI, script: string, args: string[], signal?: AbortSignal): Promise<{ stdout: string; stderr: string; code: number }> { const tmpFile = join(tmpdir(), `pi-tool-${Date.now()}-${Math.floor(Math.random() * 1e6)}.ps1`); await writeFile(tmpFile, script, "utf8"); try { const psArgs = ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", tmpFile, ...args]; const result = await pi.exec("powershell.exe", psArgs, { signal, timeout: 30000 }); return result; } finally { try { await unlink(tmpFile); } catch { /* ignore */ } } } type ScreenshotDetails = { path?: string; monitor?: string }; type HideWindowDetails = { title?: string; show?: boolean }; export function registerScreen(pi: ExtensionAPI) { if (!IS_WINDOWS) { // Register no-op reporting tools on non-Windows. pi.registerTool({ name: "take_screenshot", label: "Take Screenshot", description: "Not available on this platform (Windows only).", parameters: Type.Object({}), async execute() { return { content: [{ type: "text", text: "take_screenshot is not available on non-Windows platforms." }], details: {}, }; }, }); return; } pi.registerTool({ name: "take_screenshot", label: "Take Screenshot", description: "Capture the screen to a PNG file. By default captures the full primary display. Use monitorIndex (0-based index, 'primary', or 'all') to select which monitor to capture; 'all' stitches every display into one image. Use x/y/width/height to crop a region relative to the chosen monitor's top-left corner. Optionally rescale to a target resolution. Returns the saved image path (view it with the read tool).", promptSnippet: "Capture the screen (a monitor, full or region) to a PNG image", parameters: Type.Object({ monitorIndex: Type.Optional( Type.String({ description: "Monitor to capture: 0-based index (e.g. '0', '1'), 'primary', or 'all'. Default: primary.", }), ), x: Type.Optional(Type.Integer({ description: "Left offset from the monitor's top-left corner (default: monitor left edge)" })), y: Type.Optional(Type.Integer({ description: "Top offset from the monitor's top-left corner (default: monitor top edge)" })), width: Type.Optional(Type.Integer({ description: "Region width in pixels (default: full monitor width)" })), height: Type.Optional(Type.Integer({ description: "Region height in pixels (default: full monitor height)" })), resolutionWidth: Type.Optional(Type.Integer({ description: "Target output width to rescale to; 0/omitted keeps original" })), resolutionHeight: Type.Optional(Type.Integer({ description: "Target output height to rescale to; 0/omitted keeps original" })), outputPath: Type.Optional(Type.String({ description: "Output file path; defaults to a temp path" })), }), async execute(_toolCallId, params, signal): Promise> { if (signal?.aborted) return { content: [{ type: "text", text: "Cancelled" }], details: {} }; const outputPath = params.outputPath ?? "default"; const args = [ outputPath, params.x?.toString() ?? "", params.y?.toString() ?? "", params.width?.toString() ?? "", params.height?.toString() ?? "", params.resolutionWidth?.toString() ?? "", params.resolutionHeight?.toString() ?? "", params.monitorIndex ?? "", ]; const result = await runPs(pi, SCREENSHOT_SCRIPT, args, signal); if (result.code !== 0) { const msg = result.stdout.trim(); if (msg.startsWith("MONITOR_NOT_FOUND")) throw new Error(msg); if (msg.startsWith("LRG:")) throw new Error(`Cannot capture: ${msg.replace("LRG:", "").trim()}`); throw new Error(`Screenshot failed: ${result.stderr || result.stdout}`); } const saved = result.stdout.trim().split(/\r?\n/).pop()?.trim() || "(unknown)"; return { content: [{ type: "text", text: `Screenshot saved to: ${saved}\nUse read to view it.` }], details: { path: saved, monitor: params.monitorIndex ?? "primary" }, }; }, }); pi.registerTool({ name: "hide_window", label: "Hide Window", description: 'Hide (or show) a window. Target a window by its exact title, or use "foreground" to target the currently focused window (default). Passing show:true restores a hidden window.', promptSnippet: "Hide or restore a window on screen", parameters: Type.Object({ title: Type.Optional(Type.String({ description: "Exact window title to target; omit or 'foreground' for the focused window" })), show: Type.Optional(Type.Boolean({ description: "true to restore/show a window, false to hide it (default)" })), }), async execute(_toolCallId, params, signal): Promise> { if (signal?.aborted) return { content: [{ type: "text", text: "Cancelled" }], details: {} }; const title = params.title?.trim() || "foreground"; const show = params.show === true ? "show" : "hide"; const result = await runPs(pi, HIDE_WINDOW_SCRIPT, [title, show], signal); if (result.code !== 0 || result.stdout.trim() === "NOT_FOUND") { throw new Error(`Window not found: "${title}". Use an exact window title, or omit to target the foreground window.`); } const verb = show === "show" ? "showed" : "hid"; return { content: [{ type: "text", text: `${verb} window: "${title === "foreground" ? "foreground" : title}"` }], details: { title, show: show === "show" }, }; }, }); }