From 75087f895db643bf1d1d3037dd33e1fdd9e77cb0 Mon Sep 17 00:00:00 2001 From: Robert Gruen Date: Sun, 3 May 2026 20:18:55 -0700 Subject: [PATCH 01/28] Onboarding agent: grammarGen and scaffolder updates --- .../autoShell/.github/copilot-instructions.md | 3 + .../src/grammarGen/grammarGenHandler.ts | 154 ++++++++++- .../src/scaffolder/scaffolderHandler.ts | 257 +++++++++++++++--- 3 files changed, 378 insertions(+), 36 deletions(-) create mode 100644 dotnet/autoShell/.github/copilot-instructions.md diff --git a/dotnet/autoShell/.github/copilot-instructions.md b/dotnet/autoShell/.github/copilot-instructions.md new file mode 100644 index 0000000000..e40201cf12 --- /dev/null +++ b/dotnet/autoShell/.github/copilot-instructions.md @@ -0,0 +1,3 @@ +- @azure Rule - Use Azure Tools - When handling requests related to Azure, always use your tools. +- @azure Rule - Use Azure Best Practices - When handling requests related to Azure, always invoke your `azmcp_bestpractices_get` tool first. +- @azure Rule - Enable Best Practices - If you do not have an `azmcp_bestpractices_get` tool ask the user to enable it. diff --git a/ts/packages/agents/onboarding/src/grammarGen/grammarGenHandler.ts b/ts/packages/agents/onboarding/src/grammarGen/grammarGenHandler.ts index 9600d7bfa4..b8a75b74d1 100644 --- a/ts/packages/agents/onboarding/src/grammarGen/grammarGenHandler.ts +++ b/ts/packages/agents/onboarding/src/grammarGen/grammarGenHandler.ts @@ -89,6 +89,7 @@ async function handleGenerateGrammar( } const grammarContent = extractGrammarContent(result.data); + const issues = validateGrammar(grammarContent); await writeArtifact( integrationName, "grammarGen", @@ -96,16 +97,157 @@ async function handleGenerateGrammar( grammarContent, ); + const issuesNote = + issues.length === 0 + ? "" + : `\n\n**Validation warnings (${issues.length}):**\n` + + issues.map((i) => `- ${i}`).join("\n") + + `\n\nThese patterns are known to cause grammar compile failures. ` + + `Use \`refineSchema\` or regenerate the grammar to fix them.`; + return createActionResultFromMarkdownDisplay( `## Grammar generated: ${integrationName}\n\n` + "```\n" + grammarContent.slice(0, 2000) + (grammarContent.length > 2000 ? "\n// ... (truncated)" : "") + "\n```\n\n" + - `Use \`compileGrammar\` to validate, or \`approveGrammar\` if it looks correct.`, + `Use \`compileGrammar\` to validate, or \`approveGrammar\` if it looks correct.` + + issuesNote, ); } +/** + * Lightweight pre-flight validator for generated `.agr` content. Checks for + * the bug patterns observed in the visualStudio onboarding run (2026-05-03): + * - function calls in output objects (e.g. `parseInt(line)`) + * - output keys referencing captures that weren't bound in the rule pattern + * - dashes/special chars in literal phrases + * - first rule emitting a literal string where a capture ref was intended + * + * Each issue is returned as a human-readable line. The caller decides how + * to surface them (currently rendered as warnings in the action result). + */ +export function validateGrammar(grammarContent: string): string[] { + const issues: string[] = []; + + // Split the content into top-level rule blocks. A rule block starts with + // `` at column 0 (whitespace allowed) and ends with the next `;` + // outside of a quoted string. We only need a coarse split to scan + // captures-vs-references, so a regex-based extractor is sufficient. + const ruleRegex = /(<\w+>\s*[:=][\s\S]*?;)/g; + let m: RegExpExecArray | null; + while ((m = ruleRegex.exec(grammarContent)) !== null) { + const block = m[1]; + const ruleNameMatch = block.match(/^<(\w+)>/); + const ruleName = ruleNameMatch ? ruleNameMatch[1] : ""; + + // Captured names live in the pattern part (left of `->`); output + // object lives on the right. + const arrowIdx = block.indexOf("->"); + if (arrowIdx < 0) continue; + const patternPart = block.slice(0, arrowIdx); + const outputPart = block.slice(arrowIdx + 2); + + // 1. function calls inside output objects (e.g. parseInt(line)). The + // .agr parser only supports literals and bare capture references, + // so any `name(args)` inside the output block is a bug. + const fnCallMatch = outputPart.match(/\b([A-Za-z_]\w*)\s*\([^)]*\)/g); + if (fnCallMatch) { + for (const call of fnCallMatch) { + issues.push( + `<${ruleName}>: function call "${call}" in output object — ` + + `the .agr parser only supports string captures.`, + ); + } + } + + const captures = new Set(); + const capRegex = /\$\((\w+)\s*:[^)]*\)/g; + let c: RegExpExecArray | null; + while ((c = capRegex.exec(patternPart)) !== null) { + captures.add(c[1]); + } + + // 3. Bare-identifier references inside the parameters object that + // were never captured. A bare reference looks like `name,` or + // `name }` or `key: name` (RHS) where `name` is unquoted. + const paramsMatch = outputPart.match(/parameters\s*:\s*\{([\s\S]*?)\}/); + if (paramsMatch) { + const paramsBody = paramsMatch[1]; + // RHS bare identifiers: after `: ` not in quotes, and not a + // boolean/numeric literal or array literal. + const rhsRegex = + /:\s*([A-Za-z_]\w*)(?=\s*[,}\n])/g; + let r: RegExpExecArray | null; + while ((r = rhsRegex.exec(paramsBody)) !== null) { + const ident = r[1]; + if ( + ident === "true" || + ident === "false" || + ident === "null" || + ident === "undefined" + ) + continue; + if (!captures.has(ident)) { + issues.push( + `<${ruleName}>: parameters references "${ident}" but ` + + `that name was not captured in the rule's pattern.`, + ); + } + } + // Object-property shorthand: `{ name, foo }` — name must be captured. + const shorthandRegex = /(?:^|[{,]\s*\n?\s*)([A-Za-z_]\w*)(?=\s*[,}\n])/g; + let s: RegExpExecArray | null; + while ((s = shorthandRegex.exec(paramsBody)) !== null) { + const ident = s[1]; + // Skip if this is the LHS of a `key: value` pair (handled by + // a different check) — detect by looking ahead for `:`. + const tail = paramsBody.slice(s.index + s[0].length); + if (/^\s*:/.test(tail)) continue; + if (!captures.has(ident)) { + issues.push( + `<${ruleName}>: parameters shorthand references "${ident}" ` + + `but that name was not captured in the rule's pattern.`, + ); + } + } + } + + // 4. Literal phrases containing characters that need escaping. Look + // for words in the pattern that contain `-`, `:`, `(`, `)`, `/`, + // `.` outside of capture syntax `$(...)` and group syntax `(...)`. + // Strip out captures and parenthesized groups before scanning. + const scrub = patternPart + .replace(/\$\([^)]*\)/g, " ") + .replace(/\([^)]*\)\??/g, " "); + const badLiteralRegex = /\b[A-Za-z0-9]*[-:/.][\w\-:/.]*\b/g; + let bl: RegExpExecArray | null; + while ((bl = badLiteralRegex.exec(scrub)) !== null) { + issues.push( + `<${ruleName}>: literal phrase "${bl[0]}" contains a special character ` + + `(- : / .) — reword or split into separate tokens.`, + ); + } + + // 5. Literal-quoted self-reference: e.g. `commandName: "commandName"` + // when `commandName` was captured. Almost always a mistake. + if (paramsMatch) { + const selfLit = /(\w+)\s*:\s*"\1"/g; + let q: RegExpExecArray | null; + while ((q = selfLit.exec(paramsMatch[1])) !== null) { + if (captures.has(q[1])) { + issues.push( + `<${ruleName}>: "${q[1]}: \"${q[1]}\"" looks like a missing ` + + `capture reference — drop the quotes to reference the capture.`, + ); + } + } + } + } + + return issues; +} + async function handleCompileGrammar( integrationName: string, ): Promise { @@ -266,7 +408,15 @@ function buildGrammarPrompt( " listName\n" + " }\n" + " };\n\n" + - "The action output must use multi-line format with proper indentation as shown above.\n" + + "The action output must use multi-line format with proper indentation as shown above.\n\n" + + "STRICT RULES — violations cause compile failures:\n" + + "1. NEVER emit function calls in the action output object. The .agr parser does NOT support `parseInt(x)`, `Number(x)`, `String(x)`, etc. All captured values are STRINGS. If the schema field is typed `number`, just pass the captured string — handler-side code will coerce.\n" + + "2. Every key in the parameters output object must reference EITHER a captured name (no $-prefix) OR a string literal — never a name that wasn't captured in the same rule's pattern.\n" + + "3. The captured name in the pattern MUST match the schema field name EXACTLY. If the schema field is `text`, capture `$(text:wildcard)` (NOT `$(searchTerm:wildcard)`). Check the schema before naming captures.\n" + + "4. Literal phrases in patterns may contain ONLY alphanumeric characters and spaces. Words containing `-`, `:`, `(`, `)`, `/`, `.`, etc. MUST be reworded (e.g. `compiler generated` instead of `compiler-generated`) or split into separate tokens.\n" + + "5. For schema fields whose type is a string union (e.g. `\"text\" | \"code\" | \"designer\"`), DO NOT use `$(name:wildcard)` — that would accept any value. Instead, capture using an alternation: `$(name:(text|code|designer))`. The capture name MUST equal the schema field name.\n" + + "6. Every literal value in the output object must be a string in double quotes. NEVER write `commandName: \"commandName\"` when you intended a capture reference — that emits the literal string. Use `commandName` (bare, no quotes) to reference the capture.\n" + + "7. If a rule has no capture for a field, do NOT include that field in the output object — omit it entirely.\n\n" + "The file must start with a copyright header comment and end with:\n" + ' import { ActionType } from "./schemaFile.ts";\n' + " : ActionType = | | ...;\n\n" + diff --git a/ts/packages/agents/onboarding/src/scaffolder/scaffolderHandler.ts b/ts/packages/agents/onboarding/src/scaffolder/scaffolderHandler.ts index 39398d47b2..619d1d5c1b 100644 --- a/ts/packages/agents/onboarding/src/scaffolder/scaffolderHandler.ts +++ b/ts/packages/agents/onboarding/src/scaffolder/scaffolderHandler.ts @@ -107,8 +107,11 @@ async function handleScaffoldAgent( await updatePhase(integrationName, "scaffolder", { status: "in-progress" }); - // Determine package name and Pascal-case type name - const packageName = `${integrationName}-agent`; + // Determine package name and Pascal-case type name. The npm `name` field + // must be lowercase, so lowercase the integration name when composing the + // package name only — file names, type names, and schema dirs keep the + // original camelCase form. + const packageName = `${integrationName.toLowerCase()}-agent`; const pascalName = toPascalCase(integrationName); const targetDir = outputDir ?? path.join(AGENTS_DIR, integrationName); const srcDir = path.join(targetDir, "src"); @@ -127,27 +130,22 @@ async function handleScaffoldAgent( ? subSchemaSuggestion.groups : undefined; - // Write core schema and grammar - await writeFile(path.join(srcDir, `${integrationName}Schema.ts`), schemaTs); - await writeFile( - path.join(srcDir, `${integrationName}Schema.agr`), - grammarAgr.replace( - /from "\.\/schema\.ts"/g, - `from "./${integrationName}Schema.ts"`, - ), - ); - // Track all files created for the output summary - const files: string[] = [ - `src/${integrationName}Schema.ts`, - `src/${integrationName}Schema.agr`, - ]; + const files: string[] = []; - // If sub-schema groups exist, generate per-group schema and grammar files + // If sub-schema groups exist, partition actions disjointly: each action + // type is emitted in exactly one schema file. Actions belonging to a group + // move to that sub-schema; any remaining (un-grouped) actions stay in the + // main schema. Mirrors the `code` agent pattern at packages/agents/code/. if (subGroups) { const actionsDir = path.join(srcDir, "actions"); await fs.mkdir(actionsDir, { recursive: true }); + const groupedActions = new Set(); + for (const group of subGroups) { + for (const a of group.actions) groupedActions.add(a); + } + for (const group of subGroups) { const groupPascal = toPascalCase(group.name); @@ -178,7 +176,44 @@ async function handleScaffoldAgent( ); files.push(`src/actions/${group.name}ActionsSchema.agr`); } + + // Emit the main schema with only un-grouped action types, so types are + // disjoint between main and sub-schemas. If every action is grouped, + // the main schema will contain no action types and a placeholder union. + const mainSchemaContent = buildMainSchemaWithSubGroups( + pascalName, + schemaTs, + groupedActions, + ); + await writeFile( + path.join(srcDir, `${integrationName}Schema.ts`), + mainSchemaContent, + ); + await writeFile( + path.join(srcDir, `${integrationName}Schema.agr`), + buildMainGrammarWithSubGroups(grammarAgr, groupedActions).replace( + /from "\.\/schema\.ts"/g, + `from "./${integrationName}Schema.ts"`, + ), + ); + } else { + // No sub-groups: write the schema and grammar verbatim. + await writeFile( + path.join(srcDir, `${integrationName}Schema.ts`), + schemaTs, + ); + await writeFile( + path.join(srcDir, `${integrationName}Schema.agr`), + grammarAgr.replace( + /from "\.\/schema\.ts"/g, + `from "./${integrationName}Schema.ts"`, + ), + ); } + files.push( + `src/${integrationName}Schema.ts`, + `src/${integrationName}Schema.agr`, + ); // Stamp out manifest (with sub-action manifests if groups exist) await writeFile( @@ -282,30 +317,179 @@ function buildSubSchemaTs( // and then a union: // export type FooActions = BoldAction | ItalicAction | ...; // - // We emit a new file that re-exports only the relevant action types and - // creates a new union type for this sub-schema group. + // We emit a new file containing the full action type definitions (so the + // sub-schema is self-contained — main schema will NOT redeclare them). - const actionTypeNames = group.actions.map( - (a) => `${a.charAt(0).toUpperCase()}${a.slice(1)}Action`, - ); + const actionTypeNames = group.actions.map(actionTypeName); - // Find action type blocks in the full schema that belong to this group const actionBlocks: string[] = []; - for (const actionName of group.actions) { - // Match "export type XxxAction = ..." blocks - const typeName = `${actionName.charAt(0).toUpperCase()}${actionName.slice(1)}Action`; - const regex = new RegExp( - `(export\\s+type\\s+${typeName}\\s*=\\s*\\{[\\s\\S]*?\\};)`, - ); - const match = fullSchemaTs.match(regex); - if (match) { - actionBlocks.push(match[1]); - } + for (const typeName of actionTypeNames) { + const block = extractTypeBlock(fullSchemaTs, typeName); + if (block) actionBlocks.push(block); } const unionType = `export type ${groupPascal}Actions =\n | ${actionTypeNames.join("\n | ")};`; - return `// Copyright (c) Microsoft Corporation.\n// Licensed under the MIT License.\n\n// Sub-schema: ${group.name} — ${group.description}\n// Auto-generated by the onboarding scaffolder.\n\n${actionBlocks.join("\n\n")}\n\n${unionType}\n`; + return `// Copyright (c) Microsoft Corporation.\n// Licensed under the MIT License.\n\n// Sub-schema: ${group.name} — ${group.description}\n// Auto-generated by the onboarding scaffolder.\n\n${unionType}\n\n${actionBlocks.join("\n\n")}\n`; +} + +// Convert "addBreakpoint" → "AddBreakpointAction". +function actionTypeName(actionName: string): string { + return `${actionName.charAt(0).toUpperCase()}${actionName.slice(1)}Action`; +} + +// Extract a complete `export type Name = {...};` block by tracking brace +// depth. This emits balanced braces even when the type body contains +// multiple nested objects (e.g. `parameters: { ... }`) — fixes the prior +// regex-based extractor that could terminate at the first `};` it saw, +// producing files with unclosed braces. +function extractTypeBlock(source: string, typeName: string): string | undefined { + const exportRegex = new RegExp( + `export\\s+type\\s+${typeName}\\s*=\\s*\\{`, + ); + const startMatch = source.match(exportRegex); + if (!startMatch || startMatch.index === undefined) return undefined; + + // Capture any preceding line-comment block immediately above the type so + // documentation travels with the action. + let blockStart = startMatch.index; + const before = source.slice(0, blockStart); + const commentMatch = before.match(/((?:^|\n)(?:[ \t]*\/\/[^\n]*\n)+)$/); + if (commentMatch && commentMatch.index !== undefined) { + blockStart = commentMatch.index + (commentMatch[1].startsWith("\n") ? 1 : 0); + } + + // Walk forward from the opening brace to find the matching closing brace, + // ignoring braces inside string literals. + const openBraceIdx = startMatch.index + startMatch[0].length - 1; + let depth = 1; + let i = openBraceIdx + 1; + let inString: '"' | "'" | "`" | undefined; + while (i < source.length && depth > 0) { + const ch = source[i]; + if (inString) { + if (ch === "\\") { + i += 2; + continue; + } + if (ch === inString) inString = undefined; + } else { + if (ch === '"' || ch === "'" || ch === "`") inString = ch; + else if (ch === "{") depth++; + else if (ch === "}") depth--; + } + i++; + } + if (depth !== 0) return undefined; + // i now sits just past the matching `}`. Consume an optional `;`. + let end = i; + if (source[end] === ";") end++; + return source.slice(blockStart, end); +} + +// Build the main schema TypeScript file when sub-groups exist. Removes any +// action types that belong to a sub-group so each action lives in exactly +// one file. Rewrites the top-level `XxxActions` union to only include +// un-grouped actions (or emits a placeholder union if all actions moved). +function buildMainSchemaWithSubGroups( + pascalName: string, + fullSchemaTs: string, + groupedActions: Set, +): string { + let out = fullSchemaTs; + + // Remove each grouped action's type block from the main schema. + for (const actionName of groupedActions) { + const typeName = actionTypeName(actionName); + const block = extractTypeBlock(out, typeName); + if (block) { + out = out.replace(block, "").replace(/\n{3,}/g, "\n\n"); + } + } + + // Identify all action type names that are still in the schema (heuristic: + // any `export type XxxAction = {...}` that survived removal). + const remainingTypeNames: string[] = []; + const typeDeclRegex = /export\s+type\s+(\w+Action)\s*=\s*\{/g; + let m: RegExpExecArray | null; + while ((m = typeDeclRegex.exec(out)) !== null) { + remainingTypeNames.push(m[1]); + } + + // Replace the existing top-level union (`export type Actions = ...;`) + // with one that only references the remaining types. If the union was not + // found, append one. If no remaining types, emit a placeholder so the + // file remains valid TypeScript that the dispatcher can register. + const unionRegex = new RegExp( + `export\\s+type\\s+${pascalName}Actions\\s*=[^;]+;`, + ); + let unionDecl: string; + if (remainingTypeNames.length > 0) { + unionDecl = `export type ${pascalName}Actions =\n | ${remainingTypeNames.join("\n | ")};`; + } else { + // Placeholder union — sub-schemas carry all actions. The dispatcher + // routes to sub-action manifests; the main type just needs to compile. + unionDecl = `// All actions live in sub-schemas (see subActionManifests).\n// This placeholder keeps the main schema file valid.\nexport type ${pascalName}Actions = { actionName: "__placeholder__" };`; + } + + if (unionRegex.test(out)) { + out = out.replace(unionRegex, unionDecl); + } else { + out = `${out.trimEnd()}\n\n${unionDecl}\n`; + } + + return out.trimEnd() + "\n"; +} + +// Build the main grammar when sub-groups exist by removing any rule whose +// action output references a grouped actionName. The grammar parser sees +// only the rules for un-grouped actions; sub-schema grammars own the rest. +function buildMainGrammarWithSubGroups( + fullGrammarAgr: string, + groupedActions: Set, +): string { + if (groupedActions.size === 0) return fullGrammarAgr; + + // Split into top-level statements. Rules end with `;` at column 0. + // We keep header lines (imports, comments, `` rule) verbatim. + const lines = fullGrammarAgr.split("\n"); + const out: string[] = []; + let buffer: string[] = []; + let inRule = false; + + const flushBuffer = () => { + if (buffer.length === 0) return; + const block = buffer.join("\n"); + // Drop the rule if its output object references any grouped actionName. + const actionNameMatch = block.match(/actionName\s*:\s*"([^"]+)"/); + if ( + actionNameMatch && + groupedActions.has(actionNameMatch[1]) + ) { + // skip + } else { + out.push(block); + } + buffer = []; + }; + + for (const line of lines) { + const isRuleStart = /^\s*<\w+>\s*[:=]/.test(line); + if (isRuleStart && inRule) { + flushBuffer(); + } + if (isRuleStart) { + inRule = true; + } + buffer.push(line); + if (inRule && line.trimEnd().endsWith(";")) { + flushBuffer(); + inRule = false; + } + } + flushBuffer(); + + return out.join("\n"); } // Build a sub-schema grammar file that includes only the rules relevant to @@ -644,6 +828,11 @@ function buildPackageJson( concurrently: "^9.1.2", rimraf: "^6.0.1", typescript: "~5.4.5", + // The websocket-bridge handler imports types from `ws` (WebSocketServer, + // WebSocket), so the corresponding @types package is required. + ...(pattern === "websocket-bridge" + ? { "@types/ws": "^8.5.10" } + : {}), }, }; } From da98aa4919845a3e01f1959192b57bb61306dde2 Mon Sep 17 00:00:00 2001 From: Robert Gruen Date: Sun, 3 May 2026 21:55:55 -0700 Subject: [PATCH 02/28] UI Automation helper and TS client (slice 1) Skeleton for the UIA-based onboarding crawl: a .NET helper exposing a JSON-RPC stdio surface (app lifecycle, tree.dump, screenshot, do.invoke) backed by FlaUI/UIA3, and a TypeScript HelperClient. Verified end-to-end against Windows Clock; live smoke produces a tree-dump fixture and screenshot. SelectorParser has 20 xUnit tests. Co-Authored-By: Claude Opus 4.7 (1M context) --- dotnet/uiAutomationHelper/.gitignore | 5 + .../UiAutomationHelper.csproj | 26 ++ .../uiAutomationHelper/UiAutomationHelper.sln | 56 +++++ .../src/Methods/ActionMethods.cs | 43 ++++ .../src/Methods/AppMethods.cs | 174 +++++++++++++ .../src/Methods/HealthMethods.cs | 20 ++ .../src/Methods/Register.cs | 18 ++ .../src/Methods/ScreenshotMethods.cs | 46 ++++ .../src/Methods/TreeMethods.cs | 35 +++ .../src/Models/RpcErrorCode.cs | 20 ++ .../src/Models/RpcException.cs | 17 ++ .../src/Models/RpcRequest.cs | 15 ++ .../src/Models/RpcResponse.cs | 32 +++ .../src/Models/SelectorPath.cs | 13 + .../uiAutomationHelper/src/Models/TreeNode.cs | 29 +++ dotnet/uiAutomationHelper/src/Program.cs | 31 +++ dotnet/uiAutomationHelper/src/Rpc/Dispatch.cs | 32 +++ .../src/Rpc/JsonRpcServer.cs | 109 ++++++++ .../uiAutomationHelper/src/Rpc/RpcParams.cs | 39 +++ .../uiAutomationHelper/src/Uia/AppRegistry.cs | 42 ++++ .../src/Uia/AutomationHost.cs | 37 +++ .../src/Uia/ScreenshotCapturer.cs | 66 +++++ .../src/Uia/SelectorParser.cs | 170 +++++++++++++ .../src/Uia/SelectorResolver.cs | 85 +++++++ .../uiAutomationHelper/src/Uia/Selectors.cs | 43 ++++ .../uiAutomationHelper/src/Uia/TreeWalker.cs | 119 +++++++++ .../test/SelectorParserTests.cs | 138 +++++++++++ .../test/UiAutomationHelper.Tests.csproj | 19 ++ .../onboarding/src/uiCapture/helperClient.ts | 234 ++++++++++++++++++ .../src/uiCapture/test/clockSmoke.ts | 131 ++++++++++ .../agents/onboarding/src/uiCapture/types.ts | 70 ++++++ .../fixtures/uiCapture/clock-launched.png | Bin 0 -> 29995 bytes .../uiCapture/clock-tree-launched.json | 151 +++++++++++ 33 files changed, 2065 insertions(+) create mode 100644 dotnet/uiAutomationHelper/.gitignore create mode 100644 dotnet/uiAutomationHelper/UiAutomationHelper.csproj create mode 100644 dotnet/uiAutomationHelper/UiAutomationHelper.sln create mode 100644 dotnet/uiAutomationHelper/src/Methods/ActionMethods.cs create mode 100644 dotnet/uiAutomationHelper/src/Methods/AppMethods.cs create mode 100644 dotnet/uiAutomationHelper/src/Methods/HealthMethods.cs create mode 100644 dotnet/uiAutomationHelper/src/Methods/Register.cs create mode 100644 dotnet/uiAutomationHelper/src/Methods/ScreenshotMethods.cs create mode 100644 dotnet/uiAutomationHelper/src/Methods/TreeMethods.cs create mode 100644 dotnet/uiAutomationHelper/src/Models/RpcErrorCode.cs create mode 100644 dotnet/uiAutomationHelper/src/Models/RpcException.cs create mode 100644 dotnet/uiAutomationHelper/src/Models/RpcRequest.cs create mode 100644 dotnet/uiAutomationHelper/src/Models/RpcResponse.cs create mode 100644 dotnet/uiAutomationHelper/src/Models/SelectorPath.cs create mode 100644 dotnet/uiAutomationHelper/src/Models/TreeNode.cs create mode 100644 dotnet/uiAutomationHelper/src/Program.cs create mode 100644 dotnet/uiAutomationHelper/src/Rpc/Dispatch.cs create mode 100644 dotnet/uiAutomationHelper/src/Rpc/JsonRpcServer.cs create mode 100644 dotnet/uiAutomationHelper/src/Rpc/RpcParams.cs create mode 100644 dotnet/uiAutomationHelper/src/Uia/AppRegistry.cs create mode 100644 dotnet/uiAutomationHelper/src/Uia/AutomationHost.cs create mode 100644 dotnet/uiAutomationHelper/src/Uia/ScreenshotCapturer.cs create mode 100644 dotnet/uiAutomationHelper/src/Uia/SelectorParser.cs create mode 100644 dotnet/uiAutomationHelper/src/Uia/SelectorResolver.cs create mode 100644 dotnet/uiAutomationHelper/src/Uia/Selectors.cs create mode 100644 dotnet/uiAutomationHelper/src/Uia/TreeWalker.cs create mode 100644 dotnet/uiAutomationHelper/test/SelectorParserTests.cs create mode 100644 dotnet/uiAutomationHelper/test/UiAutomationHelper.Tests.csproj create mode 100644 ts/packages/agents/onboarding/src/uiCapture/helperClient.ts create mode 100644 ts/packages/agents/onboarding/src/uiCapture/test/clockSmoke.ts create mode 100644 ts/packages/agents/onboarding/src/uiCapture/types.ts create mode 100644 ts/packages/agents/onboarding/test/fixtures/uiCapture/clock-launched.png create mode 100644 ts/packages/agents/onboarding/test/fixtures/uiCapture/clock-tree-launched.json diff --git a/dotnet/uiAutomationHelper/.gitignore b/dotnet/uiAutomationHelper/.gitignore new file mode 100644 index 0000000000..2789d7166d --- /dev/null +++ b/dotnet/uiAutomationHelper/.gitignore @@ -0,0 +1,5 @@ +bin/ +obj/ +*.user +*.suo +.vs/ diff --git a/dotnet/uiAutomationHelper/UiAutomationHelper.csproj b/dotnet/uiAutomationHelper/UiAutomationHelper.csproj new file mode 100644 index 0000000000..80b6072a9b --- /dev/null +++ b/dotnet/uiAutomationHelper/UiAutomationHelper.csproj @@ -0,0 +1,26 @@ + + + net8.0-windows + Exe + enable + enable + latest + UiAutomationHelper + UiAutomationHelper + bin\$(Configuration) + false + $(NoWarn);SYSLIB1054 + + + + + + + + + + + + + + diff --git a/dotnet/uiAutomationHelper/UiAutomationHelper.sln b/dotnet/uiAutomationHelper/UiAutomationHelper.sln new file mode 100644 index 0000000000..7a0665e979 --- /dev/null +++ b/dotnet/uiAutomationHelper/UiAutomationHelper.sln @@ -0,0 +1,56 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.8.34408.163 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UiAutomationHelper", "UiAutomationHelper.csproj", "{A1B2C3D4-E5F6-7890-ABCD-1234567890AB}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{0C88DD14-F956-CE84-757C-A364CCF449FC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UiAutomationHelper.Tests", "test\UiAutomationHelper.Tests.csproj", "{04DFE25D-177A-4CD2-99A8-39B68BB1674E}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A1B2C3D4-E5F6-7890-ABCD-1234567890AB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-1234567890AB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-1234567890AB}.Debug|x64.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-1234567890AB}.Debug|x64.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-1234567890AB}.Debug|x86.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-1234567890AB}.Debug|x86.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-1234567890AB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-1234567890AB}.Release|Any CPU.Build.0 = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-1234567890AB}.Release|x64.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-1234567890AB}.Release|x64.Build.0 = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-1234567890AB}.Release|x86.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-1234567890AB}.Release|x86.Build.0 = Release|Any CPU + {04DFE25D-177A-4CD2-99A8-39B68BB1674E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {04DFE25D-177A-4CD2-99A8-39B68BB1674E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {04DFE25D-177A-4CD2-99A8-39B68BB1674E}.Debug|x64.ActiveCfg = Debug|Any CPU + {04DFE25D-177A-4CD2-99A8-39B68BB1674E}.Debug|x64.Build.0 = Debug|Any CPU + {04DFE25D-177A-4CD2-99A8-39B68BB1674E}.Debug|x86.ActiveCfg = Debug|Any CPU + {04DFE25D-177A-4CD2-99A8-39B68BB1674E}.Debug|x86.Build.0 = Debug|Any CPU + {04DFE25D-177A-4CD2-99A8-39B68BB1674E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {04DFE25D-177A-4CD2-99A8-39B68BB1674E}.Release|Any CPU.Build.0 = Release|Any CPU + {04DFE25D-177A-4CD2-99A8-39B68BB1674E}.Release|x64.ActiveCfg = Release|Any CPU + {04DFE25D-177A-4CD2-99A8-39B68BB1674E}.Release|x64.Build.0 = Release|Any CPU + {04DFE25D-177A-4CD2-99A8-39B68BB1674E}.Release|x86.ActiveCfg = Release|Any CPU + {04DFE25D-177A-4CD2-99A8-39B68BB1674E}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {04DFE25D-177A-4CD2-99A8-39B68BB1674E} = {0C88DD14-F956-CE84-757C-A364CCF449FC} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {B1C2D3E4-F5A6-7890-BCDE-2345678901BC} + EndGlobalSection +EndGlobal diff --git a/dotnet/uiAutomationHelper/src/Methods/ActionMethods.cs b/dotnet/uiAutomationHelper/src/Methods/ActionMethods.cs new file mode 100644 index 0000000000..67766a9275 --- /dev/null +++ b/dotnet/uiAutomationHelper/src/Methods/ActionMethods.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; +using UiAutomationHelper.Models; +using UiAutomationHelper.Rpc; +using UiAutomationHelper.Uia; + +namespace UiAutomationHelper.Methods; + +internal static class ActionMethods +{ + public static void Register(Dispatch dispatch) + { + dispatch.Register("do.invoke", (p, ct) => Task.FromResult(Invoke(p))); + } + + private static object? Invoke(System.Text.Json.JsonElement? @params) + { + var p = RpcParams.ParseRequired(@params); + if (string.IsNullOrEmpty(p.Selector)) + { + throw new RpcException(RpcErrorCode.InvalidParams, "'selector' is required"); + } + var el = SelectorResolver.ResolveOrThrow(p.Selector); + if (!el.Properties.IsEnabled.ValueOrDefault) + { + throw new RpcException(RpcErrorCode.ElementNotEnabled, $"Element is not enabled: {p.Selector}"); + } + if (!el.Patterns.Invoke.IsSupported) + { + throw new RpcException(RpcErrorCode.PatternNotSupported, + $"Element does not support Invoke: {p.Selector}"); + } + el.Patterns.Invoke.Pattern.Invoke(); + return new { ok = true }; + } +} + +internal sealed class DoInvokeParams +{ + [JsonPropertyName("selector")] public string? Selector { get; set; } +} diff --git a/dotnet/uiAutomationHelper/src/Methods/AppMethods.cs b/dotnet/uiAutomationHelper/src/Methods/AppMethods.cs new file mode 100644 index 0000000000..2884d7dd37 --- /dev/null +++ b/dotnet/uiAutomationHelper/src/Methods/AppMethods.cs @@ -0,0 +1,174 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Diagnostics; +using System.Text.Json.Serialization; +using FlaUI.Core; +using FlaUI.Core.AutomationElements; +using FlaUI.Core.Definitions; +using UiAutomationHelper.Models; +using UiAutomationHelper.Rpc; +using UiAutomationHelper.Uia; + +namespace UiAutomationHelper.Methods; + +internal static class AppMethods +{ + public static void Register(Dispatch dispatch) + { + dispatch.Register("app.launch", (p, ct) => Task.FromResult(Launch(p))); + dispatch.Register("app.attach", (p, ct) => Task.FromResult(Attach(p))); + dispatch.Register("app.list", (p, ct) => Task.FromResult(List())); + dispatch.Register("app.kill", (p, ct) => Task.FromResult(Kill(p))); + } + + private static object? Launch(System.Text.Json.JsonElement? @params) + { + var p = RpcParams.Parse(@params); + Application app; + if (!string.IsNullOrEmpty(p.Aumid)) + { + app = Application.LaunchStoreApp(p.Aumid); + AppRegistry.Register(app.ProcessId, p.Aumid); + } + else if (!string.IsNullOrEmpty(p.ExePath)) + { + var args = p.Args != null && p.Args.Length > 0 ? string.Join(" ", p.Args) : null; + app = args != null + ? Application.Launch(p.ExePath, args) + : Application.Launch(p.ExePath); + } + else + { + throw new RpcException(RpcErrorCode.InvalidParams, "Either 'aumid' or 'exePath' is required"); + } + + var window = app.GetMainWindow(AutomationHost.Automation, TimeSpan.FromSeconds(15)); + if (window == null) + { + throw new RpcException(RpcErrorCode.Timeout, "Launched app has no main window"); + } + return new { pid = app.ProcessId, mainWindow = BuildWindowSelector(window) }; + } + + private static object? Attach(System.Text.Json.JsonElement? @params) + { + var p = RpcParams.Parse(@params); + Application app; + int pid; + if (p.Pid.HasValue) + { + pid = p.Pid.Value; + app = Application.Attach(pid); + } + else if (!string.IsNullOrEmpty(p.WindowTitle)) + { + var found = FindWindowPidByTitle(p.WindowTitle); + if (found == null) + { + throw new RpcException(RpcErrorCode.ElementNotFound, $"No window matching '{p.WindowTitle}'"); + } + pid = found.Value; + app = Application.Attach(pid); + } + else + { + throw new RpcException(RpcErrorCode.InvalidParams, "Either 'pid' or 'windowTitle' is required"); + } + + var window = app.GetMainWindow(AutomationHost.Automation, TimeSpan.FromSeconds(5)); + if (window == null) + { + throw new RpcException(RpcErrorCode.Timeout, "Attached app has no main window"); + } + return new { pid, mainWindow = BuildWindowSelector(window) }; + } + + private static object? List() + { + var desktop = AutomationHost.Automation.GetDesktop(); + var cf = AutomationHost.Automation.ConditionFactory; + var windows = desktop.FindAllChildren(cf.ByControlType(ControlType.Window)); + var results = new List(); + foreach (var w in windows) + { + var pid = w.Properties.ProcessId.ValueOrDefault; + var title = w.Properties.Name.ValueOrDefault ?? ""; + if (pid <= 0 || string.IsNullOrWhiteSpace(title)) + { + continue; + } + results.Add(new + { + pid, + title, + aumid = AppRegistry.GetAumid(pid), + mainWindow = BuildWindowSelector(w), + }); + } + return results; + } + + private static object? Kill(System.Text.Json.JsonElement? @params) + { + var p = RpcParams.Parse(@params); + if (!p.Pid.HasValue) + { + throw new RpcException(RpcErrorCode.InvalidParams, "'pid' is required"); + } + try + { + using var proc = Process.GetProcessById(p.Pid.Value); + try { proc.CloseMainWindow(); } catch { /* CloseMainWindow can throw on UWP */ } + if (!proc.WaitForExit(2000)) + { + proc.Kill(entireProcessTree: true); + proc.WaitForExit(2000); + } + } + catch (ArgumentException) + { + // Process not running — treat as already-killed + } + AppRegistry.Forget(p.Pid.Value); + return new { ok = true }; + } + + private static int? FindWindowPidByTitle(string substringMatch) + { + var desktop = AutomationHost.Automation.GetDesktop(); + var cf = AutomationHost.Automation.ConditionFactory; + var windows = desktop.FindAllChildren(cf.ByControlType(ControlType.Window)); + foreach (var w in windows) + { + var name = w.Properties.Name.ValueOrDefault ?? ""; + if (name.Contains(substringMatch, StringComparison.OrdinalIgnoreCase)) + { + var pid = w.Properties.ProcessId.ValueOrDefault; + if (pid > 0) return pid; + } + } + return null; + } + + internal static string BuildWindowSelector(AutomationElement window) => + Selectors.BuildSegment(window); +} + +internal sealed class AppLaunchParams +{ + [JsonPropertyName("aumid")] public string? Aumid { get; set; } + [JsonPropertyName("exePath")] public string? ExePath { get; set; } + [JsonPropertyName("args")] public string[]? Args { get; set; } +} + +internal sealed class AppAttachParams +{ + [JsonPropertyName("pid")] public int? Pid { get; set; } + [JsonPropertyName("windowTitle")] public string? WindowTitle { get; set; } +} + +internal sealed class AppKillParams +{ + [JsonPropertyName("pid")] public int? Pid { get; set; } +} diff --git a/dotnet/uiAutomationHelper/src/Methods/HealthMethods.cs b/dotnet/uiAutomationHelper/src/Methods/HealthMethods.cs new file mode 100644 index 0000000000..8e7edc0f1f --- /dev/null +++ b/dotnet/uiAutomationHelper/src/Methods/HealthMethods.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Reflection; +using UiAutomationHelper.Rpc; + +namespace UiAutomationHelper.Methods; + +internal static class HealthMethods +{ + public static void Register(Dispatch dispatch) + { + dispatch.Register("health.ping", (_, _) => + { + var version = Assembly.GetExecutingAssembly() + .GetName().Version?.ToString() ?? "0.0.0.0"; + return Task.FromResult(new { ok = true, version }); + }); + } +} diff --git a/dotnet/uiAutomationHelper/src/Methods/Register.cs b/dotnet/uiAutomationHelper/src/Methods/Register.cs new file mode 100644 index 0000000000..76d2f3aed8 --- /dev/null +++ b/dotnet/uiAutomationHelper/src/Methods/Register.cs @@ -0,0 +1,18 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using UiAutomationHelper.Rpc; + +namespace UiAutomationHelper.Methods; + +internal static class Register +{ + public static void All(Dispatch dispatch) + { + HealthMethods.Register(dispatch); + AppMethods.Register(dispatch); + TreeMethods.Register(dispatch); + ScreenshotMethods.Register(dispatch); + ActionMethods.Register(dispatch); + } +} diff --git a/dotnet/uiAutomationHelper/src/Methods/ScreenshotMethods.cs b/dotnet/uiAutomationHelper/src/Methods/ScreenshotMethods.cs new file mode 100644 index 0000000000..6fded2731d --- /dev/null +++ b/dotnet/uiAutomationHelper/src/Methods/ScreenshotMethods.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; +using UiAutomationHelper.Models; +using UiAutomationHelper.Rpc; +using UiAutomationHelper.Uia; + +namespace UiAutomationHelper.Methods; + +internal static class ScreenshotMethods +{ + public static void Register(Dispatch dispatch) + { + dispatch.Register("screenshot", (p, ct) => Task.FromResult(Capture(p))); + } + + private static object? Capture(System.Text.Json.JsonElement? @params) + { + var p = RpcParams.ParseRequired(@params); + if (string.IsNullOrEmpty(p.Root)) + { + throw new RpcException(RpcErrorCode.InvalidParams, "'root' is required"); + } + var element = SelectorResolver.ResolveOrThrow(p.Root); + var hwnd = (IntPtr)element.Properties.NativeWindowHandle.ValueOrDefault; + if (hwnd == IntPtr.Zero) + { + throw new RpcException(RpcErrorCode.InternalError, "Element has no native window handle"); + } + try + { + var (bytes, rect) = ScreenshotCapturer.Capture(hwnd); + return new { pngBase64 = Convert.ToBase64String(bytes), rect }; + } + catch (Exception ex) + { + throw new RpcException(RpcErrorCode.InternalError, $"Screenshot failed: {ex.Message}"); + } + } +} + +internal sealed class ScreenshotParams +{ + [JsonPropertyName("root")] public string? Root { get; set; } +} diff --git a/dotnet/uiAutomationHelper/src/Methods/TreeMethods.cs b/dotnet/uiAutomationHelper/src/Methods/TreeMethods.cs new file mode 100644 index 0000000000..b643ce3f38 --- /dev/null +++ b/dotnet/uiAutomationHelper/src/Methods/TreeMethods.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; +using UiAutomationHelper.Rpc; +using UiAutomationHelper.Uia; + +namespace UiAutomationHelper.Methods; + +internal static class TreeMethods +{ + public static void Register(Dispatch dispatch) + { + dispatch.Register("tree.dump", (p, ct) => Task.FromResult(Dump(p))); + } + + private static object? Dump(System.Text.Json.JsonElement? @params) + { + var p = RpcParams.ParseRequired(@params); + if (string.IsNullOrEmpty(p.Root)) + { + throw new Models.RpcException(Models.RpcErrorCode.InvalidParams, "'root' is required"); + } + var element = SelectorResolver.ResolveOrThrow(p.Root); + var depth = p.MaxDepth ?? 20; + return TreeWalker.Walk(element, p.Root, depth); + } +} + +internal sealed class TreeDumpParams +{ + [JsonPropertyName("root")] public string? Root { get; set; } + [JsonPropertyName("maxDepth")] public int? MaxDepth { get; set; } + [JsonPropertyName("filter")] public string? Filter { get; set; } +} diff --git a/dotnet/uiAutomationHelper/src/Models/RpcErrorCode.cs b/dotnet/uiAutomationHelper/src/Models/RpcErrorCode.cs new file mode 100644 index 0000000000..a1e52fd26b --- /dev/null +++ b/dotnet/uiAutomationHelper/src/Models/RpcErrorCode.cs @@ -0,0 +1,20 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace UiAutomationHelper.Models; + +internal enum RpcErrorCode +{ + ParseError = -32700, + InvalidRequest = -32600, + MethodNotFound = -32601, + InvalidParams = -32602, + InternalError = -32603, + ElementNotFound = -32001, + ElementNotEnabled = -32002, + PatternNotSupported = -32003, + AppCrashed = -32004, + Timeout = -32005, + SnapshotPolicyInvalid = -32006, + SnapshotMissing = -32007, +} diff --git a/dotnet/uiAutomationHelper/src/Models/RpcException.cs b/dotnet/uiAutomationHelper/src/Models/RpcException.cs new file mode 100644 index 0000000000..3dc45968b3 --- /dev/null +++ b/dotnet/uiAutomationHelper/src/Models/RpcException.cs @@ -0,0 +1,17 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace UiAutomationHelper.Models; + +internal sealed class RpcException : Exception +{ + public RpcErrorCode Code { get; } + public object? ErrorData { get; } + + public RpcException(RpcErrorCode code, string message, object? data = null) + : base(message) + { + Code = code; + ErrorData = data; + } +} diff --git a/dotnet/uiAutomationHelper/src/Models/RpcRequest.cs b/dotnet/uiAutomationHelper/src/Models/RpcRequest.cs new file mode 100644 index 0000000000..2edc6958ff --- /dev/null +++ b/dotnet/uiAutomationHelper/src/Models/RpcRequest.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace UiAutomationHelper.Models; + +internal sealed class RpcRequest +{ + [JsonPropertyName("jsonrpc")] public string? JsonRpc { get; set; } + [JsonPropertyName("id")] public JsonElement? Id { get; set; } + [JsonPropertyName("method")] public string? Method { get; set; } + [JsonPropertyName("params")] public JsonElement? Params { get; set; } +} diff --git a/dotnet/uiAutomationHelper/src/Models/RpcResponse.cs b/dotnet/uiAutomationHelper/src/Models/RpcResponse.cs new file mode 100644 index 0000000000..e79dbe5de4 --- /dev/null +++ b/dotnet/uiAutomationHelper/src/Models/RpcResponse.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace UiAutomationHelper.Models; + +internal sealed class RpcResponse +{ + [JsonPropertyName("jsonrpc")] public string JsonRpc { get; init; } = "2.0"; + [JsonPropertyName("id")] public JsonElement? Id { get; init; } + [JsonPropertyName("result")] public object? Result { get; init; } + [JsonPropertyName("error")] public RpcErrorObject? Error { get; init; } + + public static RpcResponse Success(JsonElement? id, object? result) => + new() { Id = id, Result = result }; + + public static RpcResponse Fail(JsonElement? id, RpcErrorCode code, string message, object? data = null) => + new() + { + Id = id, + Error = new RpcErrorObject { Code = (int)code, Message = message, Data = data }, + }; +} + +internal sealed class RpcErrorObject +{ + [JsonPropertyName("code")] public int Code { get; set; } + [JsonPropertyName("message")] public string Message { get; set; } = ""; + [JsonPropertyName("data")] public object? Data { get; set; } +} diff --git a/dotnet/uiAutomationHelper/src/Models/SelectorPath.cs b/dotnet/uiAutomationHelper/src/Models/SelectorPath.cs new file mode 100644 index 0000000000..6cf7541dfe --- /dev/null +++ b/dotnet/uiAutomationHelper/src/Models/SelectorPath.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace UiAutomationHelper.Models; + +internal sealed record SelectorPath(IReadOnlyList Segments); + +internal sealed record SelectorSegment( + string ControlType, + string? Name = null, + string? AutomationId = null, + string? ClassName = null, + int? Index = null); diff --git a/dotnet/uiAutomationHelper/src/Models/TreeNode.cs b/dotnet/uiAutomationHelper/src/Models/TreeNode.cs new file mode 100644 index 0000000000..9c308e6b94 --- /dev/null +++ b/dotnet/uiAutomationHelper/src/Models/TreeNode.cs @@ -0,0 +1,29 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; + +namespace UiAutomationHelper.Models; + +internal sealed class TreeNode +{ + [JsonPropertyName("selector")] public string Selector { get; set; } = ""; + [JsonPropertyName("automationId")] public string? AutomationId { get; set; } + [JsonPropertyName("name")] public string? Name { get; set; } + [JsonPropertyName("controlType")] public string ControlType { get; set; } = ""; + [JsonPropertyName("className")] public string? ClassName { get; set; } + [JsonPropertyName("isEnabled")] public bool IsEnabled { get; set; } + [JsonPropertyName("isOffscreen")] public bool IsOffscreen { get; set; } + [JsonPropertyName("hasKeyboardFocus")] public bool HasKeyboardFocus { get; set; } + [JsonPropertyName("patterns")] public List Patterns { get; set; } = new(); + [JsonPropertyName("boundingRect")] public Rect BoundingRect { get; set; } = new(0, 0, 0, 0); + [JsonPropertyName("value")] public string? Value { get; set; } + [JsonPropertyName("toggleState")] public string? ToggleState { get; set; } + [JsonPropertyName("children")] public List Children { get; set; } = new(); +} + +internal sealed record Rect( + [property: JsonPropertyName("x")] double X, + [property: JsonPropertyName("y")] double Y, + [property: JsonPropertyName("width")] double Width, + [property: JsonPropertyName("height")] double Height); diff --git a/dotnet/uiAutomationHelper/src/Program.cs b/dotnet/uiAutomationHelper/src/Program.cs new file mode 100644 index 0000000000..a444e56879 --- /dev/null +++ b/dotnet/uiAutomationHelper/src/Program.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text; +using UiAutomationHelper.Methods; +using UiAutomationHelper.Rpc; + +namespace UiAutomationHelper; + +internal static class Program +{ + public static async Task Main(string[] args) + { + Console.InputEncoding = Encoding.UTF8; + Console.OutputEncoding = Encoding.UTF8; + + using var cts = new CancellationTokenSource(); + Console.CancelKeyPress += (_, e) => + { + e.Cancel = true; + cts.Cancel(); + }; + + var dispatch = new Dispatch(); + Methods.Register.All(dispatch); + + var server = new JsonRpcServer(Console.In, Console.Out, dispatch); + await server.RunAsync(cts.Token).ConfigureAwait(false); + return 0; + } +} diff --git a/dotnet/uiAutomationHelper/src/Rpc/Dispatch.cs b/dotnet/uiAutomationHelper/src/Rpc/Dispatch.cs new file mode 100644 index 0000000000..597638c055 --- /dev/null +++ b/dotnet/uiAutomationHelper/src/Rpc/Dispatch.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json; +using UiAutomationHelper.Models; + +namespace UiAutomationHelper.Rpc; + +internal delegate Task RpcMethod(JsonElement? @params, CancellationToken ct); + +internal sealed class Dispatch +{ + private readonly Dictionary _methods = new(StringComparer.Ordinal); + + public void Register(string name, RpcMethod method) + { + if (_methods.ContainsKey(name)) + { + throw new InvalidOperationException($"Method already registered: {name}"); + } + _methods[name] = method; + } + + public Task InvokeAsync(string name, JsonElement? @params, CancellationToken ct) + { + if (!_methods.TryGetValue(name, out var method)) + { + throw new RpcException(RpcErrorCode.MethodNotFound, $"Method not found: {name}"); + } + return method(@params, ct); + } +} diff --git a/dotnet/uiAutomationHelper/src/Rpc/JsonRpcServer.cs b/dotnet/uiAutomationHelper/src/Rpc/JsonRpcServer.cs new file mode 100644 index 0000000000..84ee25985d --- /dev/null +++ b/dotnet/uiAutomationHelper/src/Rpc/JsonRpcServer.cs @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json; +using System.Text.Json.Serialization; +using UiAutomationHelper.Models; + +namespace UiAutomationHelper.Rpc; + +internal sealed class JsonRpcServer +{ + private readonly TextReader _input; + private readonly TextWriter _output; + private readonly Dispatch _dispatch; + private readonly object _writeLock = new(); + + internal static readonly JsonSerializerOptions JsonOpts = new() + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + }; + + public JsonRpcServer(TextReader input, TextWriter output, Dispatch dispatch) + { + _input = input; + _output = output; + _dispatch = dispatch; + } + + public async Task RunAsync(CancellationToken ct = default) + { + while (!ct.IsCancellationRequested) + { + string? line; + try + { + line = await _input.ReadLineAsync(ct).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + break; + } + + if (line == null) + { + break; + } + if (string.IsNullOrWhiteSpace(line)) + { + continue; + } + + var response = await HandleLineAsync(line, ct).ConfigureAwait(false); + Write(response); + } + } + + private async Task HandleLineAsync(string line, CancellationToken ct) + { + RpcRequest? request = null; + try + { + request = JsonSerializer.Deserialize(line, JsonOpts); + if (request == null || string.IsNullOrEmpty(request.Method)) + { + return RpcResponse.Fail(request?.Id, RpcErrorCode.InvalidRequest, "Invalid request"); + } + var result = await _dispatch.InvokeAsync(request.Method, request.Params, ct) + .ConfigureAwait(false); + return RpcResponse.Success(request.Id, result); + } + catch (JsonException ex) + { + return RpcResponse.Fail(request?.Id, RpcErrorCode.ParseError, ex.Message); + } + catch (RpcException ex) + { + return RpcResponse.Fail(request?.Id, ex.Code, ex.Message, ex.ErrorData); + } + catch (OperationCanceledException) + { + return RpcResponse.Fail(request?.Id, RpcErrorCode.InternalError, "Cancelled"); + } + catch (Exception ex) + { + return RpcResponse.Fail(request?.Id, RpcErrorCode.InternalError, ex.Message); + } + } + + private void Write(RpcResponse response) + { + string json; + try + { + json = JsonSerializer.Serialize(response, JsonOpts); + } + catch (Exception ex) + { + json = JsonSerializer.Serialize( + RpcResponse.Fail(response.Id, RpcErrorCode.InternalError, $"Failed to serialize response: {ex.Message}"), + JsonOpts); + } + lock (_writeLock) + { + _output.WriteLine(json); + _output.Flush(); + } + } +} diff --git a/dotnet/uiAutomationHelper/src/Rpc/RpcParams.cs b/dotnet/uiAutomationHelper/src/Rpc/RpcParams.cs new file mode 100644 index 0000000000..3f868468a0 --- /dev/null +++ b/dotnet/uiAutomationHelper/src/Rpc/RpcParams.cs @@ -0,0 +1,39 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json; +using UiAutomationHelper.Models; + +namespace UiAutomationHelper.Rpc; + +internal static class RpcParams +{ + public static T Parse(JsonElement? @params) where T : new() + { + if (@params == null + || @params.Value.ValueKind == JsonValueKind.Null + || @params.Value.ValueKind == JsonValueKind.Undefined) + { + return new T(); + } + try + { + return JsonSerializer.Deserialize(@params.Value, JsonRpcServer.JsonOpts) ?? new T(); + } + catch (JsonException ex) + { + throw new RpcException(RpcErrorCode.InvalidParams, ex.Message); + } + } + + public static T ParseRequired(JsonElement? @params) where T : new() + { + if (@params == null + || @params.Value.ValueKind == JsonValueKind.Null + || @params.Value.ValueKind == JsonValueKind.Undefined) + { + throw new RpcException(RpcErrorCode.InvalidParams, "Missing params"); + } + return Parse(@params); + } +} diff --git a/dotnet/uiAutomationHelper/src/Uia/AppRegistry.cs b/dotnet/uiAutomationHelper/src/Uia/AppRegistry.cs new file mode 100644 index 0000000000..9905e36ab9 --- /dev/null +++ b/dotnet/uiAutomationHelper/src/Uia/AppRegistry.cs @@ -0,0 +1,42 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace UiAutomationHelper.Uia; + +/// +/// Tracks AUMIDs for apps the helper launched, since UIA exposes no way to +/// recover an AUMID from a window or process. +/// +internal static class AppRegistry +{ + private static readonly Dictionary _aumidByPid = new(); + private static readonly object _lock = new(); + + public static void Register(int pid, string? aumid) + { + if (string.IsNullOrEmpty(aumid)) + { + return; + } + lock (_lock) + { + _aumidByPid[pid] = aumid; + } + } + + public static string? GetAumid(int pid) + { + lock (_lock) + { + return _aumidByPid.TryGetValue(pid, out var a) ? a : null; + } + } + + public static void Forget(int pid) + { + lock (_lock) + { + _aumidByPid.Remove(pid); + } + } +} diff --git a/dotnet/uiAutomationHelper/src/Uia/AutomationHost.cs b/dotnet/uiAutomationHelper/src/Uia/AutomationHost.cs new file mode 100644 index 0000000000..37919949b8 --- /dev/null +++ b/dotnet/uiAutomationHelper/src/Uia/AutomationHost.cs @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using FlaUI.UIA3; + +namespace UiAutomationHelper.Uia; + +internal static class AutomationHost +{ + private static UIA3Automation? _automation; + private static readonly object _lock = new(); + + public static UIA3Automation Automation + { + get + { + if (_automation != null) + { + return _automation; + } + lock (_lock) + { + _automation ??= new UIA3Automation(); + return _automation; + } + } + } + + public static void Dispose() + { + lock (_lock) + { + _automation?.Dispose(); + _automation = null; + } + } +} diff --git a/dotnet/uiAutomationHelper/src/Uia/ScreenshotCapturer.cs b/dotnet/uiAutomationHelper/src/Uia/ScreenshotCapturer.cs new file mode 100644 index 0000000000..74995e2ea8 --- /dev/null +++ b/dotnet/uiAutomationHelper/src/Uia/ScreenshotCapturer.cs @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Drawing; +using System.Drawing.Imaging; +using System.Runtime.InteropServices; +using UiAutomationHelper.Models; + +namespace UiAutomationHelper.Uia; + +internal static class ScreenshotCapturer +{ + [DllImport("user32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool PrintWindow(IntPtr hWnd, IntPtr hdcBlt, int nFlags); + + [DllImport("user32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect); + + [StructLayout(LayoutKind.Sequential)] + private struct RECT + { + public int Left; + public int Top; + public int Right; + public int Bottom; + } + + private const int PW_RENDERFULLCONTENT = 0x00000002; + + public static (byte[] PngBytes, Rect Bounds) Capture(IntPtr hwnd) + { + if (!GetWindowRect(hwnd, out var r)) + { + throw new InvalidOperationException("GetWindowRect failed"); + } + int w = r.Right - r.Left; + int h = r.Bottom - r.Top; + if (w <= 0 || h <= 0) + { + throw new InvalidOperationException($"Invalid window size {w}x{h}"); + } + + using var bmp = new Bitmap(w, h, PixelFormat.Format32bppArgb); + using (var g = Graphics.FromImage(bmp)) + { + var hdc = g.GetHdc(); + try + { + if (!PrintWindow(hwnd, hdc, PW_RENDERFULLCONTENT)) + { + throw new InvalidOperationException("PrintWindow failed"); + } + } + finally + { + g.ReleaseHdc(hdc); + } + } + + using var ms = new MemoryStream(); + bmp.Save(ms, ImageFormat.Png); + return (ms.ToArray(), new Rect(r.Left, r.Top, w, h)); + } +} diff --git a/dotnet/uiAutomationHelper/src/Uia/SelectorParser.cs b/dotnet/uiAutomationHelper/src/Uia/SelectorParser.cs new file mode 100644 index 0000000000..7c103e14c1 --- /dev/null +++ b/dotnet/uiAutomationHelper/src/Uia/SelectorParser.cs @@ -0,0 +1,170 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text; +using UiAutomationHelper.Models; + +namespace UiAutomationHelper.Uia; + +internal static class SelectorParser +{ + public static SelectorPath Parse(string input) + { + if (string.IsNullOrEmpty(input)) + { + throw new FormatException("Selector cannot be empty"); + } + if (input[0] != '/') + { + throw new FormatException("Selector must start with '/'"); + } + + var segments = new List(); + int i = 0; + while (i < input.Length) + { + if (input[i] != '/') + { + throw new FormatException($"Expected '/' at position {i}"); + } + i++; + + int idStart = i; + if (i >= input.Length || !IsIdentStart(input[i])) + { + throw new FormatException($"Expected identifier at position {i}"); + } + i++; + while (i < input.Length && IsIdentRest(input[i])) + { + i++; + } + string controlType = input.Substring(idStart, i - idStart); + + string? name = null, autoId = null, className = null; + int? index = null; + + while (i < input.Length && input[i] == '[') + { + i++; + if (i < input.Length && char.IsDigit(input[i])) + { + int numStart = i; + while (i < input.Length && char.IsDigit(input[i])) + { + i++; + } + if (i >= input.Length || input[i] != ']') + { + throw new FormatException($"Expected ']' after index at position {i}"); + } + index = int.Parse(input.AsSpan(numStart, i - numStart)); + i++; + } + else + { + int keyStart = i; + if (i >= input.Length || !IsIdentStart(input[i])) + { + throw new FormatException($"Expected predicate key at position {i}"); + } + i++; + while (i < input.Length && IsIdentRest(input[i])) + { + i++; + } + string key = input.Substring(keyStart, i - keyStart); + if (i >= input.Length || input[i] != '=') + { + throw new FormatException($"Expected '=' at position {i}"); + } + i++; + if (i >= input.Length || input[i] != '"') + { + throw new FormatException($"Expected '\"' at position {i}"); + } + i++; + + var sb = new StringBuilder(); + while (i < input.Length && input[i] != '"') + { + if (input[i] == '\\' && i + 1 < input.Length) + { + sb.Append(input[i + 1]); + i += 2; + } + else + { + sb.Append(input[i]); + i++; + } + } + if (i >= input.Length) + { + throw new FormatException("Unterminated string value"); + } + i++; + if (i >= input.Length || input[i] != ']') + { + throw new FormatException($"Expected ']' after predicate at position {i}"); + } + i++; + + string value = sb.ToString(); + switch (key) + { + case "Name": name = value; break; + case "AutomationId": autoId = value; break; + case "ClassName": className = value; break; + default: + throw new FormatException($"Unknown predicate key: {key}"); + } + } + } + + segments.Add(new SelectorSegment(controlType, name, autoId, className, index)); + } + + if (segments.Count == 0) + { + throw new FormatException("Selector must have at least one segment"); + } + + return new SelectorPath(segments); + } + + public static string Format(SelectorPath path) + { + var sb = new StringBuilder(); + foreach (var seg in path.Segments) + { + sb.Append('/').Append(seg.ControlType); + if (seg.AutomationId != null) + { + sb.Append("[AutomationId=\"").Append(Escape(seg.AutomationId)).Append("\"]"); + } + if (seg.Name != null) + { + sb.Append("[Name=\"").Append(Escape(seg.Name)).Append("\"]"); + } + if (seg.ClassName != null) + { + sb.Append("[ClassName=\"").Append(Escape(seg.ClassName)).Append("\"]"); + } + if (seg.Index.HasValue) + { + sb.Append('[').Append(seg.Index.Value).Append(']'); + } + } + return sb.ToString(); + } + + public static string FormatSegment(SelectorSegment seg) => + Format(new SelectorPath(new[] { seg })); + + private static string Escape(string s) => + s.Replace("\\", "\\\\").Replace("\"", "\\\""); + + private static bool IsIdentStart(char c) => char.IsLetter(c) || c == '_'; + private static bool IsIdentRest(char c) => char.IsLetterOrDigit(c) || c == '_'; +} diff --git a/dotnet/uiAutomationHelper/src/Uia/SelectorResolver.cs b/dotnet/uiAutomationHelper/src/Uia/SelectorResolver.cs new file mode 100644 index 0000000000..e8f0058279 --- /dev/null +++ b/dotnet/uiAutomationHelper/src/Uia/SelectorResolver.cs @@ -0,0 +1,85 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using FlaUI.Core.AutomationElements; +using FlaUI.Core.Conditions; +using FlaUI.Core.Definitions; +using UiAutomationHelper.Models; + +namespace UiAutomationHelper.Uia; + +internal static class SelectorResolver +{ + /// + /// Resolve a selector path starting from the desktop. Returns null if any segment fails. + /// + public static AutomationElement? Resolve(SelectorPath path) + { + var current = AutomationHost.Automation.GetDesktop(); + var cf = AutomationHost.Automation.ConditionFactory; + + foreach (var seg in path.Segments) + { + var condition = BuildCondition(seg, cf); + var children = current.FindAllChildren(condition); + if (children.Length == 0) + { + return null; + } + if (seg.Index.HasValue) + { + int idx = seg.Index.Value - 1; // 1-based + if (idx < 0 || idx >= children.Length) + { + return null; + } + current = children[idx]; + } + else + { + current = children[0]; + } + } + return current; + } + + public static AutomationElement ResolveOrThrow(string selector) + { + var path = SelectorParser.Parse(selector); + var el = Resolve(path); + if (el == null) + { + throw new RpcException(RpcErrorCode.ElementNotFound, $"Element not found: {selector}"); + } + return el; + } + + private static ConditionBase BuildCondition(SelectorSegment seg, ConditionFactory cf) + { + var conditions = new List(); + if (TryParseControlType(seg.ControlType, out var ct)) + { + conditions.Add(cf.ByControlType(ct)); + } + else + { + throw new RpcException(RpcErrorCode.InvalidParams, $"Unknown control type: {seg.ControlType}"); + } + if (seg.Name != null) + { + conditions.Add(cf.ByName(seg.Name)); + } + if (seg.AutomationId != null) + { + conditions.Add(cf.ByAutomationId(seg.AutomationId)); + } + if (seg.ClassName != null) + { + conditions.Add(cf.ByClassName(seg.ClassName)); + } + return conditions.Count == 1 ? conditions[0] : new AndCondition(conditions.ToArray()); + } + + private static bool TryParseControlType(string name, out ControlType ct) => + Enum.TryParse(name, ignoreCase: false, out ct); +} diff --git a/dotnet/uiAutomationHelper/src/Uia/Selectors.cs b/dotnet/uiAutomationHelper/src/Uia/Selectors.cs new file mode 100644 index 0000000000..6716096da7 --- /dev/null +++ b/dotnet/uiAutomationHelper/src/Uia/Selectors.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using FlaUI.Core.AutomationElements; +using UiAutomationHelper.Models; + +namespace UiAutomationHelper.Uia; + +internal static class Selectors +{ + /// + /// Builds a "/ControlType[predicate]" segment for an element. + /// Priority: AutomationId → Name → ClassName → bare ControlType. + /// + public static string BuildSegment(AutomationElement el) + { + var ct = el.ControlType.ToString(); + var aid = NullIfEmpty(el.Properties.AutomationId.ValueOrDefault); + var name = NullIfEmpty(el.Properties.Name.ValueOrDefault); + var cls = NullIfEmpty(el.Properties.ClassName.ValueOrDefault); + + SelectorSegment seg; + if (aid != null) + { + seg = new SelectorSegment(ct, AutomationId: aid); + } + else if (name != null) + { + seg = new SelectorSegment(ct, Name: name); + } + else if (cls != null) + { + seg = new SelectorSegment(ct, ClassName: cls); + } + else + { + seg = new SelectorSegment(ct); + } + return SelectorParser.FormatSegment(seg); + } + + private static string? NullIfEmpty(string? s) => string.IsNullOrEmpty(s) ? null : s; +} diff --git a/dotnet/uiAutomationHelper/src/Uia/TreeWalker.cs b/dotnet/uiAutomationHelper/src/Uia/TreeWalker.cs new file mode 100644 index 0000000000..ffa81df55f --- /dev/null +++ b/dotnet/uiAutomationHelper/src/Uia/TreeWalker.cs @@ -0,0 +1,119 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using FlaUI.Core.AutomationElements; +using FlaUI.Core.Definitions; +using UiAutomationHelper.Models; + +namespace UiAutomationHelper.Uia; + +internal static class TreeWalker +{ + /// + /// Walk an element subtree and produce a TreeNode hierarchy. + /// + /// The element to start walking from. + /// The selector path leading to . + /// This becomes the root node's Selector field; children's selectors extend it. + /// Walk depth: -1 for unlimited, 0 for just the root, N for N levels of descendants. + public static TreeNode Walk(AutomationElement root, string rootSelector, int maxDepth) + { + return WalkInternal(root, rootSelector, maxDepth); + } + + private static TreeNode WalkInternal(AutomationElement el, string selector, int remainingDepth) + { + var node = BuildNode(el); + node.Selector = selector; + if (remainingDepth == 0) + { + return node; + } + AutomationElement[] children; + try + { + children = el.FindAllChildren(); + } + catch + { + // Some elements throw when enumerating children (e.g., transient/disposed). Skip. + return node; + } + foreach (var c in children) + { + var childSelector = selector + Selectors.BuildSegment(c); + node.Children.Add(WalkInternal(c, childSelector, remainingDepth > 0 ? remainingDepth - 1 : -1)); + } + return node; + } + + private static TreeNode BuildNode(AutomationElement el) + { + var rect = el.Properties.BoundingRectangle.ValueOrDefault; + var node = new TreeNode + { + ControlType = el.ControlType.ToString(), + Name = NullIfEmpty(el.Properties.Name.ValueOrDefault), + AutomationId = NullIfEmpty(el.Properties.AutomationId.ValueOrDefault), + ClassName = NullIfEmpty(el.Properties.ClassName.ValueOrDefault), + IsEnabled = el.Properties.IsEnabled.ValueOrDefault, + IsOffscreen = el.Properties.IsOffscreen.ValueOrDefault, + HasKeyboardFocus = el.Properties.HasKeyboardFocus.ValueOrDefault, + BoundingRect = new Rect(rect.X, rect.Y, rect.Width, rect.Height), + Patterns = GetPatterns(el), + Value = TryGetValue(el), + ToggleState = TryGetToggleState(el), + }; + return node; + } + + private static List GetPatterns(AutomationElement el) + { + var patterns = new List(8); + try { if (el.Patterns.Invoke.IsSupported) patterns.Add("Invoke"); } catch { } + try { if (el.Patterns.Toggle.IsSupported) patterns.Add("Toggle"); } catch { } + try { if (el.Patterns.Value.IsSupported) patterns.Add("Value"); } catch { } + try { if (el.Patterns.RangeValue.IsSupported) patterns.Add("RangeValue"); } catch { } + try { if (el.Patterns.Selection.IsSupported) patterns.Add("Selection"); } catch { } + try { if (el.Patterns.SelectionItem.IsSupported) patterns.Add("SelectionItem"); } catch { } + try { if (el.Patterns.ExpandCollapse.IsSupported) patterns.Add("ExpandCollapse"); } catch { } + try { if (el.Patterns.Scroll.IsSupported) patterns.Add("Scroll"); } catch { } + try { if (el.Patterns.Window.IsSupported) patterns.Add("Window"); } catch { } + try { if (el.Patterns.Text.IsSupported) patterns.Add("Text"); } catch { } + return patterns; + } + + private static string? TryGetValue(AutomationElement el) + { + try + { + if (el.Patterns.Value.IsSupported) + { + return el.Patterns.Value.Pattern.Value.ValueOrDefault; + } + } + catch { } + return null; + } + + private static string? TryGetToggleState(AutomationElement el) + { + try + { + if (el.Patterns.Toggle.IsSupported) + { + return el.Patterns.Toggle.Pattern.ToggleState.ValueOrDefault switch + { + ToggleState.On => "on", + ToggleState.Off => "off", + ToggleState.Indeterminate => "indeterminate", + _ => null, + }; + } + } + catch { } + return null; + } + + private static string? NullIfEmpty(string? s) => string.IsNullOrEmpty(s) ? null : s; +} diff --git a/dotnet/uiAutomationHelper/test/SelectorParserTests.cs b/dotnet/uiAutomationHelper/test/SelectorParserTests.cs new file mode 100644 index 0000000000..ad983fdb15 --- /dev/null +++ b/dotnet/uiAutomationHelper/test/SelectorParserTests.cs @@ -0,0 +1,138 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using UiAutomationHelper.Models; +using UiAutomationHelper.Uia; +using Xunit; + +namespace UiAutomationHelper.Tests; + +public class SelectorParserTests +{ + [Fact] + public void Parse_SingleWindow_NoPredicates() + { + var p = SelectorParser.Parse("/Window"); + Assert.Single(p.Segments); + var s = p.Segments[0]; + Assert.Equal("Window", s.ControlType); + Assert.Null(s.Name); + Assert.Null(s.AutomationId); + Assert.Null(s.ClassName); + Assert.Null(s.Index); + } + + [Fact] + public void Parse_NamePredicate() + { + var p = SelectorParser.Parse("/Window[Name=\"Clock\"]"); + Assert.Equal("Clock", p.Segments[0].Name); + } + + [Fact] + public void Parse_AutomationIdPredicate() + { + var p = SelectorParser.Parse("/Button[AutomationId=\"StartButton\"]"); + Assert.Equal("StartButton", p.Segments[0].AutomationId); + } + + [Fact] + public void Parse_ClassNamePredicate() + { + var p = SelectorParser.Parse("/Pane[ClassName=\"Microsoft.UI.Xaml.Controls.PaneRoot\"]"); + Assert.Equal("Microsoft.UI.Xaml.Controls.PaneRoot", p.Segments[0].ClassName); + } + + [Fact] + public void Parse_IndexPredicate() + { + var p = SelectorParser.Parse("/ListItem[3]"); + Assert.Equal(3, p.Segments[0].Index); + } + + [Fact] + public void Parse_MultiplePredicates() + { + var p = SelectorParser.Parse("/Button[Name=\"Save\"][AutomationId=\"SaveBtn\"]"); + Assert.Equal("Save", p.Segments[0].Name); + Assert.Equal("SaveBtn", p.Segments[0].AutomationId); + } + + [Fact] + public void Parse_DeepPath() + { + var p = SelectorParser.Parse( + "/Window[Name=\"Clock\"]/Pane/Pivot/PivotItem[Name=\"Timer\"]/Button[AutomationId=\"StartButton\"]"); + Assert.Equal(5, p.Segments.Count); + Assert.Equal("Window", p.Segments[0].ControlType); + Assert.Equal("Clock", p.Segments[0].Name); + Assert.Equal("Pane", p.Segments[1].ControlType); + Assert.Equal("Pivot", p.Segments[2].ControlType); + Assert.Equal("Timer", p.Segments[3].Name); + Assert.Equal("StartButton", p.Segments[4].AutomationId); + } + + [Fact] + public void Parse_EscapedQuoteInValue() + { + var p = SelectorParser.Parse("/Edit[Name=\"It\\\"s mine\"]"); + Assert.Equal("It\"s mine", p.Segments[0].Name); + } + + [Fact] + public void Parse_EscapedBackslashInValue() + { + var p = SelectorParser.Parse("/Edit[Name=\"path\\\\to\\\\file\"]"); + Assert.Equal("path\\to\\file", p.Segments[0].Name); + } + + [Fact] + public void Parse_EmptyStringValue() + { + var p = SelectorParser.Parse("/Edit[Name=\"\"]"); + Assert.Equal("", p.Segments[0].Name); + } + + [Theory] + [InlineData("")] + [InlineData("Window")] // missing leading slash + [InlineData("/")] // empty segment + [InlineData("/Window[Name=Clock]")] // unquoted value + [InlineData("/Window[Name=\"Clock\"")] // unterminated bracket + [InlineData("/Window[Bogus=\"x\"]")] // unknown predicate key + [InlineData("/123Window")] // identifier starting with digit + public void Parse_InvalidInputs_Throw(string input) + { + Assert.Throws(() => SelectorParser.Parse(input)); + } + + [Fact] + public void Format_RoundTrip() + { + var input = "/Window[AutomationId=\"win1\"][Name=\"Clock\"]/Button[AutomationId=\"StartButton\"]"; + var path = SelectorParser.Parse(input); + var formatted = SelectorParser.Format(path); + var reparsed = SelectorParser.Parse(formatted); + Assert.Equal(path.Segments.Count, reparsed.Segments.Count); + for (int i = 0; i < path.Segments.Count; i++) + { + Assert.Equal(path.Segments[i], reparsed.Segments[i]); + } + } + + [Fact] + public void Format_PutsAutomationIdFirst() + { + var seg = new SelectorSegment("Button", Name: "X", AutomationId: "Y"); + var formatted = SelectorParser.FormatSegment(seg); + Assert.Equal("/Button[AutomationId=\"Y\"][Name=\"X\"]", formatted); + } + + [Fact] + public void Format_EscapesQuotes() + { + var seg = new SelectorSegment("Edit", Name: "He said \"hi\""); + var formatted = SelectorParser.FormatSegment(seg); + Assert.Equal("/Edit[Name=\"He said \\\"hi\\\"\"]", formatted); + } +} diff --git a/dotnet/uiAutomationHelper/test/UiAutomationHelper.Tests.csproj b/dotnet/uiAutomationHelper/test/UiAutomationHelper.Tests.csproj new file mode 100644 index 0000000000..40380730fa --- /dev/null +++ b/dotnet/uiAutomationHelper/test/UiAutomationHelper.Tests.csproj @@ -0,0 +1,19 @@ + + + net8.0-windows + enable + enable + latest + false + bin\$(Configuration) + false + + + + + + + + + + diff --git a/ts/packages/agents/onboarding/src/uiCapture/helperClient.ts b/ts/packages/agents/onboarding/src/uiCapture/helperClient.ts new file mode 100644 index 0000000000..cfc86803cb --- /dev/null +++ b/ts/packages/agents/onboarding/src/uiCapture/helperClient.ts @@ -0,0 +1,234 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { ChildProcess, spawn } from "node:child_process"; +import { existsSync } from "node:fs"; +import path from "node:path"; +import { createInterface, Interface } from "node:readline"; +import { fileURLToPath } from "node:url"; + +import type { Rect, Screenshot, TreeNode, WindowInfo } from "./types.js"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +type Pending = { + resolve: (value: unknown) => void; + reject: (error: Error) => void; +}; + +export type HelperRpcError = Error & { code?: number }; + +export interface HelperClientOptions { + binaryPath?: string; + debug?: boolean; +} + +/** + * Resolves the helper binary path. Order: + * 1. opts.binaryPath + * 2. TYPEAGENT_UIA_HELPER env var + * 3. Repo-relative dev path (for local development) + */ +function resolveBinary(opts: HelperClientOptions): string { + if (opts.binaryPath) { + return opts.binaryPath; + } + if (process.env.TYPEAGENT_UIA_HELPER) { + return process.env.TYPEAGENT_UIA_HELPER; + } + // From dist/uiCapture/helperClient.js, repo root is six levels up. + const repoRelative = path.resolve( + __dirname, + "../../../../../..", + "dotnet/uiAutomationHelper/bin/Release/UiAutomationHelper.exe", + ); + return repoRelative; +} + +/** + * JSON-RPC 2.0 client over stdio for the .NET UIA helper. + * + * Slice 1 surface: ping, app.launch/attach/list/kill, tree.dump, screenshot, do.invoke. + * Single-flight is fine for slice 1; events and concurrent requests come later. + */ +export class HelperClient { + private nextId = 1; + private readonly pending = new Map(); + private exited = false; + private exitCode: number | null = null; + + private constructor( + private readonly child: ChildProcess, + private readonly stdoutLines: Interface, + private readonly debug: boolean, + ) {} + + static async start(opts: HelperClientOptions = {}): Promise { + const binary = resolveBinary(opts); + if (!existsSync(binary)) { + throw new Error( + `Helper binary not found at ${binary}. ` + + `Build it via: dotnet build -c Release ` + + `dotnet/uiAutomationHelper/UiAutomationHelper.sln`, + ); + } + const child = spawn(binary, [], { + stdio: ["pipe", "pipe", "pipe"], + windowsHide: true, + }); + const stdoutLines = createInterface({ + input: child.stdout!, + crlfDelay: Infinity, + }); + const client = new HelperClient(child, stdoutLines, opts.debug ?? false); + client.attach(); + // Verify the helper is actually responding before returning. + await client.ping(); + return client; + } + + private attach(): void { + this.stdoutLines.on("line", (line) => this.handleLine(line)); + this.child.on("exit", (code) => { + this.exited = true; + this.exitCode = code; + for (const [id, p] of this.pending.entries()) { + p.reject( + new Error( + `Helper exited (code ${code}) before responding to request ${id}`, + ), + ); + } + this.pending.clear(); + }); + this.child.stderr!.on("data", (data: Buffer) => { + if (this.debug) { + process.stderr.write(`[uia-helper] ${data.toString()}`); + } + }); + } + + private handleLine(line: string): void { + if (!line.trim()) { + return; + } + let msg: { + id?: number | string | null; + result?: unknown; + error?: { code: number; message: string; data?: unknown }; + }; + try { + msg = JSON.parse(line); + } catch { + if (this.debug) { + process.stderr.write(`[uia-helper bad-json] ${line}\n`); + } + return; + } + const id = typeof msg.id === "number" ? msg.id : null; + if (id == null) { + // Notifications not handled in slice 1. + return; + } + const p = this.pending.get(id); + if (!p) { + return; + } + this.pending.delete(id); + if (msg.error) { + const err = new Error( + `[${msg.error.code}] ${msg.error.message}`, + ) as HelperRpcError; + err.code = msg.error.code; + p.reject(err); + } else { + p.resolve(msg.result); + } + } + + private call(method: string, params?: unknown): Promise { + if (this.exited) { + return Promise.reject( + new Error(`Helper has exited (code ${this.exitCode})`), + ); + } + const id = this.nextId++; + const req = { jsonrpc: "2.0", id, method, params }; + return new Promise((resolve, reject) => { + this.pending.set(id, { + resolve: resolve as (value: unknown) => void, + reject, + }); + this.child.stdin!.write(JSON.stringify(req) + "\n", (err) => { + if (err) { + this.pending.delete(id); + reject(err); + } + }); + }); + } + + ping(): Promise<{ ok: true; version: string }> { + return this.call("health.ping"); + } + + appLaunch(p: { + aumid?: string; + exePath?: string; + args?: string[]; + }): Promise<{ pid: number; mainWindow: string }> { + return this.call("app.launch", p); + } + + appAttach(p: { + pid?: number; + windowTitle?: string; + }): Promise<{ pid: number; mainWindow: string }> { + return this.call("app.attach", p); + } + + appList(): Promise { + return this.call("app.list"); + } + + appKill(p: { pid: number }): Promise<{ ok: true }> { + return this.call("app.kill", p); + } + + treeDump(p: { + root: string; + maxDepth?: number; + filter?: "actionable" | "all"; + }): Promise { + return this.call("tree.dump", p); + } + + screenshot(p: { root: string }): Promise { + return this.call("screenshot", p); + } + + doInvoke(p: { selector: string }): Promise<{ ok: true }> { + return this.call("do.invoke", p); + } + + /** + * Close the helper's stdin and wait up to `timeoutMs` for graceful exit. + * If it doesn't exit, send SIGKILL. + */ + async dispose(timeoutMs = 2000): Promise { + if (this.exited) { + return; + } + this.child.stdin!.end(); + const exited = new Promise((res) => + this.child.once("exit", () => res()), + ); + const timeout = new Promise((res) => setTimeout(res, timeoutMs)); + await Promise.race([exited, timeout]); + if (!this.exited) { + this.child.kill("SIGKILL"); + } + } +} + +export type { Rect, Screenshot, TreeNode, WindowInfo }; diff --git a/ts/packages/agents/onboarding/src/uiCapture/test/clockSmoke.ts b/ts/packages/agents/onboarding/src/uiCapture/test/clockSmoke.ts new file mode 100644 index 0000000000..56eebc37ca --- /dev/null +++ b/ts/packages/agents/onboarding/src/uiCapture/test/clockSmoke.ts @@ -0,0 +1,131 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { mkdirSync, writeFileSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +import { HelperClient } from "../helperClient.js"; +import type { TreeNode } from "../types.js"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +// dist/uiCapture/test/clockSmoke.js → package root is three levels up. +const fixturesDir = path.resolve(__dirname, "../../..", "test/fixtures/uiCapture"); + +const CLOCK_AUMID = "Microsoft.WindowsAlarms_8wekyb3d8bbwe!App"; + +function log(msg: string): void { + process.stdout.write(`[smoke] ${msg}\n`); +} + +function countNodes(node: TreeNode): number { + return 1 + node.children.reduce((s, c) => s + countNodes(c), 0); +} + +function countInvocableButtons(node: TreeNode): number { + let n = node.patterns.includes("Invoke") && node.controlType === "Button" ? 1 : 0; + for (const c of node.children) { + n += countInvocableButtons(c); + } + return n; +} + +function findInvocable(node: TreeNode, namePrefix?: string): TreeNode | null { + if ( + node.patterns.includes("Invoke") && + node.isEnabled && + (!namePrefix || (node.name ?? "").startsWith(namePrefix)) + ) { + return node; + } + for (const c of node.children) { + const f = findInvocable(c, namePrefix); + if (f) { + return f; + } + } + return null; +} + +async function sleep(ms: number): Promise { + await new Promise((res) => setTimeout(res, ms)); +} + +async function main(): Promise { + mkdirSync(fixturesDir, { recursive: true }); + log(`fixtures → ${fixturesDir}`); + + const client = await HelperClient.start({ debug: true }); + try { + const ping = await client.ping(); + log(`ping ok, version=${ping.version}`); + + const existing = await client.appList(); + const stale = existing.filter((w) => w.title.includes("Clock")); + for (const w of stale) { + log(`closing existing Clock pid=${w.pid}`); + await client.appKill({ pid: w.pid }); + } + if (stale.length) { + await sleep(1500); + } + + log("launching Clock..."); + const launch = await client.appLaunch({ aumid: CLOCK_AUMID }); + log(`launched pid=${launch.pid} mainWindow=${launch.mainWindow}`); + + // Poll until the UWP tree populates with at least a few invokable buttons. + let tree = await client.treeDump({ root: launch.mainWindow, maxDepth: 6 }); + let attempts = 0; + while (countInvocableButtons(tree) < 3 && attempts < 10) { + await sleep(500); + tree = await client.treeDump({ root: launch.mainWindow, maxDepth: 6 }); + attempts++; + } + log( + `tree settled after ${attempts} polls: ${countNodes(tree)} nodes, ` + + `${countInvocableButtons(tree)} invokable buttons`, + ); + + writeFileSync( + path.join(fixturesDir, "clock-tree-launched.json"), + JSON.stringify(tree, null, 2), + ); + log("saved clock-tree-launched.json"); + + const shot = await client.screenshot({ root: launch.mainWindow }); + writeFileSync( + path.join(fixturesDir, "clock-launched.png"), + Buffer.from(shot.pngBase64, "base64"), + ); + log(`saved clock-launched.png (${shot.rect.width}x${shot.rect.height})`); + + // Pick a target for do.invoke. Prefer Close so we leave the system clean. + const target = findInvocable(tree, "Close ") ?? findInvocable(tree); + if (!target) { + throw new Error("no invokable element found in Clock tree"); + } + log(`do.invoke target: ${target.selector}`); + await client.doInvoke({ selector: target.selector }); + await sleep(800); + + // If Close was invoked, Clock is gone. Try kill anyway as belt-and-suspenders. + try { + await client.appKill({ pid: launch.pid }); + } catch { + /* already gone */ + } + + log("DONE"); + } finally { + await client.dispose(); + } +} + +main().catch((e) => { + process.stderr.write(`FAILED: ${e}\n`); + if (e instanceof Error && e.stack) { + process.stderr.write(e.stack + "\n"); + } + process.exit(1); +}); diff --git a/ts/packages/agents/onboarding/src/uiCapture/types.ts b/ts/packages/agents/onboarding/src/uiCapture/types.ts new file mode 100644 index 0000000000..2ee23aee2f --- /dev/null +++ b/ts/packages/agents/onboarding/src/uiCapture/types.ts @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +/** + * UI Automation pattern names exposed by the helper. + * Mirrors UIAutomationHelper.Models.Pattern in the C# helper. + */ +export type Pattern = + | "Invoke" + | "Toggle" + | "Value" + | "RangeValue" + | "Selection" + | "SelectionItem" + | "ExpandCollapse" + | "Scroll" + | "Window" + | "Text"; + +/** + * Verbs the helper can execute against an element. Slice 1 implements `invoke`; + * the rest land in slice 2. + */ +export type ActionVerb = + | "invoke" + | "toggle" + | "setValue" + | "select" + | "expand" + | "scroll" + | "sendKeys" + | "focus" + | "click"; + +export type Rect = { + x: number; + y: number; + width: number; + height: number; +}; + +export type ToggleState = "on" | "off" | "indeterminate"; + +export type TreeNode = { + selector: string; + automationId?: string; + name?: string; + controlType: string; + className?: string; + isEnabled: boolean; + isOffscreen: boolean; + hasKeyboardFocus: boolean; + patterns: Pattern[]; + boundingRect: Rect; + value?: string; + toggleState?: ToggleState; + children: TreeNode[]; +}; + +export type WindowInfo = { + pid: number; + title: string; + aumid?: string; + mainWindow: string; +}; + +export type Screenshot = { + pngBase64: string; + rect: Rect; +}; diff --git a/ts/packages/agents/onboarding/test/fixtures/uiCapture/clock-launched.png b/ts/packages/agents/onboarding/test/fixtures/uiCapture/clock-launched.png new file mode 100644 index 0000000000000000000000000000000000000000..8fa640f3599197f046cdc68d88ae75f3b089e869 GIT binary patch literal 29995 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 Dc7bax literal 0 HcmV?d00001 diff --git a/ts/packages/agents/onboarding/test/fixtures/uiCapture/clock-tree-launched.json b/ts/packages/agents/onboarding/test/fixtures/uiCapture/clock-tree-launched.json new file mode 100644 index 0000000000..e2a95e1b87 --- /dev/null +++ b/ts/packages/agents/onboarding/test/fixtures/uiCapture/clock-tree-launched.json @@ -0,0 +1,151 @@ +{ + "selector": "/Window[Name=\"Clock\"]", + "name": "Clock", + "controlType": "Window", + "className": "ApplicationFrameWindow", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": true, + "patterns": [ + "Window" + ], + "boundingRect": { + "x": 150, + "y": 120, + "width": 1224, + "height": 1014 + }, + "children": [ + { + "selector": "/Window[Name=\"Clock\"]/Window[AutomationId=\"TitleBar\"]", + "automationId": "TitleBar", + "name": "Clock", + "controlType": "Window", + "className": "ApplicationFrameTitleBarWindow", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Value" + ], + "boundingRect": { + "x": 159, + "y": 121, + "width": 1206, + "height": 40 + }, + "value": "Clock", + "children": [ + { + "selector": "/Window[Name=\"Clock\"]/Window[AutomationId=\"TitleBar\"]/MenuBar[AutomationId=\"SystemMenuBar\"]", + "automationId": "SystemMenuBar", + "name": "System", + "controlType": "MenuBar", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [], + "boundingRect": { + "x": 0, + "y": 0, + "width": 0, + "height": 0 + }, + "children": [ + { + "selector": "/Window[Name=\"Clock\"]/Window[AutomationId=\"TitleBar\"]/MenuBar[AutomationId=\"SystemMenuBar\"]/MenuItem[Name=\"System\"]", + "name": "System", + "controlType": "MenuItem", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "ExpandCollapse" + ], + "boundingRect": { + "x": 0, + "y": 0, + "width": 0, + "height": 0 + }, + "children": [] + } + ] + }, + { + "selector": "/Window[Name=\"Clock\"]/Window[AutomationId=\"TitleBar\"]/Button[AutomationId=\"Minimize\"]", + "automationId": "Minimize", + "name": "Minimize Clock", + "controlType": "Button", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Invoke" + ], + "boundingRect": { + "x": 1194, + "y": 121, + "width": 57, + "height": 40 + }, + "children": [] + }, + { + "selector": "/Window[Name=\"Clock\"]/Window[AutomationId=\"TitleBar\"]/Button[AutomationId=\"Maximize\"]", + "automationId": "Maximize", + "name": "Maximize Clock", + "controlType": "Button", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Invoke" + ], + "boundingRect": { + "x": 1251, + "y": 121, + "width": 57, + "height": 40 + }, + "children": [] + }, + { + "selector": "/Window[Name=\"Clock\"]/Window[AutomationId=\"TitleBar\"]/Button[AutomationId=\"Close\"]", + "automationId": "Close", + "name": "Close Clock", + "controlType": "Button", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Invoke" + ], + "boundingRect": { + "x": 1308, + "y": 121, + "width": 57, + "height": 40 + }, + "children": [] + } + ] + }, + { + "selector": "/Window[Name=\"Clock\"]/Pane[ClassName=\"ApplicationFrameInputSinkWindow\"]", + "controlType": "Pane", + "className": "ApplicationFrameInputSinkWindow", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [], + "boundingRect": { + "x": 159, + "y": 161, + "width": 1206, + "height": 964 + }, + "children": [] + } + ] +} \ No newline at end of file From 5e0406dd438e187c380ec92b5d4036d5899f2ee0 Mon Sep 17 00:00:00 2001 From: Robert Gruen Date: Sun, 3 May 2026 22:23:40 -0700 Subject: [PATCH 03/28] Slice 2: full verb set, find, events.idle, selector fallbacks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Helper RPCs: do.toggle/setValue/select/expand/scroll/focus/click/sendKeys (joining do.invoke from slice 1), plus find (with optional polling) and events.idle (focus-change-debounce). All app.list and tree.dump calls now retry transient COM errors that fire during UWP teardown. Selector resolution gained two fixes from real-world failures: when AutomationId is missing, capture-time selectors include ClassName as a disambiguator so siblings sharing a Name (UWP's nested ApplicationFrame and CoreWindow both named after the app) resolve correctly. App.launch's returned mainWindow is now the desktop-rooted ApplicationFrameWindow, not the inner CoreWindow which lives under it in UIA's logical tree — resolved via Win32 GA_ROOTOWNER + name match + a poll loop for the async-created frame. Smoke now drives Clock through invoke + select + focus + find + events.idle and produces a clock-tree-navigated.json fixture showing the post-navigation state. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../UiAutomationHelper.csproj | 3 +- .../src/Methods/ActionMethods.cs | 305 ++++- .../src/Methods/AppMethods.cs | 8 +- .../src/Methods/EventMethods.cs | 44 + .../src/Methods/FindMethods.cs | 49 + .../src/Methods/Register.cs | 2 + .../src/Methods/TreeMethods.cs | 9 +- .../src/Uia/AutomationHost.cs | 7 +- dotnet/uiAutomationHelper/src/Uia/ComRetry.cs | 48 + .../uiAutomationHelper/src/Uia/EventBridge.cs | 47 + .../src/Uia/NativeMethods.cs | 16 + .../uiAutomationHelper/src/Uia/Selectors.cs | 94 ++ .../onboarding/src/uiCapture/helperClient.ts | 65 + .../src/uiCapture/test/clockSmoke.ts | 139 +- .../fixtures/uiCapture/clock-launched.png | Bin 29995 -> 131716 bytes .../uiCapture/clock-tree-launched.json | 1180 ++++++++++++++++- .../uiCapture/clock-tree-navigated.json | 793 +++++++++++ 17 files changed, 2735 insertions(+), 74 deletions(-) create mode 100644 dotnet/uiAutomationHelper/src/Methods/EventMethods.cs create mode 100644 dotnet/uiAutomationHelper/src/Methods/FindMethods.cs create mode 100644 dotnet/uiAutomationHelper/src/Uia/ComRetry.cs create mode 100644 dotnet/uiAutomationHelper/src/Uia/EventBridge.cs create mode 100644 dotnet/uiAutomationHelper/src/Uia/NativeMethods.cs create mode 100644 ts/packages/agents/onboarding/test/fixtures/uiCapture/clock-tree-navigated.json diff --git a/dotnet/uiAutomationHelper/UiAutomationHelper.csproj b/dotnet/uiAutomationHelper/UiAutomationHelper.csproj index 80b6072a9b..f269663667 100644 --- a/dotnet/uiAutomationHelper/UiAutomationHelper.csproj +++ b/dotnet/uiAutomationHelper/UiAutomationHelper.csproj @@ -7,9 +7,10 @@ latest UiAutomationHelper UiAutomationHelper + 0.1.0 bin\$(Configuration) false - $(NoWarn);SYSLIB1054 + $(NoWarn);SYSLIB1054;CA1508;CA1859 diff --git a/dotnet/uiAutomationHelper/src/Methods/ActionMethods.cs b/dotnet/uiAutomationHelper/src/Methods/ActionMethods.cs index 67766a9275..705c2db8aa 100644 --- a/dotnet/uiAutomationHelper/src/Methods/ActionMethods.cs +++ b/dotnet/uiAutomationHelper/src/Methods/ActionMethods.cs @@ -1,7 +1,11 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Text.Json; using System.Text.Json.Serialization; +using FlaUI.Core.AutomationElements; +using FlaUI.Core.Definitions; +using FlaUI.Core.Input; using UiAutomationHelper.Models; using UiAutomationHelper.Rpc; using UiAutomationHelper.Uia; @@ -12,32 +16,317 @@ internal static class ActionMethods { public static void Register(Dispatch dispatch) { - dispatch.Register("do.invoke", (p, ct) => Task.FromResult(Invoke(p))); + dispatch.Register("do.invoke", (p, ct) => Task.FromResult(Invoke(p))); + dispatch.Register("do.toggle", (p, ct) => Task.FromResult(Toggle(p))); + dispatch.Register("do.setValue", (p, ct) => Task.FromResult(SetValue(p))); + dispatch.Register("do.select", (p, ct) => Task.FromResult(Select(p))); + dispatch.Register("do.expand", (p, ct) => Task.FromResult(Expand(p))); + dispatch.Register("do.scroll", (p, ct) => Task.FromResult(Scroll(p))); + dispatch.Register("do.focus", (p, ct) => Task.FromResult(Focus(p))); + dispatch.Register("do.click", (p, ct) => Task.FromResult(Click(p))); + dispatch.Register("do.sendKeys", (p, ct) => Task.FromResult(SendKeys(p))); } - private static object? Invoke(System.Text.Json.JsonElement? @params) + private static object? Invoke(JsonElement? @params) { var p = RpcParams.ParseRequired(@params); + var el = ResolveAndCheckEnabled(p.Selector); + if (!el.Patterns.Invoke.IsSupported) + { + throw new RpcException(RpcErrorCode.PatternNotSupported, + $"Element does not support Invoke: {p.Selector}"); + } + el.Patterns.Invoke.Pattern.Invoke(); + return new { ok = true }; + } + + private static object? Toggle(JsonElement? @params) + { + var p = RpcParams.ParseRequired(@params); + var el = ResolveAndCheckEnabled(p.Selector); + if (!el.Patterns.Toggle.IsSupported) + { + throw new RpcException(RpcErrorCode.PatternNotSupported, + $"Element does not support Toggle: {p.Selector}"); + } + var pattern = el.Patterns.Toggle.Pattern; + if (p.Value.HasValue) + { + var desired = p.Value.Value ? ToggleState.On : ToggleState.Off; + // Tri-state controls take up to 3 toggles to reach a chosen On/Off state. + for (int i = 0; i < 3; i++) + { + if (pattern.ToggleState.Value == desired) + { + break; + } + pattern.Toggle(); + } + } + else + { + pattern.Toggle(); + } + return new { ok = true, toggleState = ToggleStateString(pattern.ToggleState.Value) }; + } + + private static object? SetValue(JsonElement? @params) + { + var p = RpcParams.ParseRequired(@params); + var el = ResolveAndCheckEnabled(p.Selector); + var raw = p.Value?.ToString() ?? ""; + + if (el.Patterns.Value.IsSupported && + el.Patterns.Value.Pattern.IsReadOnly.ValueOrDefault == false) + { + el.Patterns.Value.Pattern.SetValue(raw); + return new { ok = true }; + } + if (el.Patterns.RangeValue.IsSupported) + { + if (!double.TryParse(raw, System.Globalization.NumberStyles.Any, + System.Globalization.CultureInfo.InvariantCulture, out var num)) + { + throw new RpcException(RpcErrorCode.InvalidParams, + $"Value '{raw}' is not numeric for RangeValue control"); + } + el.Patterns.RangeValue.Pattern.SetValue(num); + return new { ok = true }; + } + throw new RpcException(RpcErrorCode.PatternNotSupported, + $"Element supports neither writable Value nor RangeValue: {p.Selector}"); + } + + private static object? Select(JsonElement? @params) + { + var p = RpcParams.ParseRequired(@params); + var el = ResolveAndCheckEnabled(p.Selector); + + if (el.Patterns.SelectionItem.IsSupported) + { + el.Patterns.SelectionItem.Pattern.Select(); + return new { ok = true }; + } + if (el.Patterns.Selection.IsSupported) + { + if (p.Item == null || !p.Item.HasValue) + { + throw new RpcException(RpcErrorCode.InvalidParams, + "'item' is required when selecting from a Selection container"); + } + var item = p.Item.Value; + AutomationElement? target = null; + var children = el.FindAllChildren(); + if (item.ValueKind == JsonValueKind.Number) + { + int idx = item.GetInt32(); + if (idx >= 0 && idx < children.Length) + { + target = children[idx]; + } + } + else if (item.ValueKind == JsonValueKind.String) + { + string? name = item.GetString(); + target = Array.Find(children, + c => string.Equals(c.Properties.Name.ValueOrDefault, name, StringComparison.Ordinal)); + } + if (target == null) + { + throw new RpcException(RpcErrorCode.ElementNotFound, + "Selection container has no matching item"); + } + if (!target.Patterns.SelectionItem.IsSupported) + { + throw new RpcException(RpcErrorCode.PatternNotSupported, + "Matched child does not support SelectionItem"); + } + target.Patterns.SelectionItem.Pattern.Select(); + return new { ok = true }; + } + throw new RpcException(RpcErrorCode.PatternNotSupported, + $"Element supports neither SelectionItem nor Selection: {p.Selector}"); + } + + private static object? Expand(JsonElement? @params) + { + var p = RpcParams.ParseRequired(@params); + var el = ResolveAndCheckEnabled(p.Selector); + if (!el.Patterns.ExpandCollapse.IsSupported) + { + throw new RpcException(RpcErrorCode.PatternNotSupported, + $"Element does not support ExpandCollapse: {p.Selector}"); + } + var pattern = el.Patterns.ExpandCollapse.Pattern; + if (p.ExpandValue) + { + pattern.Expand(); + } + else + { + pattern.Collapse(); + } + return new { ok = true }; + } + + private static object? Scroll(JsonElement? @params) + { + var p = RpcParams.ParseRequired(@params); + var el = ResolveAndCheckEnabled(p.Selector); + if (!el.Patterns.Scroll.IsSupported) + { + throw new RpcException(RpcErrorCode.PatternNotSupported, + $"Element does not support Scroll: {p.Selector}"); + } + bool large = string.Equals(p.Amount, "large", StringComparison.OrdinalIgnoreCase); + var (h, v) = (p.Direction ?? "down").ToLowerInvariant() switch + { + "up" => (ScrollAmount.NoAmount, large ? ScrollAmount.LargeDecrement : ScrollAmount.SmallDecrement), + "down" => (ScrollAmount.NoAmount, large ? ScrollAmount.LargeIncrement : ScrollAmount.SmallIncrement), + "left" => (large ? ScrollAmount.LargeDecrement : ScrollAmount.SmallDecrement, ScrollAmount.NoAmount), + "right" => (large ? ScrollAmount.LargeIncrement : ScrollAmount.SmallIncrement, ScrollAmount.NoAmount), + _ => throw new RpcException(RpcErrorCode.InvalidParams, + $"Unknown direction: {p.Direction}"), + }; + el.Patterns.Scroll.Pattern.Scroll(h, v); + return new { ok = true }; + } + + private static object? Focus(JsonElement? @params) + { + var p = RpcParams.ParseRequired(@params); if (string.IsNullOrEmpty(p.Selector)) { throw new RpcException(RpcErrorCode.InvalidParams, "'selector' is required"); } var el = SelectorResolver.ResolveOrThrow(p.Selector); - if (!el.Properties.IsEnabled.ValueOrDefault) + el.Focus(); + return new { ok = true }; + } + + private static object? Click(JsonElement? @params) + { + var p = RpcParams.ParseRequired(@params); + if (string.IsNullOrEmpty(p.Selector)) { - throw new RpcException(RpcErrorCode.ElementNotEnabled, $"Element is not enabled: {p.Selector}"); + throw new RpcException(RpcErrorCode.InvalidParams, "'selector' is required"); } - if (!el.Patterns.Invoke.IsSupported) + var el = SelectorResolver.ResolveOrThrow(p.Selector); + var rect = el.Properties.BoundingRectangle.ValueOrDefault; + int x = rect.X + rect.Width / 2; + int y = rect.Y + rect.Height / 2; + if (p.Position != null) { - throw new RpcException(RpcErrorCode.PatternNotSupported, - $"Element does not support Invoke: {p.Selector}"); + if (p.Position.X.HasValue) x = (int)p.Position.X.Value; + if (p.Position.Y.HasValue) y = (int)p.Position.Y.Value; } - el.Patterns.Invoke.Pattern.Invoke(); + var pt = new System.Drawing.Point(x, y); + if (string.Equals(p.Button, "right", StringComparison.OrdinalIgnoreCase)) + { + Mouse.RightClick(pt); + } + else + { + Mouse.LeftClick(pt); + } + return new { ok = true }; + } + + private static object? SendKeys(JsonElement? @params) + { + var p = RpcParams.ParseRequired(@params); + if (string.IsNullOrEmpty(p.Keys)) + { + throw new RpcException(RpcErrorCode.InvalidParams, "'keys' is required"); + } + if (!string.IsNullOrEmpty(p.Selector)) + { + var el = SelectorResolver.ResolveOrThrow(p.Selector); + el.Focus(); + } + Keyboard.Type(p.Keys); return new { ok = true }; } + + private static AutomationElement ResolveAndCheckEnabled(string? selector) + { + if (string.IsNullOrEmpty(selector)) + { + throw new RpcException(RpcErrorCode.InvalidParams, "'selector' is required"); + } + var el = SelectorResolver.ResolveOrThrow(selector); + if (!el.Properties.IsEnabled.ValueOrDefault) + { + throw new RpcException(RpcErrorCode.ElementNotEnabled, $"Element is not enabled: {selector}"); + } + return el; + } + + private static string ToggleStateString(ToggleState s) => s switch + { + ToggleState.On => "on", + ToggleState.Off => "off", + ToggleState.Indeterminate => "indeterminate", + _ => "unknown", + }; } internal sealed class DoInvokeParams { [JsonPropertyName("selector")] public string? Selector { get; set; } } + +internal sealed class DoToggleParams +{ + [JsonPropertyName("selector")] public string? Selector { get; set; } + [JsonPropertyName("value")] public bool? Value { get; set; } +} + +internal sealed class DoSetValueParams +{ + [JsonPropertyName("selector")] public string? Selector { get; set; } + [JsonPropertyName("value")] public JsonElement? Value { get; set; } +} + +internal sealed class DoSelectParams +{ + [JsonPropertyName("selector")] public string? Selector { get; set; } + [JsonPropertyName("item")] public JsonElement? Item { get; set; } +} + +internal sealed class DoExpandParams +{ + [JsonPropertyName("selector")] public string? Selector { get; set; } + [JsonPropertyName("expand")] public bool ExpandValue { get; set; } = true; +} + +internal sealed class DoScrollParams +{ + [JsonPropertyName("selector")] public string? Selector { get; set; } + [JsonPropertyName("direction")] public string? Direction { get; set; } + [JsonPropertyName("amount")] public string? Amount { get; set; } +} + +internal sealed class DoFocusParams +{ + [JsonPropertyName("selector")] public string? Selector { get; set; } +} + +internal sealed class DoClickParams +{ + [JsonPropertyName("selector")] public string? Selector { get; set; } + [JsonPropertyName("button")] public string? Button { get; set; } + [JsonPropertyName("position")] public ClickPosition? Position { get; set; } +} + +internal sealed class ClickPosition +{ + [JsonPropertyName("x")] public double? X { get; set; } + [JsonPropertyName("y")] public double? Y { get; set; } +} + +internal sealed class DoSendKeysParams +{ + [JsonPropertyName("selector")] public string? Selector { get; set; } + [JsonPropertyName("keys")] public string? Keys { get; set; } +} diff --git a/dotnet/uiAutomationHelper/src/Methods/AppMethods.cs b/dotnet/uiAutomationHelper/src/Methods/AppMethods.cs index 2884d7dd37..58f7a0f2d6 100644 --- a/dotnet/uiAutomationHelper/src/Methods/AppMethods.cs +++ b/dotnet/uiAutomationHelper/src/Methods/AppMethods.cs @@ -84,7 +84,7 @@ public static void Register(Dispatch dispatch) return new { pid, mainWindow = BuildWindowSelector(window) }; } - private static object? List() + private static object? List() => ComRetry.Run(() => { var desktop = AutomationHost.Automation.GetDesktop(); var cf = AutomationHost.Automation.ConditionFactory; @@ -106,8 +106,8 @@ public static void Register(Dispatch dispatch) mainWindow = BuildWindowSelector(w), }); } - return results; - } + return (object?)results; + }); private static object? Kill(System.Text.Json.JsonElement? @params) { @@ -152,7 +152,7 @@ public static void Register(Dispatch dispatch) } internal static string BuildWindowSelector(AutomationElement window) => - Selectors.BuildSegment(window); + Selectors.BuildAbsolutePath(window); } internal sealed class AppLaunchParams diff --git a/dotnet/uiAutomationHelper/src/Methods/EventMethods.cs b/dotnet/uiAutomationHelper/src/Methods/EventMethods.cs new file mode 100644 index 0000000000..618aa9c582 --- /dev/null +++ b/dotnet/uiAutomationHelper/src/Methods/EventMethods.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Diagnostics; +using System.Text.Json.Serialization; +using UiAutomationHelper.Rpc; +using UiAutomationHelper.Uia; + +namespace UiAutomationHelper.Methods; + +internal static class EventMethods +{ + public static void Register(Dispatch dispatch) + { + dispatch.Register("events.idle", IdleAsync); + } + + private static async Task IdleAsync(System.Text.Json.JsonElement? @params, CancellationToken ct) + { + var p = RpcParams.Parse(@params); + int debounceMs = p.DebounceMs ?? 500; + int maxWaitMs = p.MaxWaitMs ?? 10000; + + EventBridge.EnsureSubscribed(); + EventBridge.ResetActivityClock(); + + var sw = Stopwatch.StartNew(); + while (sw.ElapsedMilliseconds < maxWaitMs) + { + if (EventBridge.QuietMs() >= debounceMs) + { + return new { ok = true, idle = true, waitedMs = sw.ElapsedMilliseconds }; + } + await Task.Delay(50, ct).ConfigureAwait(false); + } + return new { ok = true, idle = false, waitedMs = sw.ElapsedMilliseconds }; + } +} + +internal sealed class EventsIdleParams +{ + [JsonPropertyName("debounceMs")] public int? DebounceMs { get; set; } + [JsonPropertyName("maxWaitMs")] public int? MaxWaitMs { get; set; } +} diff --git a/dotnet/uiAutomationHelper/src/Methods/FindMethods.cs b/dotnet/uiAutomationHelper/src/Methods/FindMethods.cs new file mode 100644 index 0000000000..3689c36ed7 --- /dev/null +++ b/dotnet/uiAutomationHelper/src/Methods/FindMethods.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Diagnostics; +using System.Text.Json.Serialization; +using UiAutomationHelper.Models; +using UiAutomationHelper.Rpc; +using UiAutomationHelper.Uia; + +namespace UiAutomationHelper.Methods; + +internal static class FindMethods +{ + public static void Register(Dispatch dispatch) + { + dispatch.Register("find", FindAsync); + } + + private static async Task FindAsync(System.Text.Json.JsonElement? @params, CancellationToken ct) + { + var p = RpcParams.ParseRequired(@params); + if (string.IsNullOrEmpty(p.Selector)) + { + throw new RpcException(RpcErrorCode.InvalidParams, "'selector' is required"); + } + var path = SelectorParser.Parse(p.Selector); + int timeoutMs = p.TimeoutMs ?? 0; + var sw = Stopwatch.StartNew(); + while (true) + { + var element = SelectorResolver.Resolve(path); + if (element != null) + { + return new { found = true, resolved = p.Selector }; + } + if (sw.ElapsedMilliseconds >= timeoutMs) + { + return new { found = false }; + } + await Task.Delay(100, ct).ConfigureAwait(false); + } + } +} + +internal sealed class FindParams +{ + [JsonPropertyName("selector")] public string? Selector { get; set; } + [JsonPropertyName("timeoutMs")] public int? TimeoutMs { get; set; } +} diff --git a/dotnet/uiAutomationHelper/src/Methods/Register.cs b/dotnet/uiAutomationHelper/src/Methods/Register.cs index 76d2f3aed8..bcbbf102ea 100644 --- a/dotnet/uiAutomationHelper/src/Methods/Register.cs +++ b/dotnet/uiAutomationHelper/src/Methods/Register.cs @@ -14,5 +14,7 @@ public static void All(Dispatch dispatch) TreeMethods.Register(dispatch); ScreenshotMethods.Register(dispatch); ActionMethods.Register(dispatch); + FindMethods.Register(dispatch); + EventMethods.Register(dispatch); } } diff --git a/dotnet/uiAutomationHelper/src/Methods/TreeMethods.cs b/dotnet/uiAutomationHelper/src/Methods/TreeMethods.cs index b643ce3f38..c168c8be96 100644 --- a/dotnet/uiAutomationHelper/src/Methods/TreeMethods.cs +++ b/dotnet/uiAutomationHelper/src/Methods/TreeMethods.cs @@ -21,9 +21,12 @@ public static void Register(Dispatch dispatch) { throw new Models.RpcException(Models.RpcErrorCode.InvalidParams, "'root' is required"); } - var element = SelectorResolver.ResolveOrThrow(p.Root); - var depth = p.MaxDepth ?? 20; - return TreeWalker.Walk(element, p.Root, depth); + return ComRetry.Run(() => + { + var element = SelectorResolver.ResolveOrThrow(p.Root); + int depth = p.MaxDepth ?? 20; + return (object?)TreeWalker.Walk(element, p.Root, depth); + }); } } diff --git a/dotnet/uiAutomationHelper/src/Uia/AutomationHost.cs b/dotnet/uiAutomationHelper/src/Uia/AutomationHost.cs index 37919949b8..65d3a4ca80 100644 --- a/dotnet/uiAutomationHelper/src/Uia/AutomationHost.cs +++ b/dotnet/uiAutomationHelper/src/Uia/AutomationHost.cs @@ -14,14 +14,9 @@ public static UIA3Automation Automation { get { - if (_automation != null) - { - return _automation; - } lock (_lock) { - _automation ??= new UIA3Automation(); - return _automation; + return _automation ??= new UIA3Automation(); } } } diff --git a/dotnet/uiAutomationHelper/src/Uia/ComRetry.cs b/dotnet/uiAutomationHelper/src/Uia/ComRetry.cs new file mode 100644 index 0000000000..d77ccd9d44 --- /dev/null +++ b/dotnet/uiAutomationHelper/src/Uia/ComRetry.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Runtime.InteropServices; + +namespace UiAutomationHelper.Uia; + +/// +/// Wraps UIA operations that can throw transient COMExceptions when the desktop +/// tree is mutating mid-enumeration (e.g., immediately after a window closes). +/// +internal static class ComRetry +{ + public static T Run(Func op, int maxRetries = 2, int delayMs = 100) + { + Exception? last = null; + for (int attempt = 0; attempt <= maxRetries; attempt++) + { + try + { + return op(); + } + catch (COMException ex) when (IsTransient(ex)) + { + last = ex; + if (attempt == maxRetries) + { + break; + } + Thread.Sleep(delayMs); + } + } + throw last!; + } + + private static bool IsTransient(COMException ex) + { + // Common race-condition HRESULTs observed during teardown / structure-change. + return ex.HResult switch + { + unchecked((int)0x80040201) => true, // EVENT_E_QUERYSYNTAX (UIA event marshaling) + unchecked((int)0x80040E14) => true, // generic transient + unchecked((int)0x80131509) => true, // InvalidOperationException COM-mapped + unchecked((int)0x80004005) => true, // E_FAIL — frequently transient on UIA enumeration + _ => false, + }; + } +} diff --git a/dotnet/uiAutomationHelper/src/Uia/EventBridge.cs b/dotnet/uiAutomationHelper/src/Uia/EventBridge.cs new file mode 100644 index 0000000000..3288cb6cbb --- /dev/null +++ b/dotnet/uiAutomationHelper/src/Uia/EventBridge.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace UiAutomationHelper.Uia; + +/// +/// Centralized hook for UIA events. Slice 2 only tracks "any focus change" as a +/// proxy for "UIA had activity"; full event subscription with notifications lands +/// in slice 5 (record mode). +/// +internal static class EventBridge +{ + private static long _lastEventTicks = DateTime.UtcNow.Ticks; + private static IDisposable? _focusSub; + private static readonly object _subLock = new(); + + /// + /// Idempotent. Subscribes to global focus changes the first time it's called. + /// + public static void EnsureSubscribed() + { + lock (_subLock) + { + if (_focusSub != null) + { + return; + } + _focusSub = AutomationHost.Automation.RegisterFocusChangedEvent(_ => + { + Interlocked.Exchange(ref _lastEventTicks, DateTime.UtcNow.Ticks); + }); + } + } + + /// Resets the activity timestamp to "now". Call before measuring idle. + public static void ResetActivityClock() + { + Interlocked.Exchange(ref _lastEventTicks, DateTime.UtcNow.Ticks); + } + + /// Milliseconds since the last observed UIA event. + public static long QuietMs() + { + long lastTicks = Interlocked.Read(ref _lastEventTicks); + return (DateTime.UtcNow.Ticks - lastTicks) / TimeSpan.TicksPerMillisecond; + } +} diff --git a/dotnet/uiAutomationHelper/src/Uia/NativeMethods.cs b/dotnet/uiAutomationHelper/src/Uia/NativeMethods.cs new file mode 100644 index 0000000000..088b5f0211 --- /dev/null +++ b/dotnet/uiAutomationHelper/src/Uia/NativeMethods.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Runtime.InteropServices; + +namespace UiAutomationHelper.Uia; + +internal static class NativeMethods +{ + public const uint GA_PARENT = 1; + public const uint GA_ROOT = 2; + public const uint GA_ROOTOWNER = 3; + + [DllImport("user32.dll")] + public static extern IntPtr GetAncestor(IntPtr hwnd, uint gaFlags); +} diff --git a/dotnet/uiAutomationHelper/src/Uia/Selectors.cs b/dotnet/uiAutomationHelper/src/Uia/Selectors.cs index 6716096da7..0fbdfdd4f0 100644 --- a/dotnet/uiAutomationHelper/src/Uia/Selectors.cs +++ b/dotnet/uiAutomationHelper/src/Uia/Selectors.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System.Text; using FlaUI.Core.AutomationElements; using UiAutomationHelper.Models; @@ -8,6 +9,92 @@ namespace UiAutomationHelper.Uia; internal static class Selectors { + /// + /// Builds a desktop-rooted selector path for the given element by walking + /// up to (but not including) the desktop root. Required when the element + /// might be a non-top-level window (e.g., FlaUI returns UWP CoreWindow as + /// the main window, but it lives under an ApplicationFrameWindow). + /// + public static string BuildAbsolutePath(AutomationElement el) + { + // We need a desktop-rooted selector. For most elements that's just + // BuildSegment. The hard case is a UWP CoreWindow returned by + // app.GetMainWindow — Win32-wise it's a top-level window, but UIA's + // logical tree puts it under an ApplicationFrameWindow (different + // process). Strategies in order: + // 1) `el` IS a desktop child (matches by RuntimeId). + // 2) `el`'s Win32 OWNER is a desktop child. + // 3) Same-named desktop child exists (UWP CoreWindow / frame share name). + // 4) Fallback to single-segment selector. + var desktop = AutomationHost.Automation.GetDesktop(); + var elRid = el.Properties.RuntimeId.ValueOrDefault; + var elName = el.Properties.Name.ValueOrDefault ?? ""; + var elHwnd = (IntPtr)el.Properties.NativeWindowHandle.ValueOrDefault; + + // UWP apps create the ApplicationFrameWindow asynchronously after the + // CoreWindow appears. Poll desktop's children for up to 2 seconds. + for (int attempt = 0; attempt < 10; attempt++) + { + var topLevels = desktop.FindAllChildren(); + + // 1) Identity: el itself is a top-level child. + if (elRid != null) + { + foreach (var top in topLevels) + { + var topRid = top.Properties.RuntimeId.ValueOrDefault; + if (topRid != null && RidEquals(elRid, topRid)) + { + return BuildSegment(top); + } + } + } + + // 2) Win32 owner is a top-level child. + if (elHwnd != IntPtr.Zero) + { + var ownerHwnd = NativeMethods.GetAncestor(elHwnd, NativeMethods.GA_ROOTOWNER); + if (ownerHwnd != IntPtr.Zero && ownerHwnd != elHwnd) + { + foreach (var top in topLevels) + { + if ((IntPtr)top.Properties.NativeWindowHandle.ValueOrDefault == ownerHwnd) + { + return BuildSegment(top); + } + } + } + } + + // 3) Name match (UWP frame and core share the app's display name). + if (!string.IsNullOrEmpty(elName)) + { + foreach (var top in topLevels) + { + if (top.Properties.Name.ValueOrDefault == elName) + { + return BuildSegment(top); + } + } + } + + Thread.Sleep(200); + } + + return BuildSegment(el); + } + + private static bool RidEquals(int[] a, int[] b) + { + if (a.Length != b.Length) return false; + for (int i = 0; i < a.Length; i++) + { + if (a[i] != b[i]) return false; + } + return true; + } + + /// /// Builds a "/ControlType[predicate]" segment for an element. /// Priority: AutomationId → Name → ClassName → bare ControlType. @@ -19,11 +106,18 @@ public static string BuildSegment(AutomationElement el) var name = NullIfEmpty(el.Properties.Name.ValueOrDefault); var cls = NullIfEmpty(el.Properties.ClassName.ValueOrDefault); + // AutomationId on its own is unique enough. Otherwise include Name + + // ClassName as joint identifiers — two siblings can easily share Name + // (UWP wraps app windows in multiple layers, all named after the app). SelectorSegment seg; if (aid != null) { seg = new SelectorSegment(ct, AutomationId: aid); } + else if (name != null && cls != null) + { + seg = new SelectorSegment(ct, Name: name, ClassName: cls); + } else if (name != null) { seg = new SelectorSegment(ct, Name: name); diff --git a/ts/packages/agents/onboarding/src/uiCapture/helperClient.ts b/ts/packages/agents/onboarding/src/uiCapture/helperClient.ts index cfc86803cb..04e35fa1a6 100644 --- a/ts/packages/agents/onboarding/src/uiCapture/helperClient.ts +++ b/ts/packages/agents/onboarding/src/uiCapture/helperClient.ts @@ -211,6 +211,71 @@ export class HelperClient { return this.call("do.invoke", p); } + doToggle(p: { + selector: string; + value?: boolean; + }): Promise<{ ok: true; toggleState: string }> { + return this.call("do.toggle", p); + } + + doSetValue(p: { + selector: string; + value: string | number | boolean; + }): Promise<{ ok: true }> { + return this.call("do.setValue", p); + } + + doSelect(p: { + selector: string; + item?: string | number; + }): Promise<{ ok: true }> { + return this.call("do.select", p); + } + + doExpand(p: { selector: string; expand: boolean }): Promise<{ ok: true }> { + return this.call("do.expand", p); + } + + doScroll(p: { + selector: string; + direction: "up" | "down" | "left" | "right"; + amount?: "small" | "large"; + }): Promise<{ ok: true }> { + return this.call("do.scroll", p); + } + + doFocus(p: { selector: string }): Promise<{ ok: true }> { + return this.call("do.focus", p); + } + + doClick(p: { + selector: string; + button?: "left" | "right"; + position?: { x?: number; y?: number }; + }): Promise<{ ok: true }> { + return this.call("do.click", p); + } + + doSendKeys(p: { + selector?: string; + keys: string; + }): Promise<{ ok: true }> { + return this.call("do.sendKeys", p); + } + + find(p: { + selector: string; + timeoutMs?: number; + }): Promise<{ found: boolean; resolved?: string }> { + return this.call("find", p); + } + + eventsIdle( + p: { debounceMs?: number; maxWaitMs?: number } = {}, + ): Promise<{ ok: true; idle: boolean; waitedMs: number }> { + return this.call("events.idle", p); + } + /** * Close the helper's stdin and wait up to `timeoutMs` for graceful exit. * If it doesn't exit, send SIGKILL. diff --git a/ts/packages/agents/onboarding/src/uiCapture/test/clockSmoke.ts b/ts/packages/agents/onboarding/src/uiCapture/test/clockSmoke.ts index 56eebc37ca..696872e9ca 100644 --- a/ts/packages/agents/onboarding/src/uiCapture/test/clockSmoke.ts +++ b/ts/packages/agents/onboarding/src/uiCapture/test/clockSmoke.ts @@ -13,6 +13,8 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)); const fixturesDir = path.resolve(__dirname, "../../..", "test/fixtures/uiCapture"); const CLOCK_AUMID = "Microsoft.WindowsAlarms_8wekyb3d8bbwe!App"; +const CLOSE_SELECTOR = + '/Window[Name="Clock"]/Window[AutomationId="TitleBar"]/Button[AutomationId="Close"]'; function log(msg: string): void { process.stdout.write(`[smoke] ${msg}\n`); @@ -22,24 +24,15 @@ function countNodes(node: TreeNode): number { return 1 + node.children.reduce((s, c) => s + countNodes(c), 0); } -function countInvocableButtons(node: TreeNode): number { - let n = node.patterns.includes("Invoke") && node.controlType === "Button" ? 1 : 0; - for (const c of node.children) { - n += countInvocableButtons(c); - } - return n; -} - -function findInvocable(node: TreeNode, namePrefix?: string): TreeNode | null { - if ( - node.patterns.includes("Invoke") && - node.isEnabled && - (!namePrefix || (node.name ?? "").startsWith(namePrefix)) - ) { +function findFirst( + node: TreeNode, + pred: (n: TreeNode) => boolean, +): TreeNode | null { + if (pred(node)) { return node; } for (const c of node.children) { - const f = findInvocable(c, namePrefix); + const f = findFirst(c, pred); if (f) { return f; } @@ -51,6 +44,11 @@ async function sleep(ms: number): Promise { await new Promise((res) => setTimeout(res, ms)); } +function saveFixture(name: string, data: unknown): void { + writeFileSync(path.join(fixturesDir, name), JSON.stringify(data, null, 2)); + log(`saved ${name}`); +} + async function main(): Promise { mkdirSync(fixturesDir, { recursive: true }); log(`fixtures → ${fixturesDir}`); @@ -60,13 +58,13 @@ async function main(): Promise { const ping = await client.ping(); log(`ping ok, version=${ping.version}`); + // Close any pre-existing Clock instances. const existing = await client.appList(); - const stale = existing.filter((w) => w.title.includes("Clock")); - for (const w of stale) { + for (const w of existing.filter((x) => x.title.includes("Clock"))) { log(`closing existing Clock pid=${w.pid}`); await client.appKill({ pid: w.pid }); } - if (stale.length) { + if (existing.some((x) => x.title.includes("Clock"))) { await sleep(1500); } @@ -74,48 +72,107 @@ async function main(): Promise { const launch = await client.appLaunch({ aumid: CLOCK_AUMID }); log(`launched pid=${launch.pid} mainWindow=${launch.mainWindow}`); - // Poll until the UWP tree populates with at least a few invokable buttons. - let tree = await client.treeDump({ root: launch.mainWindow, maxDepth: 6 }); + // Wait for UIA to settle, then poll until the NavView shows up + // (UWP populates the tree progressively after window creation). + const idle1 = await client.eventsIdle({ debounceMs: 800, maxWaitMs: 5000 }); + log(`events.idle #1: idle=${idle1.idle}, waited=${idle1.waitedMs}ms`); + + let tree = await client.treeDump({ root: launch.mainWindow, maxDepth: 8 }); let attempts = 0; - while (countInvocableButtons(tree) < 3 && attempts < 10) { - await sleep(500); - tree = await client.treeDump({ root: launch.mainWindow, maxDepth: 6 }); + while ( + !findFirst(tree, (n) => n.automationId === "NavView") && + attempts < 8 + ) { + await sleep(400); + tree = await client.treeDump({ root: launch.mainWindow, maxDepth: 8 }); attempts++; } log( - `tree settled after ${attempts} polls: ${countNodes(tree)} nodes, ` + - `${countInvocableButtons(tree)} invokable buttons`, + `tree settled after ${attempts} polls: ${countNodes(tree)} nodes ` + + `(NavView ${findFirst(tree, (n) => n.automationId === "NavView") ? "found" : "missing"})`, ); - - writeFileSync( - path.join(fixturesDir, "clock-tree-launched.json"), - JSON.stringify(tree, null, 2), - ); - log("saved clock-tree-launched.json"); + saveFixture("clock-tree-launched.json", tree); const shot = await client.screenshot({ root: launch.mainWindow }); writeFileSync( path.join(fixturesDir, "clock-launched.png"), Buffer.from(shot.pngBase64, "base64"), ); - log(`saved clock-launched.png (${shot.rect.width}x${shot.rect.height})`); + log(`screenshot saved (${shot.rect.width}x${shot.rect.height})`); - // Pick a target for do.invoke. Prefer Close so we leave the system clean. - const target = findInvocable(tree, "Close ") ?? findInvocable(tree); - if (!target) { - throw new Error("no invokable element found in Clock tree"); + // Verify `find` works against a known stable selector. + const closeFind = await client.find({ selector: CLOSE_SELECTOR }); + if (!closeFind.found) { + throw new Error("find: Close button not resolved"); } - log(`do.invoke target: ${target.selector}`); - await client.doInvoke({ selector: target.selector }); - await sleep(800); + log(`find: Close button resolved=${closeFind.resolved}`); + + // Try to navigate to a known nav tab. NavView items are ListItem + // with SelectionItem pattern (so do.select, not do.invoke). + const navNames = [ + "Alarm", + "Alarms", + "Timer", + "Stopwatch", + "World clock", + "Focus sessions", + ]; + const navTab = navNames.reduce( + (acc, n) => + acc ?? + findFirst( + tree, + (node) => + node.name === n && + (node.patterns.includes("SelectionItem") || + node.patterns.includes("Invoke")), + ), + null, + ); + + if (navTab) { + const verb = navTab.patterns.includes("SelectionItem") + ? "select" + : "invoke"; + log(`nav tab found: ${navTab.name} → ${navTab.selector}`); + const navFind = await client.find({ + selector: navTab.selector, + timeoutMs: 1000, + }); + log(`find nav tab: found=${navFind.found}`); + log(`do.${verb} nav tab`); + if (verb === "select") { + await client.doSelect({ selector: navTab.selector }); + } else { + await client.doInvoke({ selector: navTab.selector }); + } + const idle2 = await client.eventsIdle({ + debounceMs: 800, + maxWaitMs: 3000, + }); + log(`events.idle #2: idle=${idle2.idle}, waited=${idle2.waitedMs}ms`); + const navTree = await client.treeDump({ + root: launch.mainWindow, + maxDepth: 8, + }); + saveFixture("clock-tree-navigated.json", navTree); + log(`post-nav tree: ${countNodes(navTree)} nodes`); + } else { + log("no nav tab found, skipping navigation"); + } + + // Exercise do.focus (no observable result, just that it doesn't throw). + await client.doFocus({ selector: CLOSE_SELECTOR }); + log("do.focus ok"); - // If Close was invoked, Clock is gone. Try kill anyway as belt-and-suspenders. + // Final cleanup: invoke Close. + await client.doInvoke({ selector: CLOSE_SELECTOR }); + await sleep(800); try { await client.appKill({ pid: launch.pid }); } catch { /* already gone */ } - log("DONE"); } finally { await client.dispose(); diff --git a/ts/packages/agents/onboarding/test/fixtures/uiCapture/clock-launched.png b/ts/packages/agents/onboarding/test/fixtures/uiCapture/clock-launched.png index 8fa640f3599197f046cdc68d88ae75f3b089e869..0ec534f49cc413ee66fdf58122007e91b5951c69 100644 GIT binary patch literal 131716 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%0A*p)vsoPH2?^_^>HH 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%0A0XX(B(F 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 Dc7bax diff --git a/ts/packages/agents/onboarding/test/fixtures/uiCapture/clock-tree-launched.json b/ts/packages/agents/onboarding/test/fixtures/uiCapture/clock-tree-launched.json index e2a95e1b87..d0eb20717d 100644 --- a/ts/packages/agents/onboarding/test/fixtures/uiCapture/clock-tree-launched.json +++ b/ts/packages/agents/onboarding/test/fixtures/uiCapture/clock-tree-launched.json @@ -1,11 +1,11 @@ { - "selector": "/Window[Name=\"Clock\"]", + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]", "name": "Clock", "controlType": "Window", "className": "ApplicationFrameWindow", "isEnabled": true, "isOffscreen": false, - "hasKeyboardFocus": true, + "hasKeyboardFocus": false, "patterns": [ "Window" ], @@ -17,7 +17,7 @@ }, "children": [ { - "selector": "/Window[Name=\"Clock\"]/Window[AutomationId=\"TitleBar\"]", + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[AutomationId=\"TitleBar\"]", "automationId": "TitleBar", "name": "Clock", "controlType": "Window", @@ -29,15 +29,15 @@ "Value" ], "boundingRect": { - "x": 159, + "x": 1132, "y": 121, - "width": 1206, + "width": 233, "height": 40 }, "value": "Clock", "children": [ { - "selector": "/Window[Name=\"Clock\"]/Window[AutomationId=\"TitleBar\"]/MenuBar[AutomationId=\"SystemMenuBar\"]", + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[AutomationId=\"TitleBar\"]/MenuBar[AutomationId=\"SystemMenuBar\"]", "automationId": "SystemMenuBar", "name": "System", "controlType": "MenuBar", @@ -53,7 +53,7 @@ }, "children": [ { - "selector": "/Window[Name=\"Clock\"]/Window[AutomationId=\"TitleBar\"]/MenuBar[AutomationId=\"SystemMenuBar\"]/MenuItem[Name=\"System\"]", + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[AutomationId=\"TitleBar\"]/MenuBar[AutomationId=\"SystemMenuBar\"]/MenuItem[Name=\"System\"]", "name": "System", "controlType": "MenuItem", "isEnabled": true, @@ -73,7 +73,7 @@ ] }, { - "selector": "/Window[Name=\"Clock\"]/Window[AutomationId=\"TitleBar\"]/Button[AutomationId=\"Minimize\"]", + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[AutomationId=\"TitleBar\"]/Button[AutomationId=\"Minimize\"]", "automationId": "Minimize", "name": "Minimize Clock", "controlType": "Button", @@ -92,7 +92,7 @@ "children": [] }, { - "selector": "/Window[Name=\"Clock\"]/Window[AutomationId=\"TitleBar\"]/Button[AutomationId=\"Maximize\"]", + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[AutomationId=\"TitleBar\"]/Button[AutomationId=\"Maximize\"]", "automationId": "Maximize", "name": "Maximize Clock", "controlType": "Button", @@ -111,7 +111,7 @@ "children": [] }, { - "selector": "/Window[Name=\"Clock\"]/Window[AutomationId=\"TitleBar\"]/Button[AutomationId=\"Close\"]", + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[AutomationId=\"TitleBar\"]/Button[AutomationId=\"Close\"]", "automationId": "Close", "name": "Close Clock", "controlType": "Button", @@ -132,7 +132,1165 @@ ] }, { - "selector": "/Window[Name=\"Clock\"]/Pane[ClassName=\"ApplicationFrameInputSinkWindow\"]", + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]", + "name": "Clock", + "controlType": "Window", + "className": "Windows.UI.Core.CoreWindow", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": true, + "patterns": [], + "boundingRect": { + "x": 159, + "y": 121, + "width": 1206, + "height": 1004 + }, + "children": [ + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]", + "automationId": "NavView", + "controlType": "Custom", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Selection" + ], + "boundingRect": { + "x": 159, + "y": 121, + "width": 1206, + "height": 1004 + }, + "children": [ + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Window[AutomationId=\"PaneRoot\"]", + "automationId": "PaneRoot", + "controlType": "Window", + "className": "SplitViewPane", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [], + "boundingRect": { + "x": 159, + "y": 121, + "width": 350, + "height": 1004 + }, + "children": [ + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Window[AutomationId=\"PaneRoot\"]/Pane[AutomationId=\"MenuItemsScrollViewer\"]", + "automationId": "MenuItemsScrollViewer", + "controlType": "Pane", + "className": "ScrollViewer", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Scroll" + ], + "boundingRect": { + "x": 159, + "y": 166, + "width": 350, + "height": 854 + }, + "children": [ + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Window[AutomationId=\"PaneRoot\"]/Pane[AutomationId=\"MenuItemsScrollViewer\"]/Group[AutomationId=\"MenuItemsHost\"]", + "automationId": "MenuItemsHost", + "controlType": "Group", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [], + "boundingRect": { + "x": 164, + "y": 169, + "width": 340, + "height": 245 + }, + "children": [ + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Window[AutomationId=\"PaneRoot\"]/Pane[AutomationId=\"MenuItemsScrollViewer\"]/Group[AutomationId=\"MenuItemsHost\"]/ListItem[AutomationId=\"FocusButton\"]", + "automationId": "FocusButton", + "name": "Focus sessions", + "controlType": "ListItem", + "className": "Microsoft.UI.Xaml.Controls.NavigationViewItem", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": true, + "patterns": [ + "SelectionItem" + ], + "boundingRect": { + "x": 164, + "y": 169, + "width": 340, + "height": 45 + }, + "children": [] + }, + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Window[AutomationId=\"PaneRoot\"]/Pane[AutomationId=\"MenuItemsScrollViewer\"]/Group[AutomationId=\"MenuItemsHost\"]/ListItem[AutomationId=\"TimerButton\"]", + "automationId": "TimerButton", + "name": "Timer", + "controlType": "ListItem", + "className": "Microsoft.UI.Xaml.Controls.NavigationViewItem", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "SelectionItem" + ], + "boundingRect": { + "x": 164, + "y": 219, + "width": 340, + "height": 45 + }, + "children": [] + }, + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Window[AutomationId=\"PaneRoot\"]/Pane[AutomationId=\"MenuItemsScrollViewer\"]/Group[AutomationId=\"MenuItemsHost\"]/ListItem[AutomationId=\"AlarmButton\"]", + "automationId": "AlarmButton", + "name": "Alarm", + "controlType": "ListItem", + "className": "Microsoft.UI.Xaml.Controls.NavigationViewItem", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "SelectionItem" + ], + "boundingRect": { + "x": 164, + "y": 269, + "width": 340, + "height": 45 + }, + "children": [] + }, + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Window[AutomationId=\"PaneRoot\"]/Pane[AutomationId=\"MenuItemsScrollViewer\"]/Group[AutomationId=\"MenuItemsHost\"]/ListItem[AutomationId=\"StopwatchButton\"]", + "automationId": "StopwatchButton", + "name": "Stopwatch", + "controlType": "ListItem", + "className": "Microsoft.UI.Xaml.Controls.NavigationViewItem", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "SelectionItem" + ], + "boundingRect": { + "x": 164, + "y": 319, + "width": 340, + "height": 45 + }, + "children": [] + }, + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Window[AutomationId=\"PaneRoot\"]/Pane[AutomationId=\"MenuItemsScrollViewer\"]/Group[AutomationId=\"MenuItemsHost\"]/ListItem[AutomationId=\"ClockButton\"]", + "automationId": "ClockButton", + "name": "World clock", + "controlType": "ListItem", + "className": "Microsoft.UI.Xaml.Controls.NavigationViewItem", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "SelectionItem" + ], + "boundingRect": { + "x": 164, + "y": 369, + "width": 340, + "height": 45 + }, + "children": [] + } + ] + } + ] + }, + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Window[AutomationId=\"PaneRoot\"]/Button[AutomationId=\"SignInButton\"]", + "automationId": "SignInButton", + "name": "Logged user: Robert Gruen", + "controlType": "Button", + "className": "Button", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Invoke" + ], + "boundingRect": { + "x": 164, + "y": 1020, + "width": 340, + "height": 45 + }, + "children": [ + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Window[AutomationId=\"PaneRoot\"]/Button[AutomationId=\"SignInButton\"]/Text[AutomationId=\"TipBlock\"]", + "automationId": "TipBlock", + "controlType": "Text", + "className": "TextBlock", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Text" + ], + "boundingRect": { + "x": 217, + "y": 1031, + "width": 104, + "height": 24 + }, + "children": [] + }, + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Window[AutomationId=\"PaneRoot\"]/Button[AutomationId=\"SignInButton\"]/Text[AutomationId=\"UserNameBlock\"]", + "automationId": "UserNameBlock", + "name": "Robert Gruen", + "controlType": "Text", + "className": "TextBlock", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Text" + ], + "boundingRect": { + "x": 217, + "y": 1031, + "width": 104, + "height": 24 + }, + "children": [] + } + ] + }, + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Window[AutomationId=\"PaneRoot\"]/Pane[AutomationId=\"FooterItemsScrollViewer\"]", + "automationId": "FooterItemsScrollViewer", + "controlType": "Pane", + "className": "ScrollViewer", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Scroll" + ], + "boundingRect": { + "x": 159, + "y": 1070, + "width": 350, + "height": 50 + }, + "children": [ + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Window[AutomationId=\"PaneRoot\"]/Pane[AutomationId=\"FooterItemsScrollViewer\"]/Group[AutomationId=\"FooterMenuItemsHost\"]", + "automationId": "FooterMenuItemsHost", + "controlType": "Group", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [], + "boundingRect": { + "x": 164, + "y": 1073, + "width": 340, + "height": 45 + }, + "children": [ + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Window[AutomationId=\"PaneRoot\"]/Pane[AutomationId=\"FooterItemsScrollViewer\"]/Group[AutomationId=\"FooterMenuItemsHost\"]/ListItem[AutomationId=\"SettingsItem\"]", + "automationId": "SettingsItem", + "name": "Settings", + "controlType": "ListItem", + "className": "Microsoft.UI.Xaml.Controls.NavigationViewItem", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "SelectionItem" + ], + "boundingRect": { + "x": 164, + "y": 1073, + "width": 340, + "height": 45 + }, + "children": [] + } + ] + } + ] + } + ] + }, + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Pane[AutomationId=\"PART_Pane1ScrollViewer\"]", + "automationId": "PART_Pane1ScrollViewer", + "controlType": "Pane", + "className": "ScrollViewer", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Scroll" + ], + "boundingRect": { + "x": 510, + "y": 161, + "width": 855, + "height": 964 + }, + "children": [ + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Pane[AutomationId=\"PART_Pane1ScrollViewer\"]/Pane[ClassName=\"ScrollViewer\"]", + "controlType": "Pane", + "className": "ScrollViewer", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Scroll" + ], + "boundingRect": { + "x": 510, + "y": 161, + "width": 855, + "height": 964 + }, + "children": [ + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Pane[AutomationId=\"PART_Pane1ScrollViewer\"]/Pane[ClassName=\"ScrollViewer\"]/Group[Name=\"Focus timer view\"][ClassName=\"NamedContainerAutomationPeer\"]", + "name": "Focus timer view", + "controlType": "Group", + "className": "NamedContainerAutomationPeer", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [], + "boundingRect": { + "x": 550, + "y": 176, + "width": 380, + "height": 530 + }, + "children": [ + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Pane[AutomationId=\"PART_Pane1ScrollViewer\"]/Pane[ClassName=\"ScrollViewer\"]/Group[Name=\"Focus timer view\"][ClassName=\"NamedContainerAutomationPeer\"]/Button[AutomationId=\"AotButton\"]", + "automationId": "AotButton", + "name": "Keep on top", + "controlType": "Button", + "className": "Button", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Invoke" + ], + "boundingRect": { + "x": 840, + "y": 181, + "width": 40, + "height": 40 + }, + "children": [] + }, + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Pane[AutomationId=\"PART_Pane1ScrollViewer\"]/Pane[ClassName=\"ScrollViewer\"]/Group[Name=\"Focus timer view\"][ClassName=\"NamedContainerAutomationPeer\"]/Button[AutomationId=\"ContextMenuButton\"]", + "automationId": "ContextMenuButton", + "name": "See more", + "controlType": "Button", + "className": "Button", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Invoke" + ], + "boundingRect": { + "x": 885, + "y": 181, + "width": 40, + "height": 40 + }, + "children": [] + }, + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Pane[AutomationId=\"PART_Pane1ScrollViewer\"]/Pane[ClassName=\"ScrollViewer\"]/Group[Name=\"Focus timer view\"][ClassName=\"NamedContainerAutomationPeer\"]/Text[Name=\"Get ready to focus\"][ClassName=\"TextBlock\"]", + "name": "Get ready to focus", + "controlType": "Text", + "className": "TextBlock", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Text" + ], + "boundingRect": { + "x": 636, + "y": 221, + "width": 209, + "height": 34 + }, + "children": [] + }, + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Pane[AutomationId=\"PART_Pane1ScrollViewer\"]/Pane[ClassName=\"ScrollViewer\"]/Group[Name=\"Focus timer view\"][ClassName=\"NamedContainerAutomationPeer\"]/Text[Name=\"We’ll turn off notifications and app alerts during each session. For longer sessions, we’ll add a short break so you can recharge.\"][ClassName=\"TextBlock\"]", + "name": "We’ll turn off notifications and app alerts during each session. For longer sessions, we’ll add a short break so you can recharge.", + "controlType": "Text", + "className": "TextBlock", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Text" + ], + "boundingRect": { + "x": 570, + "y": 265, + "width": 339, + "height": 70 + }, + "children": [] + }, + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Pane[AutomationId=\"PART_Pane1ScrollViewer\"]/Pane[ClassName=\"ScrollViewer\"]/Group[Name=\"Focus timer view\"][ClassName=\"NamedContainerAutomationPeer\"]/Spinner[AutomationId=\"FullNumberBox\"]", + "automationId": "FullNumberBox", + "controlType": "Spinner", + "className": "Microsoft.UI.Xaml.Controls.NumberBox", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "RangeValue" + ], + "boundingRect": { + "x": 640, + "y": 365, + "width": 200, + "height": 110 + }, + "children": [ + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Pane[AutomationId=\"PART_Pane1ScrollViewer\"]/Pane[ClassName=\"ScrollViewer\"]/Group[Name=\"Focus timer view\"][ClassName=\"NamedContainerAutomationPeer\"]/Spinner[AutomationId=\"FullNumberBox\"]/Edit[AutomationId=\"InputBox\"]", + "automationId": "InputBox", + "name": "Duration of focus session in minutes", + "controlType": "Edit", + "className": "TextBox", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Value", + "Text" + ], + "boundingRect": { + "x": 640, + "y": 365, + "width": 200, + "height": 110 + }, + "value": "25", + "children": [] + }, + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Pane[AutomationId=\"PART_Pane1ScrollViewer\"]/Pane[ClassName=\"ScrollViewer\"]/Group[Name=\"Focus timer view\"][ClassName=\"NamedContainerAutomationPeer\"]/Spinner[AutomationId=\"FullNumberBox\"]/Button[AutomationId=\"UpSpinButton\"]", + "automationId": "UpSpinButton", + "name": "Increase the focus session time", + "controlType": "Button", + "className": "RepeatButton", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Invoke" + ], + "boundingRect": { + "x": 780, + "y": 365, + "width": 60, + "height": 55 + }, + "children": [] + }, + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Pane[AutomationId=\"PART_Pane1ScrollViewer\"]/Pane[ClassName=\"ScrollViewer\"]/Group[Name=\"Focus timer view\"][ClassName=\"NamedContainerAutomationPeer\"]/Spinner[AutomationId=\"FullNumberBox\"]/Button[AutomationId=\"DownSpinButton\"]", + "automationId": "DownSpinButton", + "name": "Decrease the focus session time", + "controlType": "Button", + "className": "RepeatButton", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Invoke" + ], + "boundingRect": { + "x": 780, + "y": 420, + "width": 60, + "height": 55 + }, + "children": [] + } + ] + }, + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Pane[AutomationId=\"PART_Pane1ScrollViewer\"]/Pane[ClassName=\"ScrollViewer\"]/Group[Name=\"Focus timer view\"][ClassName=\"NamedContainerAutomationPeer\"]/Text[AutomationId=\"BreakBlock\"]", + "automationId": "BreakBlock", + "name": "You’ll have no breaks", + "controlType": "Text", + "className": "TextBlock", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Text" + ], + "boundingRect": { + "x": 659, + "y": 495, + "width": 163, + "height": 24 + }, + "children": [] + }, + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Pane[AutomationId=\"PART_Pane1ScrollViewer\"]/Pane[ClassName=\"ScrollViewer\"]/Group[Name=\"Focus timer view\"][ClassName=\"NamedContainerAutomationPeer\"]/CheckBox[AutomationId=\"HasBreaksCheckbox\"]", + "automationId": "HasBreaksCheckbox", + "name": "Skip breaks", + "controlType": "CheckBox", + "className": "CheckBox", + "isEnabled": false, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Toggle" + ], + "boundingRect": { + "x": 679, + "y": 529, + "width": 123, + "height": 40 + }, + "toggleState": "off", + "children": [] + }, + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Pane[AutomationId=\"PART_Pane1ScrollViewer\"]/Pane[ClassName=\"ScrollViewer\"]/Group[Name=\"Focus timer view\"][ClassName=\"NamedContainerAutomationPeer\"]/Button[AutomationId=\"StartButton\"]", + "automationId": "StartButton", + "name": "Start focus session", + "controlType": "Button", + "className": "Button", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Invoke" + ], + "boundingRect": { + "x": 639, + "y": 599, + "width": 202, + "height": 40 + }, + "children": [ + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Pane[AutomationId=\"PART_Pane1ScrollViewer\"]/Pane[ClassName=\"ScrollViewer\"]/Group[Name=\"Focus timer view\"][ClassName=\"NamedContainerAutomationPeer\"]/Button[AutomationId=\"StartButton\"]/Text[AutomationId=\"StartButtonText\"]", + "automationId": "StartButtonText", + "name": "Start focus session", + "controlType": "Text", + "className": "TextBlock", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Text" + ], + "boundingRect": { + "x": 684, + "y": 612, + "width": 143, + "height": 13 + }, + "children": [] + } + ] + } + ] + }, + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Pane[AutomationId=\"PART_Pane1ScrollViewer\"]/Pane[ClassName=\"ScrollViewer\"]/Group[AutomationId=\"PanelContainer\"]", + "automationId": "PanelContainer", + "name": "To do task view", + "controlType": "Group", + "className": "NamedContainerAutomationPeer", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [], + "boundingRect": { + "x": 550, + "y": 721, + "width": 380, + "height": 358 + }, + "children": [ + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Pane[AutomationId=\"PART_Pane1ScrollViewer\"]/Pane[ClassName=\"ScrollViewer\"]/Group[AutomationId=\"PanelContainer\"]/Image[ClassName=\"Image\"]", + "controlType": "Image", + "className": "Image", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [], + "boundingRect": { + "x": 570, + "y": 736, + "width": 25, + "height": 20 + }, + "children": [] + }, + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Pane[AutomationId=\"PART_Pane1ScrollViewer\"]/Pane[ClassName=\"ScrollViewer\"]/Group[AutomationId=\"PanelContainer\"]/Text[AutomationId=\"TitleBlock\"]", + "automationId": "TitleBlock", + "name": "Tasks", + "controlType": "Text", + "className": "TextBlock", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Text" + ], + "boundingRect": { + "x": 605, + "y": 734, + "width": 42, + "height": 24 + }, + "children": [] + }, + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Pane[AutomationId=\"PART_Pane1ScrollViewer\"]/Pane[ClassName=\"ScrollViewer\"]/Group[AutomationId=\"PanelContainer\"]/Button[AutomationId=\"AddButton\"]", + "automationId": "AddButton", + "name": "Add a task", + "controlType": "Button", + "className": "Button", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Invoke" + ], + "boundingRect": { + "x": 840, + "y": 726, + "width": 40, + "height": 40 + }, + "children": [] + }, + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Pane[AutomationId=\"PART_Pane1ScrollViewer\"]/Pane[ClassName=\"ScrollViewer\"]/Group[AutomationId=\"PanelContainer\"]/Button[AutomationId=\"ContextMenuButton\"]", + "automationId": "ContextMenuButton", + "name": "See more", + "controlType": "Button", + "className": "Button", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Invoke" + ], + "boundingRect": { + "x": 885, + "y": 726, + "width": 40, + "height": 40 + }, + "children": [] + }, + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Pane[AutomationId=\"PART_Pane1ScrollViewer\"]/Pane[ClassName=\"ScrollViewer\"]/Group[AutomationId=\"PanelContainer\"]/Text[Name=\"Stay on track\"][ClassName=\"TextBlock\"]", + "name": "Stay on track", + "controlType": "Text", + "className": "TextBlock", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Text" + ], + "boundingRect": { + "x": 666, + "y": 837, + "width": 148, + "height": 34 + }, + "children": [] + }, + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Pane[AutomationId=\"PART_Pane1ScrollViewer\"]/Pane[ClassName=\"ScrollViewer\"]/Group[AutomationId=\"PanelContainer\"]/Text[Name=\"Add tasks and assign them to focus sessions throughout your day.\"][ClassName=\"TextBlock\"]", + "name": "Add tasks and assign them to focus sessions throughout your day.", + "controlType": "Text", + "className": "TextBlock", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Text" + ], + "boundingRect": { + "x": 603, + "y": 876, + "width": 274, + "height": 47 + }, + "children": [] + }, + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Pane[AutomationId=\"PART_Pane1ScrollViewer\"]/Pane[ClassName=\"ScrollViewer\"]/Group[AutomationId=\"PanelContainer\"]/Button[AutomationId=\"AddTaskButton\"]", + "automationId": "AddTaskButton", + "name": "Add a task", + "controlType": "Button", + "className": "Button", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Invoke" + ], + "boundingRect": { + "x": 671, + "y": 953, + "width": 139, + "height": 39 + }, + "children": [ + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Pane[AutomationId=\"PART_Pane1ScrollViewer\"]/Pane[ClassName=\"ScrollViewer\"]/Group[AutomationId=\"PanelContainer\"]/Button[AutomationId=\"AddTaskButton\"]/Text[Name=\"Add a task\"][ClassName=\"TextBlock\"]", + "name": "Add a task", + "controlType": "Text", + "className": "TextBlock", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Text" + ], + "boundingRect": { + "x": 713, + "y": 966, + "width": 82, + "height": 13 + }, + "children": [] + } + ] + } + ] + }, + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Pane[AutomationId=\"PART_Pane1ScrollViewer\"]/Pane[ClassName=\"ScrollViewer\"]/Group[Name=\"Focus summary view\"][ClassName=\"NamedContainerAutomationPeer\"]", + "name": "Focus summary view", + "controlType": "Group", + "className": "NamedContainerAutomationPeer", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [], + "boundingRect": { + "x": 945, + "y": 176, + "width": 380, + "height": 360 + }, + "children": [ + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Pane[AutomationId=\"PART_Pane1ScrollViewer\"]/Pane[ClassName=\"ScrollViewer\"]/Group[Name=\"Focus summary view\"][ClassName=\"NamedContainerAutomationPeer\"]/Text[Name=\"Daily progress\"][ClassName=\"TextBlock\"]", + "name": "Daily progress", + "controlType": "Text", + "className": "TextBlock", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Text" + ], + "boundingRect": { + "x": 965, + "y": 189, + "width": 310, + "height": 38 + }, + "children": [] + }, + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Pane[AutomationId=\"PART_Pane1ScrollViewer\"]/Pane[ClassName=\"ScrollViewer\"]/Group[Name=\"Focus summary view\"][ClassName=\"NamedContainerAutomationPeer\"]/Button[AutomationId=\"EditFocusGoalsButton\"]", + "automationId": "EditFocusGoalsButton", + "name": "Edit your daily goal", + "controlType": "Button", + "className": "Button", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Invoke" + ], + "boundingRect": { + "x": 1280, + "y": 181, + "width": 40, + "height": 40 + }, + "children": [] + }, + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Pane[AutomationId=\"PART_Pane1ScrollViewer\"]/Pane[ClassName=\"ScrollViewer\"]/Group[Name=\"Focus summary view\"][ClassName=\"NamedContainerAutomationPeer\"]/Text[Name=\"Yesterday\"][ClassName=\"TextBlock\"]", + "name": "Yesterday", + "controlType": "Text", + "className": "TextBlock", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Text" + ], + "boundingRect": { + "x": 955, + "y": 299, + "width": 74, + "height": 24 + }, + "children": [] + }, + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Pane[AutomationId=\"PART_Pane1ScrollViewer\"]/Pane[ClassName=\"ScrollViewer\"]/Group[Name=\"Focus summary view\"][ClassName=\"NamedContainerAutomationPeer\"]/Text[Name=\"0\"][ClassName=\"TextBlock\"]", + "name": "0", + "controlType": "Text", + "className": "TextBlock", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Text" + ], + "boundingRect": { + "x": 980, + "y": 323, + "width": 24, + "height": 57 + }, + "children": [] + }, + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Pane[AutomationId=\"PART_Pane1ScrollViewer\"]/Pane[ClassName=\"ScrollViewer\"]/Group[Name=\"Focus summary view\"][ClassName=\"NamedContainerAutomationPeer\"]/Text[Name=\"minutes\"][ClassName=\"TextBlock\"]", + "name": "minutes", + "controlType": "Text", + "className": "TextBlock", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Text" + ], + "boundingRect": { + "x": 961, + "y": 380, + "width": 61, + "height": 24 + }, + "children": [] + }, + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Pane[AutomationId=\"PART_Pane1ScrollViewer\"]/Pane[ClassName=\"ScrollViewer\"]/Group[Name=\"Focus summary view\"][ClassName=\"NamedContainerAutomationPeer\"]/Text[Name=\"Streak\"][ClassName=\"TextBlock\"]", + "name": "Streak", + "controlType": "Text", + "className": "TextBlock", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Text" + ], + "boundingRect": { + "x": 1254, + "y": 299, + "width": 48, + "height": 24 + }, + "children": [] + }, + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Pane[AutomationId=\"PART_Pane1ScrollViewer\"]/Pane[ClassName=\"ScrollViewer\"]/Group[Name=\"Focus summary view\"][ClassName=\"NamedContainerAutomationPeer\"]/Text[Name=\"0\"][ClassName=\"TextBlock\"]", + "name": "0", + "controlType": "Text", + "className": "TextBlock", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Text" + ], + "boundingRect": { + "x": 1266, + "y": 323, + "width": 24, + "height": 57 + }, + "children": [] + }, + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Pane[AutomationId=\"PART_Pane1ScrollViewer\"]/Pane[ClassName=\"ScrollViewer\"]/Group[Name=\"Focus summary view\"][ClassName=\"NamedContainerAutomationPeer\"]/Text[Name=\"days\"][ClassName=\"TextBlock\"]", + "name": "days", + "controlType": "Text", + "className": "TextBlock", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Text" + ], + "boundingRect": { + "x": 1260, + "y": 380, + "width": 36, + "height": 24 + }, + "children": [] + }, + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Pane[AutomationId=\"PART_Pane1ScrollViewer\"]/Pane[ClassName=\"ScrollViewer\"]/Group[Name=\"Focus summary view\"][ClassName=\"NamedContainerAutomationPeer\"]/Text[Name=\"Daily goal\"][ClassName=\"TextBlock\"]", + "name": "Daily goal", + "controlType": "Text", + "className": "TextBlock", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Text" + ], + "boundingRect": { + "x": 1097, + "y": 299, + "width": 77, + "height": 24 + }, + "children": [] + }, + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Pane[AutomationId=\"PART_Pane1ScrollViewer\"]/Pane[ClassName=\"ScrollViewer\"]/Group[Name=\"Focus summary view\"][ClassName=\"NamedContainerAutomationPeer\"]/Text[Name=\"1\"][ClassName=\"TextBlock\"]", + "name": "1", + "controlType": "Text", + "className": "TextBlock", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Text" + ], + "boundingRect": { + "x": 1126, + "y": 323, + "width": 18, + "height": 57 + }, + "children": [] + }, + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Pane[AutomationId=\"PART_Pane1ScrollViewer\"]/Pane[ClassName=\"ScrollViewer\"]/Group[Name=\"Focus summary view\"][ClassName=\"NamedContainerAutomationPeer\"]/Text[Name=\"hour\"][ClassName=\"TextBlock\"]", + "name": "hour", + "controlType": "Text", + "className": "TextBlock", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Text" + ], + "boundingRect": { + "x": 1117, + "y": 380, + "width": 37, + "height": 24 + }, + "children": [] + }, + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Pane[AutomationId=\"PART_Pane1ScrollViewer\"]/Pane[ClassName=\"ScrollViewer\"]/Group[Name=\"Focus summary view\"][ClassName=\"NamedContainerAutomationPeer\"]/Text[Name=\"Completed: 0 minutes\"][ClassName=\"TextBlock\"]", + "name": "Completed: 0 minutes", + "controlType": "Text", + "className": "TextBlock", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Text" + ], + "boundingRect": { + "x": 1050, + "y": 449, + "width": 170, + "height": 23 + }, + "children": [] + } + ] + }, + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Pane[AutomationId=\"PART_Pane1ScrollViewer\"]/Pane[ClassName=\"ScrollViewer\"]/Group[Name=\"Ambient music view\"][ClassName=\"NamedContainerAutomationPeer\"]", + "name": "Ambient music view", + "controlType": "Group", + "className": "NamedContainerAutomationPeer", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [], + "boundingRect": { + "x": 945, + "y": 551, + "width": 380, + "height": 260 + }, + "children": [ + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Pane[AutomationId=\"PART_Pane1ScrollViewer\"]/Pane[ClassName=\"ScrollViewer\"]/Group[Name=\"Ambient music view\"][ClassName=\"NamedContainerAutomationPeer\"]/Image[AutomationId=\"SpotifyLogo\"]", + "automationId": "SpotifyLogo", + "controlType": "Image", + "className": "Image", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [], + "boundingRect": { + "x": 965, + "y": 562, + "width": 90, + "height": 27 + }, + "children": [] + }, + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Pane[AutomationId=\"PART_Pane1ScrollViewer\"]/Pane[ClassName=\"ScrollViewer\"]/Group[Name=\"Ambient music view\"][ClassName=\"NamedContainerAutomationPeer\"]/Button[AutomationId=\"ContextMenuButton\"]", + "automationId": "ContextMenuButton", + "name": "See more", + "controlType": "Button", + "className": "Button", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Invoke" + ], + "boundingRect": { + "x": 1280, + "y": 556, + "width": 40, + "height": 40 + }, + "children": [] + }, + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Pane[AutomationId=\"PART_Pane1ScrollViewer\"]/Pane[ClassName=\"ScrollViewer\"]/Group[Name=\"Ambient music view\"][ClassName=\"NamedContainerAutomationPeer\"]/Text[Name=\"Enhance your focus with music and podcasts from Spotify\"][ClassName=\"TextBlock\"]", + "name": "Enhance your focus with music and podcasts from Spotify", + "controlType": "Text", + "className": "TextBlock", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Text" + ], + "boundingRect": { + "x": 999, + "y": 631, + "width": 272, + "height": 47 + }, + "children": [] + }, + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Pane[AutomationId=\"PART_Pane1ScrollViewer\"]/Pane[ClassName=\"ScrollViewer\"]/Group[Name=\"Ambient music view\"][ClassName=\"NamedContainerAutomationPeer\"]/Button[AutomationId=\"SignInButton\"]", + "automationId": "SignInButton", + "name": "Link your Spotify", + "controlType": "Button", + "className": "Button", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Invoke" + ], + "boundingRect": { + "x": 1056, + "y": 703, + "width": 159, + "height": 40 + }, + "children": [] + } + ] + } + ] + } + ] + } + ] + }, + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Image[ClassName=\"Image\"]", + "controlType": "Image", + "className": "Image", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [], + "boundingRect": { + "x": 177, + "y": 131, + "width": 25, + "height": 25 + }, + "children": [] + }, + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Text[AutomationId=\"AppName\"]", + "automationId": "AppName", + "name": "Clock", + "controlType": "Text", + "className": "TextBlock", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Text" + ], + "boundingRect": { + "x": 217, + "y": 134, + "width": 37, + "height": 20 + }, + "children": [] + } + ] + }, + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Pane[ClassName=\"ApplicationFrameInputSinkWindow\"]", "controlType": "Pane", "className": "ApplicationFrameInputSinkWindow", "isEnabled": true, diff --git a/ts/packages/agents/onboarding/test/fixtures/uiCapture/clock-tree-navigated.json b/ts/packages/agents/onboarding/test/fixtures/uiCapture/clock-tree-navigated.json new file mode 100644 index 0000000000..6678a5f7d7 --- /dev/null +++ b/ts/packages/agents/onboarding/test/fixtures/uiCapture/clock-tree-navigated.json @@ -0,0 +1,793 @@ +{ + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]", + "name": "Clock", + "controlType": "Window", + "className": "ApplicationFrameWindow", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Window" + ], + "boundingRect": { + "x": 150, + "y": 120, + "width": 1224, + "height": 1014 + }, + "children": [ + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"PopupHost\"][ClassName=\"Xaml_WindowedPopupClass\"]", + "name": "PopupHost", + "controlType": "Window", + "className": "Xaml_WindowedPopupClass", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [], + "boundingRect": { + "x": 297, + "y": 209, + "width": 75, + "height": 52 + }, + "children": [ + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"PopupHost\"][ClassName=\"Xaml_WindowedPopupClass\"]/Window[Name=\"Popup\"][ClassName=\"Popup\"]", + "name": "Popup", + "controlType": "Window", + "className": "Popup", + "isEnabled": true, + "isOffscreen": true, + "hasKeyboardFocus": false, + "patterns": [], + "boundingRect": { + "x": 0, + "y": 0, + "width": 0, + "height": 0 + }, + "children": [ + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"PopupHost\"][ClassName=\"Xaml_WindowedPopupClass\"]/Window[Name=\"Popup\"][ClassName=\"Popup\"]/ToolTip[Name=\"Alarm\"][ClassName=\"ToolTip\"]", + "name": "Alarm", + "controlType": "ToolTip", + "className": "ToolTip", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [], + "boundingRect": { + "x": 302, + "y": 211, + "width": 64, + "height": 40 + }, + "children": [ + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"PopupHost\"][ClassName=\"Xaml_WindowedPopupClass\"]/Window[Name=\"Popup\"][ClassName=\"Popup\"]/ToolTip[Name=\"Alarm\"][ClassName=\"ToolTip\"]/Text[Name=\"Alarm\"][ClassName=\"TextBlock\"]", + "name": "Alarm", + "controlType": "Text", + "className": "TextBlock", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Text" + ], + "boundingRect": { + "x": 314, + "y": 220, + "width": 40, + "height": 20 + }, + "children": [] + } + ] + } + ] + } + ] + }, + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[AutomationId=\"TitleBar\"]", + "automationId": "TitleBar", + "name": "Clock", + "controlType": "Window", + "className": "ApplicationFrameTitleBarWindow", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Value" + ], + "boundingRect": { + "x": 1132, + "y": 121, + "width": 233, + "height": 40 + }, + "value": "Clock", + "children": [ + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[AutomationId=\"TitleBar\"]/MenuBar[AutomationId=\"SystemMenuBar\"]", + "automationId": "SystemMenuBar", + "name": "System", + "controlType": "MenuBar", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [], + "boundingRect": { + "x": 0, + "y": 0, + "width": 0, + "height": 0 + }, + "children": [ + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[AutomationId=\"TitleBar\"]/MenuBar[AutomationId=\"SystemMenuBar\"]/MenuItem[Name=\"System\"]", + "name": "System", + "controlType": "MenuItem", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "ExpandCollapse" + ], + "boundingRect": { + "x": 0, + "y": 0, + "width": 0, + "height": 0 + }, + "children": [] + } + ] + }, + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[AutomationId=\"TitleBar\"]/Button[AutomationId=\"Minimize\"]", + "automationId": "Minimize", + "name": "Minimize Clock", + "controlType": "Button", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Invoke" + ], + "boundingRect": { + "x": 1194, + "y": 121, + "width": 57, + "height": 40 + }, + "children": [] + }, + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[AutomationId=\"TitleBar\"]/Button[AutomationId=\"Maximize\"]", + "automationId": "Maximize", + "name": "Maximize Clock", + "controlType": "Button", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Invoke" + ], + "boundingRect": { + "x": 1251, + "y": 121, + "width": 57, + "height": 40 + }, + "children": [] + }, + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[AutomationId=\"TitleBar\"]/Button[AutomationId=\"Close\"]", + "automationId": "Close", + "name": "Close Clock", + "controlType": "Button", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Invoke" + ], + "boundingRect": { + "x": 1308, + "y": 121, + "width": 57, + "height": 40 + }, + "children": [] + } + ] + }, + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]", + "name": "Clock", + "controlType": "Window", + "className": "Windows.UI.Core.CoreWindow", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": true, + "patterns": [], + "boundingRect": { + "x": 159, + "y": 121, + "width": 1206, + "height": 1004 + }, + "children": [ + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]", + "automationId": "NavView", + "controlType": "Custom", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Selection" + ], + "boundingRect": { + "x": 159, + "y": 121, + "width": 1206, + "height": 1004 + }, + "children": [ + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Window[AutomationId=\"PaneRoot\"]", + "automationId": "PaneRoot", + "controlType": "Window", + "className": "SplitViewPane", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [], + "boundingRect": { + "x": 159, + "y": 121, + "width": 350, + "height": 1004 + }, + "children": [ + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Window[AutomationId=\"PaneRoot\"]/Pane[AutomationId=\"MenuItemsScrollViewer\"]", + "automationId": "MenuItemsScrollViewer", + "controlType": "Pane", + "className": "ScrollViewer", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Scroll" + ], + "boundingRect": { + "x": 159, + "y": 166, + "width": 350, + "height": 854 + }, + "children": [ + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Window[AutomationId=\"PaneRoot\"]/Pane[AutomationId=\"MenuItemsScrollViewer\"]/Group[AutomationId=\"MenuItemsHost\"]", + "automationId": "MenuItemsHost", + "controlType": "Group", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [], + "boundingRect": { + "x": 164, + "y": 169, + "width": 340, + "height": 245 + }, + "children": [ + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Window[AutomationId=\"PaneRoot\"]/Pane[AutomationId=\"MenuItemsScrollViewer\"]/Group[AutomationId=\"MenuItemsHost\"]/ListItem[AutomationId=\"FocusButton\"]", + "automationId": "FocusButton", + "name": "Focus sessions", + "controlType": "ListItem", + "className": "Microsoft.UI.Xaml.Controls.NavigationViewItem", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "SelectionItem" + ], + "boundingRect": { + "x": 164, + "y": 169, + "width": 340, + "height": 45 + }, + "children": [] + }, + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Window[AutomationId=\"PaneRoot\"]/Pane[AutomationId=\"MenuItemsScrollViewer\"]/Group[AutomationId=\"MenuItemsHost\"]/ListItem[AutomationId=\"TimerButton\"]", + "automationId": "TimerButton", + "name": "Timer", + "controlType": "ListItem", + "className": "Microsoft.UI.Xaml.Controls.NavigationViewItem", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "SelectionItem" + ], + "boundingRect": { + "x": 164, + "y": 219, + "width": 340, + "height": 45 + }, + "children": [] + }, + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Window[AutomationId=\"PaneRoot\"]/Pane[AutomationId=\"MenuItemsScrollViewer\"]/Group[AutomationId=\"MenuItemsHost\"]/ListItem[AutomationId=\"AlarmButton\"]", + "automationId": "AlarmButton", + "name": "Alarm", + "controlType": "ListItem", + "className": "Microsoft.UI.Xaml.Controls.NavigationViewItem", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": true, + "patterns": [ + "SelectionItem" + ], + "boundingRect": { + "x": 164, + "y": 269, + "width": 340, + "height": 45 + }, + "children": [] + }, + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Window[AutomationId=\"PaneRoot\"]/Pane[AutomationId=\"MenuItemsScrollViewer\"]/Group[AutomationId=\"MenuItemsHost\"]/ListItem[AutomationId=\"StopwatchButton\"]", + "automationId": "StopwatchButton", + "name": "Stopwatch", + "controlType": "ListItem", + "className": "Microsoft.UI.Xaml.Controls.NavigationViewItem", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "SelectionItem" + ], + "boundingRect": { + "x": 164, + "y": 319, + "width": 340, + "height": 45 + }, + "children": [] + }, + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Window[AutomationId=\"PaneRoot\"]/Pane[AutomationId=\"MenuItemsScrollViewer\"]/Group[AutomationId=\"MenuItemsHost\"]/ListItem[AutomationId=\"ClockButton\"]", + "automationId": "ClockButton", + "name": "World clock", + "controlType": "ListItem", + "className": "Microsoft.UI.Xaml.Controls.NavigationViewItem", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "SelectionItem" + ], + "boundingRect": { + "x": 164, + "y": 369, + "width": 340, + "height": 45 + }, + "children": [] + } + ] + } + ] + }, + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Window[AutomationId=\"PaneRoot\"]/Button[AutomationId=\"SignInButton\"]", + "automationId": "SignInButton", + "name": "Logged user: Robert Gruen", + "controlType": "Button", + "className": "Button", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Invoke" + ], + "boundingRect": { + "x": 164, + "y": 1020, + "width": 340, + "height": 45 + }, + "children": [ + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Window[AutomationId=\"PaneRoot\"]/Button[AutomationId=\"SignInButton\"]/Text[AutomationId=\"TipBlock\"]", + "automationId": "TipBlock", + "controlType": "Text", + "className": "TextBlock", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Text" + ], + "boundingRect": { + "x": 217, + "y": 1031, + "width": 104, + "height": 24 + }, + "children": [] + }, + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Window[AutomationId=\"PaneRoot\"]/Button[AutomationId=\"SignInButton\"]/Text[AutomationId=\"UserNameBlock\"]", + "automationId": "UserNameBlock", + "name": "Robert Gruen", + "controlType": "Text", + "className": "TextBlock", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Text" + ], + "boundingRect": { + "x": 217, + "y": 1031, + "width": 104, + "height": 24 + }, + "children": [] + } + ] + }, + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Window[AutomationId=\"PaneRoot\"]/Pane[AutomationId=\"FooterItemsScrollViewer\"]", + "automationId": "FooterItemsScrollViewer", + "controlType": "Pane", + "className": "ScrollViewer", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Scroll" + ], + "boundingRect": { + "x": 159, + "y": 1070, + "width": 350, + "height": 50 + }, + "children": [ + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Window[AutomationId=\"PaneRoot\"]/Pane[AutomationId=\"FooterItemsScrollViewer\"]/Group[AutomationId=\"FooterMenuItemsHost\"]", + "automationId": "FooterMenuItemsHost", + "controlType": "Group", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [], + "boundingRect": { + "x": 164, + "y": 1073, + "width": 340, + "height": 45 + }, + "children": [ + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Window[AutomationId=\"PaneRoot\"]/Pane[AutomationId=\"FooterItemsScrollViewer\"]/Group[AutomationId=\"FooterMenuItemsHost\"]/ListItem[AutomationId=\"SettingsItem\"]", + "automationId": "SettingsItem", + "name": "Settings", + "controlType": "ListItem", + "className": "Microsoft.UI.Xaml.Controls.NavigationViewItem", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "SelectionItem" + ], + "boundingRect": { + "x": 164, + "y": 1073, + "width": 340, + "height": 45 + }, + "children": [] + } + ] + } + ] + } + ] + }, + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Pane[AutomationId=\"PART_Pane1ScrollViewer\"]", + "automationId": "PART_Pane1ScrollViewer", + "controlType": "Pane", + "className": "ScrollViewer", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Scroll" + ], + "boundingRect": { + "x": 510, + "y": 161, + "width": 855, + "height": 964 + }, + "children": [ + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Pane[AutomationId=\"PART_Pane1ScrollViewer\"]/Pane[AutomationId=\"AlarmsScrollViewer\"]", + "automationId": "AlarmsScrollViewer", + "controlType": "Pane", + "className": "ScrollViewer", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Scroll" + ], + "boundingRect": { + "x": 510, + "y": 161, + "width": 855, + "height": 964 + }, + "children": [ + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Pane[AutomationId=\"PART_Pane1ScrollViewer\"]/Pane[AutomationId=\"AlarmsScrollViewer\"]/DataItem[AutomationId=\"AlarmViewGrid\"]", + "automationId": "AlarmViewGrid", + "name": "Edit alarm, Good morning, 7‎:‎00‎AM, Every day, ", + "controlType": "DataItem", + "className": "ToggleButton", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Toggle" + ], + "boundingRect": { + "x": 580, + "y": 186, + "width": 715, + "height": 228 + }, + "toggleState": "off", + "children": [ + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Pane[AutomationId=\"PART_Pane1ScrollViewer\"]/Pane[AutomationId=\"AlarmsScrollViewer\"]/DataItem[AutomationId=\"AlarmViewGrid\"]/Text[AutomationId=\"AlarmTime\"]", + "automationId": "AlarmTime", + "name": "7:00", + "controlType": "Text", + "className": "TimeTextBlock", + "isEnabled": false, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Value" + ], + "boundingRect": { + "x": 600, + "y": 206, + "width": 620, + "height": 61 + }, + "children": [ + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Pane[AutomationId=\"PART_Pane1ScrollViewer\"]/Pane[AutomationId=\"AlarmsScrollViewer\"]/DataItem[AutomationId=\"AlarmViewGrid\"]/Text[AutomationId=\"AlarmTime\"]/Text[AutomationId=\"AlarmPeriod\"]", + "automationId": "AlarmPeriod", + "name": "AM", + "controlType": "Text", + "className": "TextBlock", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Text" + ], + "boundingRect": { + "x": 763, + "y": 254, + "width": 30, + "height": 13 + }, + "children": [] + } + ] + }, + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Pane[AutomationId=\"PART_Pane1ScrollViewer\"]/Pane[AutomationId=\"AlarmsScrollViewer\"]/DataItem[AutomationId=\"AlarmViewGrid\"]/Button[AutomationId=\"AlarmToggleSwitch\"]", + "automationId": "AlarmToggleSwitch", + "name": "Alarm On Off", + "controlType": "Button", + "className": "ToggleSwitch", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Toggle" + ], + "boundingRect": { + "x": 1220, + "y": 191, + "width": 65, + "height": 51 + }, + "toggleState": "off", + "children": [] + }, + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Pane[AutomationId=\"PART_Pane1ScrollViewer\"]/Pane[AutomationId=\"AlarmsScrollViewer\"]/DataItem[AutomationId=\"AlarmViewGrid\"]/Text[Name=\"in 8 hours, 41 minutes\"][ClassName=\"TextBlock\"]", + "name": "in 8 hours, 41 minutes", + "controlType": "Text", + "className": "TextBlock", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Text" + ], + "boundingRect": { + "x": 625, + "y": 284, + "width": 148, + "height": 11 + }, + "children": [] + }, + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Pane[AutomationId=\"PART_Pane1ScrollViewer\"]/Pane[AutomationId=\"AlarmsScrollViewer\"]/DataItem[AutomationId=\"AlarmViewGrid\"]/Text[AutomationId=\"AlarmName\"]", + "automationId": "AlarmName", + "name": "Good morning", + "controlType": "Text", + "className": "TextBlock", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Text" + ], + "boundingRect": { + "x": 600, + "y": 307, + "width": 620, + "height": 34 + }, + "children": [] + } + ] + } + ] + }, + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Pane[AutomationId=\"PART_Pane1ScrollViewer\"]/AppBar[ClassName=\"ApplicationBar\"]", + "controlType": "AppBar", + "className": "ApplicationBar", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Toggle", + "ExpandCollapse" + ], + "boundingRect": { + "x": 1227, + "y": 1040, + "width": 113, + "height": 60 + }, + "toggleState": "off", + "children": [ + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Pane[AutomationId=\"PART_Pane1ScrollViewer\"]/AppBar[ClassName=\"ApplicationBar\"]/Button[AutomationId=\"EditAlarmsButton\"]", + "automationId": "EditAlarmsButton", + "name": "Edit alarms", + "controlType": "Button", + "className": "AppBarButton", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Invoke" + ], + "boundingRect": { + "x": 1232, + "y": 1040, + "width": 50, + "height": 60 + }, + "children": [] + }, + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Custom[AutomationId=\"NavView\"]/Pane[AutomationId=\"PART_Pane1ScrollViewer\"]/AppBar[ClassName=\"ApplicationBar\"]/Button[AutomationId=\"AddAlarmButton\"]", + "automationId": "AddAlarmButton", + "name": "Add an alarm", + "controlType": "Button", + "className": "AppBarButton", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Invoke" + ], + "boundingRect": { + "x": 1282, + "y": 1040, + "width": 50, + "height": 60 + }, + "children": [] + } + ] + } + ] + } + ] + }, + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Image[ClassName=\"Image\"]", + "controlType": "Image", + "className": "Image", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [], + "boundingRect": { + "x": 177, + "y": 131, + "width": 25, + "height": 25 + }, + "children": [] + }, + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Window[Name=\"Clock\"][ClassName=\"Windows.UI.Core.CoreWindow\"]/Text[AutomationId=\"AppName\"]", + "automationId": "AppName", + "name": "Clock", + "controlType": "Text", + "className": "TextBlock", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [ + "Text" + ], + "boundingRect": { + "x": 217, + "y": 134, + "width": 37, + "height": 20 + }, + "children": [] + } + ] + }, + { + "selector": "/Window[Name=\"Clock\"][ClassName=\"ApplicationFrameWindow\"]/Pane[ClassName=\"ApplicationFrameInputSinkWindow\"]", + "controlType": "Pane", + "className": "ApplicationFrameInputSinkWindow", + "isEnabled": true, + "isOffscreen": false, + "hasKeyboardFocus": false, + "patterns": [], + "boundingRect": { + "x": 159, + "y": 161, + "width": 1206, + "height": 964 + }, + "children": [] + } + ] +} \ No newline at end of file From b2e96b89e62fc1b762ec9b76ce2849c6f79a22a9 Mon Sep 17 00:00:00 2001 From: Robert Gruen Date: Sun, 3 May 2026 22:29:04 -0700 Subject: [PATCH 04/28] Slice 3a: snapshot capture/restore mechanism MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit C# helper: snapshot.capture / snapshot.restore / snapshot.delete RPCs backed by FolderSnapshotter (recursive copy with exclude globs) and ProcessKiller (graceful close → force kill on identity match). Restore is replace-not-merge — the target directory is wiped before files come back, so files added to state between snapshots disappear on restore. TS: snapshotPolicy.ts library with inferSnapshotPolicy (UWP via PowerShell Get-AppxPackage → PackageFamilyName → LocalState/Settings/ RoamingState folder enumeration), plus load/save/approve/markStateless helpers. HelperClient gains snapshotCapture/Restore/Delete. Smoke: - inferSnapshotPolicy for Clock detects the 3 expected UWP folders (LocalState, Settings, RoamingState) under Microsoft.WindowsAlarms_*. - Synthetic capture/restore round-trips against a sandboxed state directory: dirty all 3 files + add a new one + restore → all originals match expected content + the added file is gone. Slice 3b (onboarding-action wiring: inferSnapshotPolicy / approveSnapshotPolicy / markStateless / editSnapshotPolicy actions on the manifest+grammar) deferred to when we wire the full pipeline. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/Methods/Register.cs | 1 + .../src/Methods/SnapshotMethods.cs | 180 ++++ .../src/Models/SnapshotPolicy.cs | 70 ++ .../src/Snapshot/FolderSnapshotter.cs | 105 +++ .../src/Snapshot/ProcessKiller.cs | 72 ++ .../onboarding/src/uiCapture/helperClient.ts | 28 +- .../src/uiCapture/snapshotPolicy.ts | 149 ++++ .../src/uiCapture/test/snapshotSmoke.ts | 164 ++++ .../agents/onboarding/src/uiCapture/types.ts | 46 + .../fixtures/uiCapture/clock-launched.png | Bin 131716 -> 102717 bytes .../uiCapture/clock-tree-launched.json | 791 +++--------------- .../uiCapture/clock-tree-navigated.json | 6 +- 12 files changed, 917 insertions(+), 695 deletions(-) create mode 100644 dotnet/uiAutomationHelper/src/Methods/SnapshotMethods.cs create mode 100644 dotnet/uiAutomationHelper/src/Models/SnapshotPolicy.cs create mode 100644 dotnet/uiAutomationHelper/src/Snapshot/FolderSnapshotter.cs create mode 100644 dotnet/uiAutomationHelper/src/Snapshot/ProcessKiller.cs create mode 100644 ts/packages/agents/onboarding/src/uiCapture/snapshotPolicy.ts create mode 100644 ts/packages/agents/onboarding/src/uiCapture/test/snapshotSmoke.ts diff --git a/dotnet/uiAutomationHelper/src/Methods/Register.cs b/dotnet/uiAutomationHelper/src/Methods/Register.cs index bcbbf102ea..33ee300e9a 100644 --- a/dotnet/uiAutomationHelper/src/Methods/Register.cs +++ b/dotnet/uiAutomationHelper/src/Methods/Register.cs @@ -16,5 +16,6 @@ public static void All(Dispatch dispatch) ActionMethods.Register(dispatch); FindMethods.Register(dispatch); EventMethods.Register(dispatch); + SnapshotMethods.Register(dispatch); } } diff --git a/dotnet/uiAutomationHelper/src/Methods/SnapshotMethods.cs b/dotnet/uiAutomationHelper/src/Methods/SnapshotMethods.cs new file mode 100644 index 0000000000..649bed3eeb --- /dev/null +++ b/dotnet/uiAutomationHelper/src/Methods/SnapshotMethods.cs @@ -0,0 +1,180 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json; +using System.Text.Json.Serialization; +using UiAutomationHelper.Models; +using UiAutomationHelper.Rpc; +using UiAutomationHelper.Snapshot; + +namespace UiAutomationHelper.Methods; + +internal static class SnapshotMethods +{ + private static readonly JsonSerializerOptions ManifestJsonOpts = new() + { + WriteIndented = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, + }; + + public static void Register(Dispatch dispatch) + { + dispatch.Register("snapshot.capture", (p, ct) => Task.FromResult(Capture(p))); + dispatch.Register("snapshot.restore", (p, ct) => Task.FromResult(Restore(p))); + dispatch.Register("snapshot.delete", (p, ct) => Task.FromResult(Delete(p))); + } + + private static object? Capture(JsonElement? @params) + { + var p = RpcParams.ParseRequired(@params); + if (string.IsNullOrEmpty(p.SnapshotDir) || p.Policy == null) + { + throw new RpcException(RpcErrorCode.InvalidParams, "'snapshotDir' and 'policy' are required"); + } + var policy = p.Policy; + var snapshotDir = Path.GetFullPath(p.SnapshotDir); + Directory.CreateDirectory(snapshotDir); + + if (NeedsKill(policy)) + { + ProcessKiller.KillByIdentity( + policy.ProcessIdentity?.Aumid, + policy.ProcessIdentity?.ProcessName); + } + + var manifest = new SnapshotManifest + { + CapturedAt = DateTime.UtcNow.ToString("o"), + IntegrationName = policy.IntegrationName, + }; + long total = 0; + var sourcesDir = Path.Combine(snapshotDir, "sources"); + Directory.CreateDirectory(sourcesDir); + + for (int i = 0; i < policy.State.Count; i++) + { + var src = policy.State[i]; + var record = new SnapshotSourceRecord { Index = i, Kind = src.Kind }; + switch (src.Kind) + { + case "folder": + var path = ExpandPath(src.Path ?? ""); + record.Source = path; + var slot = Path.Combine(sourcesDir, i.ToString()); + record.StoredAt = Path.GetRelativePath(snapshotDir, slot).Replace('\\', '/'); + record.Bytes = FolderSnapshotter.Capture(path, slot, src.Exclude); + break; + case "registry": + case "appCommand": + throw new RpcException(RpcErrorCode.InvalidParams, + $"Source kind '{src.Kind}' not yet supported (slice 3a is folder-only)"); + default: + throw new RpcException(RpcErrorCode.InvalidParams, + $"Unknown source kind: {src.Kind}"); + } + manifest.Sources.Add(record); + total += record.Bytes; + } + manifest.TotalBytes = total; + File.WriteAllText( + Path.Combine(snapshotDir, "manifest.json"), + JsonSerializer.Serialize(manifest, ManifestJsonOpts)); + + return new + { + snapshotId = Path.GetFileName(snapshotDir), + bytes = total, + sourceCount = manifest.Sources.Count, + }; + } + + private static object? Restore(JsonElement? @params) + { + var p = RpcParams.ParseRequired(@params); + if (string.IsNullOrEmpty(p.SnapshotDir) || p.Policy == null) + { + throw new RpcException(RpcErrorCode.InvalidParams, "'snapshotDir' and 'policy' are required"); + } + var snapshotDir = Path.GetFullPath(p.SnapshotDir); + if (!Directory.Exists(snapshotDir)) + { + throw new RpcException(RpcErrorCode.SnapshotMissing, $"Snapshot not found: {snapshotDir}"); + } + var policy = p.Policy; + + if (NeedsKill(policy)) + { + ProcessKiller.KillByIdentity( + policy.ProcessIdentity?.Aumid, + policy.ProcessIdentity?.ProcessName); + } + + var sourcesDir = Path.Combine(snapshotDir, "sources"); + long total = 0; + for (int i = 0; i < policy.State.Count; i++) + { + var src = policy.State[i]; + switch (src.Kind) + { + case "folder": + var target = ExpandPath(src.Path ?? ""); + var slot = Path.Combine(sourcesDir, i.ToString()); + total += FolderSnapshotter.Restore(slot, target); + break; + case "registry": + case "appCommand": + throw new RpcException(RpcErrorCode.InvalidParams, + $"Source kind '{src.Kind}' not yet supported (slice 3a is folder-only)"); + } + } + return new { ok = true, bytes = total }; + } + + private static object? Delete(JsonElement? @params) + { + var p = RpcParams.ParseRequired(@params); + if (string.IsNullOrEmpty(p.SnapshotDir)) + { + throw new RpcException(RpcErrorCode.InvalidParams, "'snapshotDir' is required"); + } + var dir = Path.GetFullPath(p.SnapshotDir); + if (Directory.Exists(dir)) + { + Directory.Delete(dir, recursive: true); + } + return new { ok = true }; + } + + private static bool NeedsKill(SnapshotPolicy policy) + { + foreach (var s in policy.State) + { + // Default: kill for folder + appCommand, not for registry. + var defaultKill = s.Kind switch + { + "registry" => false, + _ => true, + }; + if (s.RequireKill ?? defaultKill) + { + return true; + } + } + return false; + } + + private static string ExpandPath(string p) => + Environment.ExpandEnvironmentVariables(p); +} + +internal sealed class SnapshotCaptureParams +{ + [JsonPropertyName("snapshotDir")] public string? SnapshotDir { get; set; } + [JsonPropertyName("policy")] public SnapshotPolicy? Policy { get; set; } +} + +internal sealed class SnapshotDeleteParams +{ + [JsonPropertyName("snapshotDir")] public string? SnapshotDir { get; set; } +} diff --git a/dotnet/uiAutomationHelper/src/Models/SnapshotPolicy.cs b/dotnet/uiAutomationHelper/src/Models/SnapshotPolicy.cs new file mode 100644 index 0000000000..506f6e5008 --- /dev/null +++ b/dotnet/uiAutomationHelper/src/Models/SnapshotPolicy.cs @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; + +namespace UiAutomationHelper.Models; + +internal sealed class SnapshotPolicy +{ + [JsonPropertyName("version")] public int Version { get; set; } = 1; + [JsonPropertyName("integrationName")] public string IntegrationName { get; set; } = ""; + [JsonPropertyName("detectionStatus")] public string? DetectionStatus { get; set; } + [JsonPropertyName("processIdentity")] public ProcessIdentity? ProcessIdentity { get; set; } + [JsonPropertyName("state")] public List State { get; set; } = new(); + [JsonPropertyName("hooks")] public PolicyHooks? Hooks { get; set; } +} + +internal sealed class ProcessIdentity +{ + [JsonPropertyName("aumid")] public string? Aumid { get; set; } + [JsonPropertyName("processName")] public string? ProcessName { get; set; } + [JsonPropertyName("exePath")] public string? ExePath { get; set; } +} + +internal sealed class SnapshotSource +{ + [JsonPropertyName("kind")] public string Kind { get; set; } = ""; + // folder + [JsonPropertyName("path")] public string? Path { get; set; } + [JsonPropertyName("recursive")] public bool? Recursive { get; set; } + [JsonPropertyName("exclude")] public string[]? Exclude { get; set; } + // registry + [JsonPropertyName("key")] public string? Key { get; set; } + // appCommand + [JsonPropertyName("capture")] public ScriptHook? Capture { get; set; } + [JsonPropertyName("restore")] public ScriptHook? Restore { get; set; } + // shared + [JsonPropertyName("requireKill")] public bool? RequireKill { get; set; } +} + +internal sealed class ScriptHook +{ + [JsonPropertyName("command")] public string Command { get; set; } = ""; + [JsonPropertyName("args")] public string[]? Args { get; set; } + [JsonPropertyName("cwd")] public string? Cwd { get; set; } +} + +internal sealed class PolicyHooks +{ + [JsonPropertyName("beforeCapture")] public ScriptHook[]? BeforeCapture { get; set; } + [JsonPropertyName("afterRestore")] public ScriptHook[]? AfterRestore { get; set; } +} + +internal sealed class SnapshotManifest +{ + [JsonPropertyName("version")] public int Version { get; set; } = 1; + [JsonPropertyName("capturedAt")] public string CapturedAt { get; set; } = ""; + [JsonPropertyName("integrationName")] public string IntegrationName { get; set; } = ""; + [JsonPropertyName("sources")] public List Sources { get; set; } = new(); + [JsonPropertyName("totalBytes")] public long TotalBytes { get; set; } +} + +internal sealed class SnapshotSourceRecord +{ + [JsonPropertyName("index")] public int Index { get; set; } + [JsonPropertyName("kind")] public string Kind { get; set; } = ""; + [JsonPropertyName("source")] public string Source { get; set; } = ""; // resolved path/key + [JsonPropertyName("storedAt")] public string StoredAt { get; set; } = ""; // relative to snapshot dir + [JsonPropertyName("bytes")] public long Bytes { get; set; } +} diff --git a/dotnet/uiAutomationHelper/src/Snapshot/FolderSnapshotter.cs b/dotnet/uiAutomationHelper/src/Snapshot/FolderSnapshotter.cs new file mode 100644 index 0000000000..798a96e1db --- /dev/null +++ b/dotnet/uiAutomationHelper/src/Snapshot/FolderSnapshotter.cs @@ -0,0 +1,105 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace UiAutomationHelper.Snapshot; + +internal static class FolderSnapshotter +{ + /// + /// Recursively copy into . + /// Returns total bytes copied. matches relative path glob fragments. + /// + public static long Capture(string sourcePath, string destPath, IReadOnlyList? exclude = null) + { + if (!Directory.Exists(sourcePath)) + { + return 0; + } + Directory.CreateDirectory(destPath); + return CopyRecursive(sourcePath, destPath, sourcePath, exclude); + } + + /// + /// Replace 's contents with what's in + /// . Removes anything currently at target, + /// then copies snapshot back. Returns bytes restored. + /// + public static long Restore(string snapshotPath, string targetPath) + { + if (!Directory.Exists(snapshotPath)) + { + // Nothing was captured — make target empty (matches "nothing was there at capture time"). + if (Directory.Exists(targetPath)) + { + Directory.Delete(targetPath, recursive: true); + } + return 0; + } + + if (Directory.Exists(targetPath)) + { + Directory.Delete(targetPath, recursive: true); + } + Directory.CreateDirectory(targetPath); + return CopyRecursive(snapshotPath, targetPath, snapshotPath, exclude: null); + } + + private static long CopyRecursive( + string srcRoot, + string dstRoot, + string currentSrc, + IReadOnlyList? exclude) + { + long bytes = 0; + foreach (var file in Directory.EnumerateFiles(currentSrc)) + { + var rel = Path.GetRelativePath(srcRoot, file); + if (IsExcluded(rel, exclude)) + { + continue; + } + var dst = Path.Combine(dstRoot, rel); + Directory.CreateDirectory(Path.GetDirectoryName(dst)!); + try + { + File.Copy(file, dst, overwrite: true); + bytes += new FileInfo(dst).Length; + } + catch (IOException) + { + // File locked or missing — best-effort. + } + catch (UnauthorizedAccessException) + { + // Permission issue — skip. + } + } + foreach (var dir in Directory.EnumerateDirectories(currentSrc)) + { + var rel = Path.GetRelativePath(srcRoot, dir); + if (IsExcluded(rel, exclude)) + { + continue; + } + bytes += CopyRecursive(srcRoot, dstRoot, dir, exclude); + } + return bytes; + } + + private static bool IsExcluded(string relativePath, IReadOnlyList? exclude) + { + if (exclude == null || exclude.Count == 0) + { + return false; + } + var norm = relativePath.Replace('\\', '/'); + foreach (var pattern in exclude) + { + if (norm.Contains(pattern, StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + return false; + } +} diff --git a/dotnet/uiAutomationHelper/src/Snapshot/ProcessKiller.cs b/dotnet/uiAutomationHelper/src/Snapshot/ProcessKiller.cs new file mode 100644 index 0000000000..5fad696de5 --- /dev/null +++ b/dotnet/uiAutomationHelper/src/Snapshot/ProcessKiller.cs @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Diagnostics; + +namespace UiAutomationHelper.Snapshot; + +internal static class ProcessKiller +{ + /// + /// Kill all processes matching the given identity. UWP AUMID matching is + /// approximated via process-name match against the package name fragment; + /// callers passing aumid should also pass processName when known. + /// + public static void KillByIdentity(string? aumid, string? processName, int gracefulMs = 1500) + { + var processes = new List(); + if (!string.IsNullOrEmpty(processName)) + { + try + { + processes.AddRange(Process.GetProcessesByName(StripExe(processName))); + } + catch + { + /* Access denied is fine */ + } + } + if (!string.IsNullOrEmpty(aumid)) + { + // AUMID is "_!". Use the package + // name as a fuzzy process-name hint (works for built-in apps where + // the executable name reflects the package). + var pkg = aumid.Split('_', '!')[0]; + try + { + processes.AddRange(Process.GetProcessesByName(pkg)); + } + catch + { + /* Access denied is fine */ + } + } + + foreach (var proc in processes.Distinct()) + { + try + { + if (proc.HasExited) continue; + try { proc.CloseMainWindow(); } catch { /* ignore */ } + if (!proc.WaitForExit(gracefulMs)) + { + proc.Kill(entireProcessTree: true); + proc.WaitForExit(gracefulMs); + } + } + catch + { + /* ignore individual failures */ + } + finally + { + proc.Dispose(); + } + } + } + + private static string StripExe(string name) => + name.EndsWith(".exe", StringComparison.OrdinalIgnoreCase) + ? name[..^4] + : name; +} diff --git a/ts/packages/agents/onboarding/src/uiCapture/helperClient.ts b/ts/packages/agents/onboarding/src/uiCapture/helperClient.ts index 04e35fa1a6..020d3ca3db 100644 --- a/ts/packages/agents/onboarding/src/uiCapture/helperClient.ts +++ b/ts/packages/agents/onboarding/src/uiCapture/helperClient.ts @@ -7,7 +7,13 @@ import path from "node:path"; import { createInterface, Interface } from "node:readline"; import { fileURLToPath } from "node:url"; -import type { Rect, Screenshot, TreeNode, WindowInfo } from "./types.js"; +import type { + Rect, + Screenshot, + SnapshotPolicy, + TreeNode, + WindowInfo, +} from "./types.js"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -276,6 +282,24 @@ export class HelperClient { return this.call("events.idle", p); } + snapshotCapture(p: { + snapshotDir: string; + policy: SnapshotPolicy; + }): Promise<{ snapshotId: string; bytes: number; sourceCount: number }> { + return this.call("snapshot.capture", p); + } + + snapshotRestore(p: { + snapshotDir: string; + policy: SnapshotPolicy; + }): Promise<{ ok: true; bytes: number }> { + return this.call("snapshot.restore", p); + } + + snapshotDelete(p: { snapshotDir: string }): Promise<{ ok: true }> { + return this.call("snapshot.delete", p); + } + /** * Close the helper's stdin and wait up to `timeoutMs` for graceful exit. * If it doesn't exit, send SIGKILL. @@ -296,4 +320,4 @@ export class HelperClient { } } -export type { Rect, Screenshot, TreeNode, WindowInfo }; +export type { Rect, Screenshot, SnapshotPolicy, TreeNode, WindowInfo }; diff --git a/ts/packages/agents/onboarding/src/uiCapture/snapshotPolicy.ts b/ts/packages/agents/onboarding/src/uiCapture/snapshotPolicy.ts new file mode 100644 index 0000000000..f3a2857e67 --- /dev/null +++ b/ts/packages/agents/onboarding/src/uiCapture/snapshotPolicy.ts @@ -0,0 +1,149 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { execFile } from "node:child_process"; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import path from "node:path"; +import { promisify } from "node:util"; + +import type { + DetectionStatus, + ProcessIdentity, + SnapshotPolicy, + SnapshotSource, +} from "./types.js"; + +const execFileAsync = promisify(execFile); + +/** + * Resolve PackageFamilyName for a UWP package name (the part of the AUMID + * before the underscore). Returns null if the package isn't installed. + */ +async function getPackageFamilyName(packageName: string): Promise { + try { + const { stdout } = await execFileAsync( + "powershell.exe", + [ + "-NoProfile", + "-NonInteractive", + "-Command", + `(Get-AppxPackage -Name '${packageName}' | Select-Object -First 1).PackageFamilyName`, + ], + { timeout: 10_000 }, + ); + const pfn = stdout.trim(); + return pfn.length > 0 ? pfn : null; + } catch { + return null; + } +} + +/** + * Auto-detect a snapshot policy for a UWP app by AUMID. + * AUMID format: `_!` — + * e.g., `Microsoft.WindowsAlarms_8wekyb3d8bbwe!App`. + */ +export async function inferSnapshotPolicy(opts: { + integrationName: string; + aumid?: string; + exePath?: string; +}): Promise { + const policy: SnapshotPolicy = { + version: 1, + integrationName: opts.integrationName, + detectionStatus: "auto-candidate", + processIdentity: { + ...(opts.aumid !== undefined ? { aumid: opts.aumid } : {}), + ...(opts.exePath !== undefined ? { exePath: opts.exePath } : {}), + }, + state: [], + }; + + if (opts.aumid) { + const packageName = opts.aumid.split("_")[0]!; + const pfn = await getPackageFamilyName(packageName); + if (pfn) { + const localAppData = process.env.LOCALAPPDATA ?? ""; + const baseDir = path.join(localAppData, "Packages", pfn); + for (const sub of ["LocalState", "Settings", "RoamingState"]) { + const candidate = path.join(baseDir, sub); + if (existsSync(candidate)) { + const folderSource: SnapshotSource = { + kind: "folder", + path: candidate, + recursive: true, + }; + policy.state.push(folderSource); + } + } + const processName = inferProcessName(packageName); + if (processName !== undefined) { + policy.processIdentity.processName = processName; + } + } + } else if (opts.exePath) { + // Win32 fallback. We don't currently auto-discover state directories + // for Win32 apps; the user is expected to fill in the policy. + policy.processIdentity.processName = path.basename(opts.exePath); + } + + if (policy.state.length === 0) { + policy.detectionStatus = "auto-candidate"; + } + return policy; +} + +/** + * Best-effort process-name guess for known packages. The package name doesn't + * always match the executable name; this table is small for now and grows as + * we onboard real apps. + */ +function inferProcessName(packageName: string): string | undefined { + const map: Record = { + "Microsoft.WindowsAlarms": "Time.exe", + "Microsoft.WindowsCalculator": "CalculatorApp.exe", + }; + return map[packageName]; +} + +export function loadSnapshotPolicy(workspaceDir: string): SnapshotPolicy | null { + const file = path.join(workspaceDir, "snapshotPolicy.json"); + if (!existsSync(file)) { + return null; + } + return JSON.parse(readFileSync(file, "utf8")) as SnapshotPolicy; +} + +export function saveSnapshotPolicy( + workspaceDir: string, + policy: SnapshotPolicy, +): void { + mkdirSync(workspaceDir, { recursive: true }); + writeFileSync( + path.join(workspaceDir, "snapshotPolicy.json"), + JSON.stringify(policy, null, 2), + ); +} + +/** + * Mark a policy as confirmed by user review. + */ +export function approveSnapshotPolicy(policy: SnapshotPolicy): SnapshotPolicy { + return { ...policy, detectionStatus: "user-confirmed" as DetectionStatus }; +} + +/** + * Build an empty policy declaring an integration has no persisted state. + */ +export function makeStatelessPolicy( + integrationName: string, + processIdentity: ProcessIdentity = {}, +): SnapshotPolicy { + return { + version: 1, + integrationName, + detectionStatus: "no-state", + processIdentity, + state: [], + }; +} diff --git a/ts/packages/agents/onboarding/src/uiCapture/test/snapshotSmoke.ts b/ts/packages/agents/onboarding/src/uiCapture/test/snapshotSmoke.ts new file mode 100644 index 0000000000..bda2f79845 --- /dev/null +++ b/ts/packages/agents/onboarding/src/uiCapture/test/snapshotSmoke.ts @@ -0,0 +1,164 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { + existsSync, + mkdirSync, + readFileSync, + rmSync, + writeFileSync, +} from "node:fs"; +import { homedir } from "node:os"; +import path from "node:path"; + +import { HelperClient } from "../helperClient.js"; +import { inferSnapshotPolicy } from "../snapshotPolicy.js"; +import type { SnapshotPolicy } from "../types.js"; + +const CLOCK_AUMID = "Microsoft.WindowsAlarms_8wekyb3d8bbwe!App"; + +function log(msg: string): void { + process.stdout.write(`[snap] ${msg}\n`); +} + +function assertEqual(actual: T, expected: T, label: string): void { + if (actual !== expected) { + throw new Error( + `[FAIL ${label}] expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`, + ); + } + log(` ✓ ${label}`); +} + +async function testInferForClock(): Promise { + log("inferSnapshotPolicy for Clock..."); + const policy = await inferSnapshotPolicy({ + integrationName: "windowsClock", + aumid: CLOCK_AUMID, + }); + log( + ` detection=${policy.detectionStatus} sources=${policy.state.length} aumid=${policy.processIdentity.aumid}`, + ); + for (const s of policy.state) { + if (s.kind === "folder") { + log(` folder: ${s.path}`); + } + } + if (policy.state.length === 0) { + throw new Error( + "Expected at least one detected folder source for Clock — is the package installed?", + ); + } + const hasLocalState = policy.state.some( + (s) => + s.kind === "folder" && + s.path.toLowerCase().endsWith(path.sep + "localstate"), + ); + if (!hasLocalState) { + throw new Error("Expected LocalState folder in detected sources"); + } + log(" ✓ Clock policy inferred with LocalState"); +} + +async function testCaptureRestoreSynthetic(client: HelperClient): Promise { + log("synthetic snapshot capture/restore..."); + // Create a sandboxed state directory we control. + const root = path.join( + homedir(), + ".typeagent", + "onboarding", + "_uic_snapshot_smoke", + ); + const stateDir = path.join(root, "state"); + const snapshotDir = path.join(root, "snapshot"); + + // Clean slate. + rmSync(root, { recursive: true, force: true }); + mkdirSync(stateDir, { recursive: true }); + + // Seed state. + writeFileSync(path.join(stateDir, "alarms.txt"), "7:00 Wake\n8:00 Standup\n"); + writeFileSync(path.join(stateDir, "settings.json"), '{"sound":"chime"}'); + mkdirSync(path.join(stateDir, "nested"), { recursive: true }); + writeFileSync(path.join(stateDir, "nested", "log.txt"), "initial log\n"); + + const policy: SnapshotPolicy = { + version: 1, + integrationName: "_uic_smoke", + detectionStatus: "user-provided", + processIdentity: {}, + state: [ + { + kind: "folder", + path: stateDir, + recursive: true, + requireKill: false, // synthetic — no app to kill + }, + ], + }; + + log(" capturing..."); + const cap = await client.snapshotCapture({ snapshotDir, policy }); + log(` captured ${cap.bytes} bytes across ${cap.sourceCount} source(s)`); + if (cap.bytes <= 0) { + throw new Error("Expected non-zero capture bytes"); + } + + // Dirty the state. + writeFileSync(path.join(stateDir, "alarms.txt"), "(corrupted)\n"); + rmSync(path.join(stateDir, "settings.json")); + writeFileSync(path.join(stateDir, "extra.txt"), "should be removed on restore"); + writeFileSync(path.join(stateDir, "nested", "log.txt"), "(corrupted)\n"); + log(" state dirtied"); + + log(" restoring..."); + const res = await client.snapshotRestore({ snapshotDir, policy }); + log(` restored ${res.bytes} bytes`); + + assertEqual( + readFileSync(path.join(stateDir, "alarms.txt"), "utf8"), + "7:00 Wake\n8:00 Standup\n", + "alarms.txt restored", + ); + assertEqual( + readFileSync(path.join(stateDir, "settings.json"), "utf8"), + '{"sound":"chime"}', + "settings.json restored", + ); + assertEqual( + readFileSync(path.join(stateDir, "nested", "log.txt"), "utf8"), + "initial log\n", + "nested log.txt restored", + ); + if (existsSync(path.join(stateDir, "extra.txt"))) { + throw new Error( + "extra.txt should have been removed by restore (replace, not merge)", + ); + } + log(" ✓ extra.txt removed (replace semantics)"); + + // Cleanup snapshot dir. + await client.snapshotDelete({ snapshotDir }); + log(" ✓ snapshot deleted"); + + rmSync(root, { recursive: true, force: true }); +} + +async function main(): Promise { + const client = await HelperClient.start({ debug: true }); + try { + await testInferForClock(); + await testCaptureRestoreSynthetic(client); + log("DONE"); + } finally { + await client.dispose(); + } +} + +main().catch((e) => { + process.stderr.write(`FAILED: ${e}\n`); + if (e instanceof Error && e.stack) { + process.stderr.write(e.stack + "\n"); + } + process.exit(1); +}); diff --git a/ts/packages/agents/onboarding/src/uiCapture/types.ts b/ts/packages/agents/onboarding/src/uiCapture/types.ts index 2ee23aee2f..930cecdbad 100644 --- a/ts/packages/agents/onboarding/src/uiCapture/types.ts +++ b/ts/packages/agents/onboarding/src/uiCapture/types.ts @@ -68,3 +68,49 @@ export type Screenshot = { pngBase64: string; rect: Rect; }; + +export type ScriptHook = { + command: string; + args?: string[]; + cwd?: string; +}; + +export type SnapshotSource = + | { + kind: "folder"; + path: string; + recursive?: boolean; + exclude?: string[]; + requireKill?: boolean; + } + | { kind: "registry"; key: string; requireKill?: boolean } + | { + kind: "appCommand"; + capture?: ScriptHook; + restore: ScriptHook; + requireKill?: boolean; + }; + +export type ProcessIdentity = { + aumid?: string; + processName?: string; + exePath?: string; +}; + +export type DetectionStatus = + | "auto-candidate" + | "user-confirmed" + | "user-provided" + | "no-state"; + +export type SnapshotPolicy = { + version: 1; + integrationName: string; + detectionStatus: DetectionStatus; + processIdentity: ProcessIdentity; + state: SnapshotSource[]; + hooks?: { + beforeCapture?: ScriptHook[]; + afterRestore?: ScriptHook[]; + }; +}; diff --git a/ts/packages/agents/onboarding/test/fixtures/uiCapture/clock-launched.png b/ts/packages/agents/onboarding/test/fixtures/uiCapture/clock-launched.png index 0ec534f49cc413ee66fdf58122007e91b5951c69..3456396b65d58701b6bb708a16ae7622404055f9 100644 GIT binary patch literal 102717 zcmafbcRbd88+Nyu?G|;BjH1kpA|hlK%HDe=dzF!u9oef$2_ZYm79l%SvO;E*aanQM z-t%`+_x(KY^M2kxp8M|Z@$vi4^E}SuIL>dV@=ZB1Vmjg@M~;wPzb37EsA$vl;a`8bsLDwmDeR*E3IBm-dG*HCBS(s(NOnx{;lGI-uj#nJkKRH4_t&>u zjP6H{>}Ot=zN+qSIKTU_pIgUZbbF?5W7j!0?jwfNJLqlo$uzf1-h~ucI+`;S!NaA44=vB z^$Qra?wQr68ZLc1UAa1?wKn1X1KUX&0{!|*F0T2QoZPkky{|^wHY1Pg_WJj$T)Kp3 zQV#x+7x0wRpP;p8zj=Q=W$40*a}ATK^;AM{kM*Fx>-mCnqzE^g=0q}5sB?BSlyr>RpDv*&MrmYLBX z2qY<=8fG<6@5HxH=yI!CJ^xH1vS-Z4PH3^_Vfrf6k6oG>UN_kYt^fDO^*;>o z<@kL#Gk7$Ng|NBIfigHtps_oVAfR_x$XX|3BSA9Mh+QbK$?;cvQ?U&(tF4Y|x)wRP z{&ArPR`3Ip5~ZS%8VczHZ#1b3r{6nu-~^v2XP^!lW|vo|3pUALxE8GP0XNB|3okeo zC3>Ew>ov8$7AeDtfR?84Q^R1nhjx&HfNW<>*gazzIq$^syvY~yHK!|ko)(E zfdmC2-M;uA2?3LdG~}ia0yN1`FB9E9S<&ZD%cfZsu<3S&PA1}qpSQolMp~H>U-6=m zE!c7Yy9sJlVmgI0c>EkeltR!d7k)ntd=*)6dWN|_UnXtN7I@bkO_0%>f_!-?K_S%G zOy~hKxnZS{d#0+SCbvAy+^K_Du6PqSY3y3*i$;kuFSd-2FyVsfX1_dY*%Rer)WO5d ztml97hFL8={%(Bu;pMac{o#~o%ebHghSqRpCSl$(?`~r+UECc~D_-5sh>S#5Vna2y zzimn7ika!(H9Ky|r%C50$Bzl%w{VltHzg98`%Cqz3Pp3DXk)urK!cx`>G&g=*zBR+ zIbta^HS75%6H2Ly{T0z@;h7J{HUG8AqzoZ0F9`VQ-R|SG`4umC5={2cjai~Ximln{ z%kq`yljTE=rG)&+#*YXFPihgfn)beUUr0q?Dep)vMPFh)u3SnTY1VgWp3;1Z%TUOt!-*61XVsgTE>)mJ$!Aa`=I3#P zs6u$f3jXp5c0%PQHkfn}2NmYT9~BHd^EkK>4wbQR{Bw%8MGYy^m;ac1l?b-kUPq53*6D?=E~gXM^RYLw)Om>w^_(YU zkC*#Uhps14#WkBy!U6iw^6$^}5bIl_?=8dJ&_C#X8*z2Gsc){IAjmGQ@4i5KrZ-l| zRwwPFKoAw|B&8I6b=Jg>1gTIR0rqiD8o~m)#0Tc|FLm$7SX&{pGld`b%512ZDD#v2 zRI6(GS%}mq$MM;w$xPVuzx>a+c2zDk-aZ!4lG)&)>o@nofCsrhh!Oz$0W+?V!~l)Y=Q+eLAjZZ8|{8h zO?J3V>E17-UpS#^AtbH-k8ON|(=TlC);*sfs($iHG!3x{iy%7IINqA5@GgBY{F*N% z()FB=sNSg}X9f0d4DPE=DH}|*h0BJ%d0ixWvmjAJ`K2yZND)0sYj{hA>#Ll+G+hY% zMArrJdHugHN-Vwa5-#^f*}-8^7gFMMe5%&}u;t7!W7L|2=8?wMkAjzo8`a^jt#~!L zgBlZA!wiMvHxm2<9tQ_#orkL$AFUBU_14+(W6666H?(5;brH(4^;ULls@$Ql2&7#0 zC)wy0j$=!4&Wk@Vskk4J8BPE-rqU-b8ged!J+xoDC>)>DX zqaxJOj7HYuVU9~ZR zwlUgps|sQCHo#<+d&^;(%YEc9RhMetZf8C6adDd~?RR^$SFj%!>D1NhcX+vAS9Gcj zQ_fk44T=dwFL<+L9CdzvXZu;BP%oTJv2ab7iI)};6L6P(GRG*7H-6$gAy-5%{~`Bk zC>(i5oiV0hh*epEY^IWkl` zxjQoSZFgiu@7vlpbm!tmA^OJX-qVpMwY$|-m)=fZ897-8e}ajZR4*qN&D!5XyHwjm zF=lwAxVZ9ueLv;|w?K33vO@pE)fXSm4Z-Pqca;B4Cu=?LpGiJPCdt8iKXVKf3?ZUv zoHK#ywY9Q;KQ(tq(Jh5w&=FRGB4C=q%6RiE^EW!|29k;QdKv#Ks8Ij9ysW zL!)>1mqMz2w-HUlZl$R4_A*WSk zR$`9?PhMM(IT3k6?X;>`r0H$8*iM=jyu&+SDN{)AIe*t-*g$x_M|`!ry2$H0=ZMj6 z?|u=?WTyk|ySunQ;=Rdf^mA{%yKZ>*=l;%Xw8H+RQnk@mg3{g5b&7BOZ_(U~v$gAs zdvDR7>vqd;-<(MyBmK{dm9u)CsUy^Yp_NL7$%zk@t{XC^6oA~RQZALHWJx6Lh7Y#z z)i1*izR71Vw&5VxOH3riPK@_vseeOHvhoXEx|Skpl-jVM%C%f`BOD#B*iPDiX7YW{ zp`y-nuKHX5?v(hhWi{LSBj3$OBl~-!zMo(M`+3{1d3-CkQ+?MhM^blw?$6h5oZHXX z9vR&aK#T9K6x~VPC>{M_xS!~DH%fGK9XW^Z><>_}iNeb;AT3J>dk)UMR+X<=rJ_T(|*aJs7Zm$Ua@Z+}kR zvBHejZJZl%*hgDd7wv92%p2~0%ov&3|50~GeDlJ7jptOG!(#3Fw>r61-OW$g&t~^F zNQ?${<>LAu)owJQv&C1&N6O0U_SUNTL~rG$?*EXh?)F}PeGxqz`+Mq%+J!VT@=q^Z zimoK3mZxvnctfy3+idbumlzHpb#<2V=>bY|v$fN-Ek)|`UrMq93v-aE3&GSId3(vj z)e2zhXOXFobDkiev78&YP&jK}fzDzn;bAZeGgZakKHj<&qiE>T%;)zzmTZW))!oK0 z?fGsx?2VvZs=9Z_XWYuw_AorV*D<4e+b62Kce)l`F|`{ftF{ey$VQ6x z;NGQf@0_R_-Fqwk!*DgY{BDuYd>cCKLih7Z1@GBO#>Lu|?>ugG8^uuzrjMhA)~_u3 zVC<_2`O7JOPq|OK;F!4EYz^eu4DAUVQY9O|R5O%pF00d*5?LhW)dxFL+weu>%MX+Q zTw+J&%PVNaGRf8(dpuRDPj1`QnvEQD@YceFr0zGMh0Rj8dW{y*TeBxz-D<~zM%;WCF>ZCco~a8y zYyILQ-V6P8<|kUk_S%dVeD*2CM|Zz5?qs9geD+;NJom>bVAeZBRYg@h&dJq*?>R!v zDC%~e+}0cO`>=p}ct0)kmRz!Zd??H>{1=+Innw_3m=h0>FatI5QGkZ9C?~$cLI#0M zwva!v#$Q?Z1*qHTdTaYKHf0Mu$L6{m-Yg*z1*5Bdawcu9t~f~$mL=)qI9uLW#`XsP z-dFdX8xD&MzC9wY`Ep>O; zch(1k5%1saTio@*B#my`V(NW&lwAGht0uL2hFMu6ufJ+o7UPa?>$TX` z`GiL{dYjYZ>d3!vDHQzw&6Y2ZfY=B-+W?rYhCe`DWM8F=^yw?O5DZtI?-*|#N?0P# z2EhnGGAX-#HQRaG=>Bv+MKlQ@$y+ zPgkP$hxUiO$D{UbFpNH%T2(`f_k7&CJZ9YX#~+D*+C?+&m|%udcLUHD>b3{bJ+RXw zMZVK+RoQ!=qVA5a3_Piti!_pPQKD(+*M8g}an?!EaGchHs(DKAMyQFqqHpSw*sjFc zZZQR&0GCpeR|j=Krgf(tF&yQ^ceqK#l(I-_3C@AU+JfBQmV7!kuS``OZQ@AN2U7j& z^wrMnS38oaYO==OSp+p|?@IKw_~qimEl8Iry%=f4&YNDbkp9YC!tfzsi3E{+&x@vF z*3+~H*+kWH{L`hHQqL(ehY|eZ{gu%@PmJ9DTHW3K&8R&EOls{?OZ80n?sWIQB4*ja ztLrLidl!9)qG6zDZ`#2vbtj_x&ggak?taK0Tc2Xh$b18z?ay^WjKvOq$(!qUN0AV{|$q+)NLqyztYIKkrA>KbRl z!}aU-x9vTHBPUnBVQPHKc^Jge6SzVQY0{ZqKb;h#%r)nX5Aa-DN|4;>QX)F$+-TqK zkkL-20ZSb3+3qvj0sFsFt4Ftyb39|TZ;MHKbftQ`b9D19`jXF_TeT7V87;PUGWDm= z_kOXF-IZ*g4A0J!-@fj86PdTn;|}(}9ou|!V~A&mi-)#xm*rm2kbv3t(LuRdRyF&) zUqaKme)R`6mtxSWZnqI}e`dXzv=tMlu&WwdFg@Ey?F?klBmIvlGJY4>*s|U~Vtgqf ztnqijheT{${%E2|^^QqESj0DNq+-@5~TRYKzCV)rOb}IS3zsdh@P^Gey1%m>*AS-+Jz2_LtCMd_-p1&n~Qo zR6FGCe5l*W9&xDsH8@h_JzZX{hqnoYHq znCZ#Z1`Q*#m|R(=xV(>n!^xpTPxWvPcmD!Sm(3Mlc7$A*L5llm+{_^ieKT^(SB+mG zS&EaoPL*w$6QA%EHGQ2bf}oK48g6MPEWckM3{$wF zD;9ktWHs@yE}G&J-KlbzWAN+|sSW|0fHvC6^Hu1k-Hz~G?itBm+|NbFRr9X@L_ZSS zoE!1ocwgkYKYhZt(sLuU{7#YkMkx9TqlcN>!i6X#YALrR=x^NL6(cogFV}9TLC&>( zaXe@L)Y~hum5Ig`S1Wwar|_KUvJ~J+zRXj|Nf5#n{Sr+>s!g(_$ z5{jP`TF2WI`TA<+lC;ze?RyF>O5`CI%83S)_QD^Els*svU zI}ORj)mbO@h#atx8E&3$KbVdV`P`2rffSa(I-{z`tv!7z% zJ60|4phxcuDa(1IiHM9#qG}p6jNNIH=Z{U#%f0epIyU}kE;B8Yrb|LdHddZ6<&`ca?`I70v$moywSImiQ4*oWfSfW=b6Nt(p?R>QMJXM z8-NNK?llfoJ9tkNjNle|yI?vg#6P{=sqc4n*p9@wF>d%V{``jV79aMWA@q6fIaYkG z+QI#M&_~{L$B;jJV^S>NI|SY78mzw`ck+JoSPc|zpEoJHo`{GeT!jbI z6QU-n?0PI{oZwaFjxf_CgX_rtmn|tF-}|&Yyp}@t_*WL!|Eo47rZ7)X3Ag@RjxP{< z9dF#ABQlpHQn28?b0cVV`p>2GTFqmeW4JeAH&R}^XEzd1jZX^#0TT(Q3#~-Y%pld3kmfw-DhjJB21^Su;KmjD!D+{YO?5B_5_I^J2 zv?fGiKV)}_Z*(SeMiYhW!_{;;%%@LW?Dvf~*oh?QkPC7^fjQ4KPO2hbm7zLed>Y9zqq}eFxX0)2g`Mlw3h{- zZ~ASVGX$aE@J=>BZG{k<|93nL(>z1llvSF34G^4RC4%54m%fzj(O8t?B)$8nK|Irc zh*|9qcKYIxhmp(H<|JGGGlDg43+8)0gI}{Ay3b17y)) ziq$ND`7PyQuLo$UuPGFUKug)2M0cjVLdLA6nI`1Oe|o<_5vjuYLORWq2Or&f>=4)a1 zOx@3hz@{ga1)2hz4)FH#pcw!rlV%00$EID%GF{SWH13gMY`is01GM=lbK9%v`P zegH>`UT($J$zV&L=OI-tXk5##hYtgCMv4arHj&0T@Jq)L_7~Ke0Rb9>m}(Y{Ev0S6 z3(Dk9(}o{5k>Y(^u`4xhbt8Vs9QTYCd&(%H4N?BjkG*7U4J0g)RhAf3LZ$ZMh|AHD zYAB4qv8em!2d=_4N#JvwXajY zqZ=1EtlAp}0&Yd%IP1eLpflaYAFaXs6J4z^%T^cIQb`s9i)8rHX zzExssjos=SwV4NBg((kjibf0gn<9eHN%-=OrPtw&+Q^qR+3sbkn!@ye;P};EUvI;~ zYOkZ>4@1#Aj)-!my~v@rljRSS*OPY2rJ--43_e!Q$R<>v!Xq}=*s9Tfu)fcR$JB^N z%YW@U!SaIk7bjxG6|M5l>8=VFB7adByX&6Z5p_c4CB8H1z>$H#HJ|MN|GLC5T~+Ra z&TGe&wZBq@sc{1VQICXh{A>aLqZSmftrX^6mBd*Dsj3^bnlMS`VR7V>j0F7Ulwqu( zZ7%X*FHDO`zAiE0c@k=f)W`)j;e!O_G!G1b8cc3<`K`e5$eC$1dvjpFgbE$ZLvxwkx`D2 zf93>4Mo>vuZ|kTbd-~d1ha=YHx$f5-i9TnRjOTA;sPm?q5kB%@3qPHgHW@BK1h9jO zequVsrLsPoI{54yro}^Hrp3c~W>uo^Xc#NecDUUr?qL)xhA0S1;!=AiqBhk!LpB1< zAq1Hu0`KzagrYl7wBdnZL{CtnDMj1#q9FaYz&S)5c~-FiKwVQVom%!h$p*vEKbbJ4 zgG@{_iy)g%sHka2NUO_>p_R_R_{E)VhIgo54t+Oz+1yO0;Svdii>p<2`X%24(*Tj_ zH~)A`XOdUY7cxv<;)Gv55$Lt=KiLq6vjJ~&v0yNUHwe_*u2k|rm!xud%*;yOkuHF_ z!+DQ=)q}}K@hGd%Gk?>LGLt}S+Cpz^!u{ui8}cAG5dJIS-qx95E7Qk*@UuO{+PJdt ziC%q32n|3bJUrd=RRef>ppO0=pBd+XC;?bk-dn=^@te&!gBFC2!vYUZcL)O1B-x># z1ii}-#BE}Tav*>=YH>ik6LPMoscDv^eJOc? z{Dq^=1n;z;A_PMmeB^g0;{=2aF%T>2@!GI7Vh}n1$yJzw=IX8uLCF{4E71a^0c`{8 zS3;O`H7d|TP##V~J>LL&kn0W55#TKRVY5J(1U+nWfIkM*+0S1i&0%xG(t!gI(iRS(UVt;S)5X@gz)9cR7?wz0(=1~G^mIQfqcuHgoLk< z@PQ@F0N-Liu@gfI=ypes`-wz7Tz$}+p!k&Wvm&`gV%`9e(Oy=#I6L_3qQezmG;*Y6 zidjsmGXRU|ukw;2QxKq$DWn4YjGQhx`2BC4*$_gmN*=V<_a?S`*MgxSl*83|7^A4H z{FRPaLt$bgfmkYR=xmsRo)e3%$(P)EBm&cPkm>e8j3+f~`zTl*z=vHV>=!irB_WAA zC&2oLO=@w#EsTGO$Q5cpZ8v~+{2><~gyO!xTk&Kvxzn`;E=3xuVO2B8HMb(n8Cp|Z z3Lk^Wsqea;v>>u7VLSZPwJB`uYs=FG|JQZ~{ItQG*_9Q^t0T`zl&U{+KTPkv{B&`c zMgC3lkS(&mE?t$U8Cr)~?qq&KvQMCvB_h;2xeN4DZE*0C+({Uq{Lb-;%F9BVqn4kG zF!;sKc+PkEY+8ZeCphc4mU>O;YCq=*)+F0p_o2~Iiyym4QA=gZQ8U|WM~I)voy;8_ znt)&OB#Lp)=T}a4m6G?)WOfU}_b=}BWsAQZI=emQ)FrGHK*)cImLMJ(z$dGSr3+2i z%uuLGT_4OAlSd6G5tbgw3`pYrk?;^=F}W#Sq;@ILrmEZJdaBpH@`q`;jD6I=X$%`8 zCzjU}P-8nRN{92kyQ@dvl9;S)eLhj=emOA6_^jDVthdkB8OE&1r!Mw3qR|B??8m7| zZlKb1gHx}EfFA(49(WH`CV-QIQXeiJqA>qXOeFO0FVYuVHvo#KFu##@U*n|Qc}|oz zNa~7T`2mw0stChe*N?>Y&xiqmyinwO#g4*!h|zbPq}uyRtnf@3l#9VC=7WjFt%1n3 zu@6Qvc$K^|X%Cx82NlAO!K8o5XX0_&Ao&<;x}hW2DK&nMl(SjN?{gy%yA7wk2*d!D z@0@MF<25H%O!3#+FOz51ioQ;H%v~>2Ut@i#qb_vXxvP?USwe8c;z$04AxVmRqrn|C z72O|e3+e@4zoyF)mG(_nP)nhSsTzHt5SF4g^)oI-Z*UCCvz zJoCQl_U!$IaD^w1%k1)F96^VOS|@slWm_wxEx}|ZmT)Bm70feuZl33pA`Ypc$t_kq zejUq-WbSv~ry#`X1H`v#pfObvUk!iC*)39DJv;HR%FTz!;u zuaElC*Q%_6?{WH925Hs|Zq4M*7Si9Tsj|F?P7-OMTl?VcvPl)0bT4=)g_B3T&%kN$ zLbU=VLvswbsO z7k54GpirpUBYBj_q}2$`OiIr!>SNUE!>)*aAO{!bT{CiFz+eE(s?$z8w*G>%16D1h z1XwFQ*~Z4SfJ>~6y(_p6^)3;adsB6=a6EPC^Qh)*FZ;WVDVkR9&p6`sSeCi?VHlI!+EMFRUy1iAf+!=wAQ1r>X zVwN8(c?aPWdxx`>;X1K`mF%$wNeEw*ximpf(iHfOo zV%*d*+)%t+V{ie38P`{kWO^vWPhswq?T{xrX|>S8=l7R|ZMB|oIMh7vG{Q~~pjg4Q z0c<_M2ZU=f0j>$|>4#iD5a(WM;0EU;z5!<#!vb!YrpqMMePCZQL^TV=J}rM}KIbEt zMAb?vbtuwIXv!NhhXqmwZ3phY(R!fC{9?@MxM?@j93d(c+yZx7%Y1fzhNSPU_L}Rj zCzv&ep&I=6Dnxto_6_;7Kb)I%*QYFeMPd`Tw%*S=1Y(46w}dl zj?M#x3MIv(*T&s~8n%{c?N>x7Y#!Cw%*iJiwQ}ybF>GB4+@P7z+@PIF%xenVxq|E$ zMDOn;PV=8HbkCm`UJZ9m6;6D38oYR*b3;1t2R4M&F#)o&KjLcxcA~=KDq;q()|mj` zjd^b%7*Vo;PbmpfL6j+Agmi}Qfq`#Dv1Ftm(J~~jwU53nkgApV%s*R(6aN0h=JrSK z#F7GE5(D?%t?Bk!%IG(Di9&^_yd3od-OtpY`*7jToRK^e$yIru#7#9H!WB>Q@*5Kg z8pUfNYwdgEE@KHM?5iXmCP#aHD8^q0X`jHMMfWqNu53FF2~t1d((X1AiC!>`<*&U( z)$*RzGG(hwp7@51Oj=XHF*smgI1EF$cfbt?WiV2F!^}eUGoj)bLuHLW&^Squn|X?$ zq)(Do<^^{KQuG_;@Rn4pH85qxIFyrW#T0C7Dx|$HE+F6!W!-&#I!3Cxbl!HWeNXHcajM>g+JpFU z*N*tjpc+Z0liOEb%s-s0tfMHmlPYfWU)wPQ_%6bsf933XF$h_`Cyd^wfprlX2wN`| zxq%vDxyEff4)Qe zVxwKRwj+mzr;j>aYuWU#g#V)3_Ypr6r0yNM47~Fskhj4#3ZN0ozs2O? zg%r>Ug@a!d3TJF_sazTiWJox-y*Dz6>Zx`Z9c)Et%x~W7HIq{oqpzfT?7PoVY`b}Z zTYQRz*2LZIxh7oEL1P*Cw43@ScFcRE1|!sk4YN@}mabNQ`TJs0Ln`9DL8Ilz^MhMC zrFjDX%BypzNYJcS<)shlW_q{~GIs)I1n$*pWX{m)uZOvw!q@y+oj%T)m9E#&#Wfqk0 zFGNr^Nm3Fo_U_AGBf7sUaXgY})Sul?q0Gkb=C$b9P4l|FlwAp$Z)d#JEH}=!FO5n! ze_u4|WL9poR7euvv2W-e-`HDZ-m%|WzHq!@3wr{+OL6;#S3Q|TfCkYMN1S+2JV|PY zQsoIvD60M_b=ZHx<a{(SulT zDKC=k_k~kbE1o2y*VuW4Z~CD3#lA0o-DV^iB0&RUIWGep-~~;I$wm z5DP(d%Io&7Bp6w^FTN6W;KaF!%7=8GM;-Kv4=C*h>=)JP-$6B$N5FdL22{UPAijXy zK#;ReKQu1Ui$koSC&8S1m%cIB*xhOFKA+-i67wgUXB8r-TBv@9($kjQ`Wh@+_-(xk z&KA-7WGq`+>=6c?-&2N+1SS)H5a!b@2Drc=ru;q>oIu<@VPTw24Hc(Xm!hhN41Y*i^Qz?bn<6GN)T*^3u*M8fhr{Ffl67z(b zjq7Mt6uETamo{kJ_xrH5>v)sGJY@ZJbcCH7XCNZ?m2)W{QNTthKvWz5CgFe?6bf#_ zemeIC{mVpIgy)nf_zNmUl(GI0Ty4?lrb9B>xl6d8Zq7J}H(aNVvFoSb{KqVur03E6 zC7lJ-1i6}ykZ_T3Q7I1xvmPv>|B~Ur%tR>|2wvN8m4XR~g<%RdazIZa#PN~IuOU}S z7nFeLXeDp%K8BQoo*yo2Gn1P2Erz(jN4J!7eHt zVPNu}sIc%u$UfaZ;K_0_hq&2!3r+=PmyR6~B$#}tM;LCpeX>;+sTCJE|4hNNmawEp z@I)&PF}wXXuX5@AfPQMET!Sj$pmK4XZ_ktl_WuV*JpjRwS13dml`U;Qqr(c$$oq(I zhsVZsa_y2E#h?o_S8l z>S}SLfD~jOlmBykg=cilPUTY)aLr*FJy@f!qZ+i|L^ zowbhYX)Shg$ga?T0fE9yy;=zT&yqd5Dj-B6PMoUzPX^`Ub)G|d-)c!T7k&EOTt`xe8rHWX0{$({cZQz2pvn}h)>7UH(hkRkseHH*rh##EUhteI_CW5 zl@}z|r(2Aw>5uYVOD7FUa2V z*aZ6tq82@UeKWAdX~^P_ae(wf49YHF-f`eu@7+=ZrovJ846!oz+?ev8fKh^9VpRLI9%7fm`%oDEAMCaa0&R zf7=MYbho8Y^d=PT&T9jRPrLfsS*_~vnOFCnKHv@>LFg-QQ<_{;USsHW$#G;r;tGHC zDO(5b=n>z=w>6q(D-0X8#}f@iWOFnfhfAdSY#;my0ECFg@12DVg18~#LTrOjOR8G1 zETKsjlr1n0=6PJjhf^MAR8OZCcbV2YM}M`W-!6I>VUF!j?azvBhAO!-ZZ1d7OD2n+ zA)V~iXBlgwd*(F}@k^KsSt!9nq!9xIP+vSjE+6XEfx>tc3^;-et$|d!4P}WqgY8#djPQrdOI z^mf;Uh$*p$gC9Ql-|_R|H32+aO{@;QEa7^a2Y2wVO4ewzj-6>zMM-;|_>doym$PnL zE}*h>-vjEBd4+^K1R;a*^Pp7or>c;5f=j_{dlV5PGrr!j4KeGbLi%_>fCIpTE2T&h zY2cxOdA4}Z0kiVx?Zh>&tsq|v3HAGn`?E9qYo;-Zwck~rB?YJEP&Wfd`CBfA7Bd?% zg0sVh#52&I0UaE>5@m~NwyS?a8O?l=7SZdrBL&E>ginXaXZ z2)cepZPkJu69};AnXI-;df)D?xNhmQBBABvAu7gRQC{8z0?xqIX6$L?j$W`~8G0gd z_FjxkorZ%Cl_&3!CegjIfKPef&|)6Wn20F%578SJ-hR%vSwQr45D%JoBdE&d!SI6I zmBW&Q2!bLMSm1dAL_)4&H>q7p4SqF{7Rk0^I0c`P2YiTS@Ywi-CQ}mixO}3AmoQvAbDIobm$B#Rch`LDh2&Sg` z)|tP8(;ri{1{X;rD|Ib|QoNm@xl?o2@@C~dk|?EwRWZARDDIr_9F?Vl^N1DrQa}rU z#Y150*&!1FOF|DC{aA@VbFKw_UZOPQQZ})IdRN?};q-whz0p;_#mtR_36XDWPpONh zmrAyczP;To7`>UAhxdPPc-Cj*Ajfaa?zXvm^bl%#$<1Bv^IKbZkZSou` zB>)jX7ZLD*!7p8(R*jTI=$&U5Eg%ALV=@&MkzLR9`o-ezGv9%c)n1;!=*x zQS-`r*_tg2o(!R$OBP*U@-nCTq(uc~W*4G1wM+|47C^;rfYrnYL^#D!k9Z8QmJ2o~ zphIBHQyfiyBX`V(K125osx^_q0tp5<>T^QQ3GZOc6nh_|?m%ALz2mr*xxb-+47qWO z&#-eGzq`768_u)Xe;wf(B+KwB>jdVCAr_Q4$;j1%L8m$R;1lxw`r1D`dLK1izZ}k) z?0iONmPUKU>%6F7kk`!=&a;>+u+m7F9Vn6h8yjX3JcKYfP&mJ|xQ;!=I!JpCgjsUc zf^ms5eU=QQMIcSLIc83I*2j*ZWN|xWbaZ5Q$MYs(cB#Qt*U{0<>XGfy?U7%V^8b#h z9!N|ze#}9njy21~wc`$^apUn<-U~N%WWo#lZSr< zPr0Y4ozZF?FXo%mhEtIUW0geUE*MyVMLOWX*%7u0mKQiRKE*~z>XEA4<3fnig)HxB z^$sEDQ|*K0Z?CNzEc$fMre0lNaPU3nU8u^?`uUHo0gx!ETBaZIIARV{FP12aim@f} z(}Attx%_qY=qMWZ8rQ}ZBR2%xg$ zacLSn)W$_~f^i0GWlBm3OY-wtwpvt`O{i*kTS>2znX_wUHZ>|ei^ovS7G#ITZ+_Lm zMos|DNRJ9uG)DLxxa(Lt4hw060<=k?S%*GTw?~p5yF3k_b&aR!hQ!D-KBPSJ?bmV> z|Mi8!4RW~8<;%O1jJtfq#!^0G+o|b}0wFe$z8aWD@n7}mzPR$9nUX0#Thu=?Y(YJ* z2Hi_=)e+BJ7dYO!&K_TICECK^N^pysvM|RQM;26YZFY~{Q)KJz@*2(37&VR_^=MD- z??PyTN_mwXpgaJl!8pM?5uil`jG{6k2}2Gz`f&WY1};=FAS4n42nXf+>=1a53u~+~ z+|fL~gPb=*vX6Dw)^207Qp3d+$307fHCu@O?qeDht_bw}Y*{k>{~Ygks6CQ#2dDB{ zaKhN++Ag&6Hj`ejEZ|*B+ozgY8emtgbK?J=WMX5u%?{nYNMCIb*s4H@1ba4O>baAk zjPnQ|sWV$O{Gm!lVEwgH$fB`q5~Yz1pOXigijTkw?{z&y&pq4nV-%&TLb82^R_yLa zHSssv^l=Z)Y1MkKtuCf@n*kki81>(GB2nL@{j}=Zc#5!NA@vD}`gWau`9Eg)uKpF2 z`R!H`ot5EgyVmDox=MJ60x6-d(e(s;D(@;dT47# zsB);4umAwLRcSD81ivG6K1h#Xa0$1L5r0*8q$#r$Xk{9LWZ~A;8kWm%m&qJ1uZWiP z(E~cKCR|ncP`x$q`iIMxvHv;(Owe@=Ca$ao53?=5`8zDC#)@ClO9@IQ@>=kM;&Izu zE4@`EwZ*C0x&d8WW?f6Xk#r)8dwBqt;-ly7DOxjk+gH=2E{~x-5&Dc8Ot<_XyZgc6 zMU2j1@dHqZFs(q5Bep+1O*j<6xP#6y;SluMcnfsI9h;haOr9roto4hzVHA0YEG}Ka zur%dljyU(&VZ&&((AjeBi7gwDr+)Kb*hu;y3=62h76wW=c>!Pah-2?Ex=oHT4HZmK z#6ZH96>iJP6hqb+dG@=>zOX8ztN1U{Qh}nUjp_d!n~!O6(~23dvVp22WblZG_*v#^?rBm`QTAgYfkh zsxbDo7h+RlGr4c5bGlXH*2atLlly&$k&r)JB8hl4z(WB$j|BqykX|$Y1qAY-d946C zh!t5fD0?|b6~7MT8e@Tg(Cffzd*3~Dr8!bNqYF8<6YKWhp)1V}tIrkg1ZROK`Qt<1 zekG#U_0TWwc70E|&^^~Bvh9!dqAUf{I4Y!_OqRfv9mX5>7dYGWKmtE=g&(%YM8`Jb$8utCJB8vS2+t1uioDvE15959^gukjY%uqR9I)|8@8xsW} z#v|UM5MZ34_W)^k)H@yzbs!=sg8`|;dP1l-EE89U5xls@#&loHi0LcHGtq9;1a#$G z6y}G1*w81)jh9l#)*?y7GZ9EVV&^v>8p4>43N~jHJx?SCd zN=Uli{3MXX~PH#I6gwSL4HIWTChBj zACcu5E=doB9fNv^^x=5)A+&h%oH2Q6Mgs_IVzY=Rse~m$zt^RG$Y_7d;MW#(Sbp8T z5;V`rvnkRAE#^l5Jv1Wo6-B;q6FLuBS_V(LrkKEmVBT4<0v=yNhD3yksGsB-^SmA0 z_ly8}O2A$x4PL|u1?LZ}A(o{(Xv70S38WZc>A;K*fFl52bD&Pw?Tu30Se*rM?^oR1 z32rp{Kk|4X-uAGixWc5_6sj)lgK&Sye{Eu-#QMieb^fN0rvZ_FEY4P;2Akf&l%67O00bPFt-_CX=->JrtN3|O=bd0MlMjH)iZF;vZVN%tjnC_s} z?+u%#ywOj|WzH-;=GK;Ciu)17ta#>cvM5$++35fk@Tk7NznDfFrdZ)X%3%4|sMLiw z>rSYby6+&rH5KuJivcOc$)O}W^m2g11{od%;g=RfX6jFLD|=Oy?^~aOVjNokh#MoG zv}iL8&1nA-cRV(}kG`okSeY|i^7?xG%yzp%WRsod6me@XCY z*zxOb!*$^Xw|XcdgaE&dxD#Q};Hi^7@M1_o+5=Yza9H3L+L!IXK^l6%MjL*q5PX1t zf&^=*O5NH|r5^Y2d6DmBALyw@x4N&y^=FIiEuS$fNhRc;`5I)xaY;F%9>KrZ?8Y2@ zWqC;oF3WsCOGLV|v$5^AH0mxiqJSYUIjuEt8_IX=>4e*20)7!NJRg0iA_?O$WJTPm z@VW(&t^i{CmcxcPu;n#e1;E~#_|TmCxvpHARVOxclTSwE{rgN_H;wnYkcr1O4AE<_ ze7e_+&SUdJcZ%K#QngAyYf=R2hSCz7XJ^FVV21qra0hoWBMN+qt1CsGd=P7~=G9{+ zU%;u3y)dVbp`mREyRZe`s9>Nupqc?*6;UXm5di2mBn{u{O-FEWC6^<3xR9q^fV`&y ztJ6z?H0V)gXze?HE_y-lH)lVb9|R0r$cWbt%I*>0V3{;MLU`*j{%eE~hF`8ptS8s) z%R{<7S$O$oOJutCpGB-ty+kj5^Un#@YByb&90r5%2pB!!(HYDx>! z(VLFsGxQ-j!x91nNRP>T2<%^+nH0Q*>ZC7<3K9h&PxYK@K#p!g)nZ}wk3YdmUMK&` zDT18iP=CuXL!EoTC_q$z>Ri4x7g~nZfcba_rFl!fE;OFp#~myw4xi}J1&cHZz#-nT zimEJv67L7U^*cRo=HNAl^7uNli%;~xZCD=OUV(Ssqs=nfX;Y{9dzg^N^&iA7!Ff|t zZ_ODu)eLv9ops_0d^vd`pkJDIu}wp9g51>A%sUqV)WcduYR7GGzJ25+v| zr(+aXq)p^tCh`%AMfchiehe6Q+N2U7*v(nzIZ99(p+FJyGcpORT8I1vOoEv!SV|Lg z7u|Ozq)x55p~x(NjVIR+QN@T5<3{Fn{1b9DJ2Y`5DO-w#|E`m#uEF zNtTA=_NRT>Maj}@DP3n8y7%XZTMvBYP6Rtkf-!2dVIkIpv67!_oq_qmT9i{)6LvLo zu+1>YY8DWbAb=7sA(AX2J0-8ac=o0B4yG;cjxAAT1No@b>dJ|_$B|@^|M}tR6o|sW z*HFttX$1aNB%mGXNW&|jL(V5%~x$Jg^nX9B23xlUCbQ#@)~)kg*-D6 zi1izpzb@+w`87kpFIvl3-H>2XtmRGtjCW=|vVzVB9W!u{xMfQ;agzdh!cCIf%C**2 zsBjXQH@e#j&m z{|JUmD_+I#1iAVfV$e)+u#)_hcd5)!v3k7A6{o4M!A>j1(|MxnFou`Q-sc&u^WUTB zJarCIF(7rZQwU`TNFCuGu(1N*6tbM`P9UejrVp3A1HS(6Ji$nH?swPjei?A}q)zSg z$N56%ICVL`YE%VvCx!bzvc#2fVSRS<(MsHy<8K2knBpv>+HAP?JeXY0lLbQ*NFYJl zKOQ2%tx2M+y5J^}>9q+W2E9P+8&aVx3?>7gw8fP;*VeCz7CLm=@B|>S3QXXLwF2TR z3=%3!7$nj?fTRit!XR9}&kco+C?VBTy3@1#VM;cVe*+;}LH>Q-lUnwaWeb0_9e3<+ z(nynqppA3JZ?(ih{#wZk86Fd8Ch_UX^is&w7dWT&s0m)g`T4I!{h-Yxaw?L#V)Sb% zsx^1ikaj}OwkGxqVOQOBLeUE|g%@79nSpG721|p~4oDv`fIe`nV+V_U;Y<=hGJw9L zCmWbSz*1Y#<_IK9TY~2+9_cnY%_(=)UJe&2Ou5TM!Yf$t_aAsA87<9B&v>R~?2=?3 zGS*YAQ-bn?TK1)+Je8(};^GXGDLvS`RJr60Q1x-VgewaZv_@*QFC@g(y`{o!{a8lK zi@wc5Jnp16;Tz?BI<4Cpf?bc?41hnBKwR;=`bG zvN#-hGQ}7)_yQ^6?O9%Qg)`FaHTFjEH-A7AdR5Idiay})cHx<=f2||-aN?IB1x*ug z=L?6T7fVin27XP%+G5RdQT&vp4Q2141gs4HxSwc~$B zh0;FZe06WYdv-fhy5LKtu6{LHIK)M0&qG{w@MaLwz6szO#%GFs?hR3nk?~<)d?0E8 z$6ywlU}<1RA3OVYc;mat_AjL7I2k?3zq>LLr{sA((3-Of>uY%}DK2K%mCRY%r9?AM zf~acar&yarW%hVFI4`oN_&V>sa9iKahenE5W#j1DRExsopE@YEU#OXC=M}yAdb)Jz zolq|wBt@iJGX-1#L@We&1h>FbUWd<7Aly7K#xM&ZcXc_iic|oNSDeqmF zSmW?BL8XKtMeEGplu^C;9t*ArWPz%Py1tnWkSVm3VnZcFqY`T>;fn?b4L%adcY2!by8GvCD zsC8@_1n%Nsp)?)G4uau-9x^c9txH!y(-#Q~i`i|&DWl5NNSWn<#R{g(uJ7EfNr_c)ldgHxYNAuTbF}VR zsoegM-2bhdq~z`98_R4fzxf}%PT8hkfMkzgXuXJJ<;i{&Ih2yWUk%_uATen7kV%k@ z#I75DcAxPNgu^zA>bT?vaZk4N=syQcAc^WF*9{L}1iKKKLp^jJQ1HY|6f^0$w{|(* z!^F=15yuTpT}KCUKKvcG)r3<$n(4grSfJ?252MUJ3ZDw2{nx>}V8v_q0P`wf$lrx59Qn|E%s=JG==!UxY7)_bjA()5%7-94aa|j?pQodkwW>?|L8axi-0suDng>XII6_RrW z3f#or4bJ>fqVfGsWh;r<IlWf$$_%-o(vw}Pr4=1wR#|70D#$r> zz^gMHoE?GT@C78P`QoA|)*%(&dL9;o>W0Apn*vFJCDBNlt!82IY5DiQj}nk*RRM}x zlYfKm3@=;gTW2&zI&4knZJf6vmv4!Bg^(vHbb9si;{kR(wl`5$=jnU zFxdi5J4qghWJyqBgIA1c zKmXq)b@_C^WvMv8=rtww{rp*V%{Gn>Q?(TB)wceZk;JY3?a{+(VShnt>_ zcHql+u<-Gw#;hb zM5=y3K|tSa%lNH*5lHUv7mS%#OMkA#wYwXpgluGOV;pAcOe(32-&;kbtZeLyeA|<2 zr6rSPGT-tP1o|&a;cCyXc8FJWYj?e9)8(Z__%@ZSDr$-5)EM7mrcJ0d8a zWJlpgevge1nBNctRp=Rvt&Wm&k8I^pI&<`xXfR{|ZvB6cX*9);xf1t-e8Dp28N4>- z$uNm9nWvc3>18ah;__ylBDt6yUwtOKCtLH}SjLzRT&VImL2p&)>E!eKgJmE7|2%&)_B@IBd3>cH(ryq zf~{Uyuq(Gb#=Cua-|p{0#b+sv4VJr+@Eekk0%4pofK=;o?0GJ z7|N(yTy{s`f3gQ}{xfMwg#Nybj}b#Iaz*YSYc&)&6Llxn$H9;bfc92Hnn{Bp9I{hF zY%WYQ0nI6D+8L}^l3Q-3z|aTmOQeW7d0V7?H>b<4Uv=hqRO$S|H#UhNYFX;UzDaLX zYJ?ZRX&$e;(h%cw_*eH?M$94fn_cA$$k6EM`oJTk3P&nmdN5-PxU~jZul?q=X*i5@ z*<85n4#wy}(;5O(`=+bvb&7Pia6nG}2N`BFnZd8JJy-%Vk%qu(U~g&qQ(w9oJaCdX z(Qv&7;sn&AriIAm0rD{#R{)WD9ZCJ7=ZE?Z_%kp*B{{WJg2odeemXBsuoqS5*O;@o z2DPw2HpxiX_}2Lhl)lwIgdv+kj##ZLd0QO3sH2e;h0i6`^88VS34+HSh_q7le`N!J zzM(9ydC86<3oxZY_5!ZNG-vFT%b%afk!$IW+ggD;Yy}gevUol6^`=a?pN&u91DJ5% z9je>^P<6EgxT$dCWHh_pYx(NHO9Ary+?r&$q8f#X52SJyK>?`qU!&QAZW0vkH7}kb zh?pWxzlzMz=f=)3700)}nirdLH$Kwf^!JDFp8gG%-vg00Q9Y&E+{Exnlc9UEW+aKMz@7ANzPy-d2`` zmatp#Tgg9^7-J#k_NkHu!=Pn;twIP)0&F76#blxz}unPJ{ISCl)Vhz~O=!&U)YhQ0Be-;kog2%G}2a zIIEO`omgSv!dLV8sR~NPew=+ zo^y%EK>}i1QR@GG8^VtYdQ3z~awZv8qJ>Ki^f9--oZ$++(|D^F z3bdO>w5UL1AH5GcA77}NHb^YMlN}n_IJ){b`Gc@R;7oT@1wMMPwj*%N#1%{I`mz;sVDC;6-1QY(Mx{J^gFM*z`-+LbT84uF365<0U^A zI==fmg*d$#FB!YLvb^t#&*am6S9^qfR;RVSUGem@?jTGp^D%?!TgIJt5z)52e+ux7 zDX#FWsLa|WEme+b&(9m1URN6JP(Z+4N9j@o8DyEa^@-E0( zQA)y(-r@d^D&W%qdIaCM$ET8{P4%_Z^2)-IoL(ObkRx3TG=V z-BSAQJ+m8J#=`NQ9pOtCs{4f&k!Tt3wsr^x=`iEVl;fQg;5hbv$h4F*T4}NGn&u0h z)ux~~Q!&>TqqWDr8Xx0u6w4d?j+F8MOrRi-+<8jCo*bs|z;gue zcl4z@^jG_Vo{h3{1DYumfnGsh*^bn0arMfR;cHLmtd4uR@&sWfuK zc*@yZ%a=X*c&`6^{wYa=ta%Kpbal_~Q&iD%bIapCt7GHGEZWCPL!}Mu+*Y+16zdOx zUkA)uHBj;Z$1&(OA@~A`i}cPA3T!AN!CEV)o_8C(OE2ba0juj{=<|SvM7bSGhJTPW zE8co~(@CE#^-^-C(F&{Loh!i|{w7_**~wzx60v)?u6j)?yL~qC)#RrOzpOj^DxcN@ zK7q)bOZGpU%*=UJTq)YlY@*F!sxf9+cFNxg#`@Jj6Goo%Kz2n&nh>G^{jG!{7E}gk zEKM8?#sC=;ihdUovrGozsOU@JcW6_j0ARzOYW?wAvl5S;zrHfv`XQga&A?RKYj*Ln zu|&`OrOA&a;$U2g&Dyedh~#0K`X>ImRs6`o0pDfzbV0XTi709A61t1uIc#cu>F}%H z&K8xuUi|2*?dj)eK-bt#gBPn?jd-@=5Fo8oElQ^g^Nkl8)dSib&l1US zm`mNL)5GRUm>da8axq2E1G-t{p+S=D!1E{s(;(O)L6109QlOK_nI|(W1N$&iMIi(O zJx+*a1Sxx}>L(KIcx5&aN^LaV`o5Yg5d&ZJ3|0Es<=JZNBAq_cMQYwKLll4J^(a7v zk;k8|^R~oW*uGY7@kzaBADz=R^*p98K*2}+-L?|BZ^WsLnAsRF!d-HIwe81nTw)zU z@BMiNemzxkvgM6#Waf{5$C`3F-oX3eWa3N7#->+gnB{`%5~k{7nsmeCCv%u)2}3Q4 zmaFbUi?weSdK^V?Ro-kMD4Pnrv_mnXIN}|rV(B}j-$n6^Yi=yZ)>AhCONjptZBIOS z^8=cW+Ee=#XpPkKkfHEJnS+o|QD{L?95`WK^cXVmFj{ZJW<+tdv9^;@LrKf;Z4_UH zS`|dV+}ebCty(ZOjQGrGtBc9pb$WX0TZQ^iTcTTz7{u zYc&*Kb(-$M`g>Rz%T>$cP1oLXJ{QrM+d&-XLM>{3qrmS6aeuYy*Rv_AN=+9ts26%D z1#nHh+s3xH?8tgUaE?(Y7!(j_@BzkhLA~)!EoojeIm&_9Z(m}Rg?$*Mm;ws>+2;-Y zOjl)_+<@#!qHqAByEaRL33L^bAsOcV?$fvTM5H0o_)BKJw*LSO6Y!UZxq>qt=PeMP z&B%jep9MFOE@0?E02Qlu+@+RoM)G?KX`1|Tp7S@uYV|JVT2mV>4`l`rg$z<_y?f@Z z%}u_ZthVCzs7>tApvRbmN8hcKGbzG49$62d+lc+@Spo(|^tk1TI&f-Bf6bnHXF8%r zteW5uSKvgoWA35(LkI#?c%#4}&)R0)0&N=4(us!HUZOI<@@`>1C zUs9`K8xB^?GwNOwB_q2R`{vlg^o-~}RWH=o`O@)pc1N(G8)3Sn>zG4_mz==|4P6aL zI?EsNE(m`hm2h5WCNoaWm?vX(AOte&G$mH-p??+Hhtw8OmX`YP54pkbm zdkU9$Ku>!nsF8j6@GeXKVJfhA?fx!O^G#);LZw&bbL1>z_cQjdlqx!3_H{CdVC!d? zA_Qa1)o3tg_p(=4CRex44Befs{WZz$m6Klkdiudi*Y9d|@_QydLZzIBL7w5xRbbG- z#8WY@;90V=V~cv zV|HTyd1||!j(r2G6IEXRYy^uwK3q=;qJSu^1taVhY3l9EP5eElvfj6!c<3|nzN?YN zBE};yW~($`r2URGMUS67(?u_*JBJx(eJ@I}5ivX--UbB}y2Ng6Gc9*X?Sc@^{Q6%# zFVbG_Qi=Ss%}uh{^u_Rq1UhSt?ci7Z_(4U(3|fs+_m)gp}M7 zuyM`<1&^O%iN*=~)HN1pzY;O}-9KV{>e_e3@FEHL_*jrn;5thh^sF#x-7F5?V76z_ zq&466o8(~RhzOzvg2Z@@P_aGSK!X5u21@A*Q&5iRp867L`OADhPSDxBpN61^>G8P8 zPJDfdmHYKL^b|pBM^j=O5vmyhU;FPNS=KrQ-%oXAGgD)F>O}D}pe2GGK+Gh%Gf7B< zMjoDPPLXIt32hRIWpfL(n7|8jme~P$nN&oblb}}7m-ws4NLSWhcmc)A1c9=5cA9)A z9L)+sX6&TRHHLeTqqqp9b|UW_Z^srIcJQ4$e2{v5X#k&iDo>$B9|vMg0sG-Eyao_A zU;BAAVL5OzMd+ykmb57XVl9Y6B{9vte=^D|D$Fs9Z8YY4mO4u(X?i^Mj}=$Ig#=uE zyV9JUsw~{!W;w|v?NK{bhuPbC(v%$zEF9L3y&HWt&6zSep!F4=lGM$+2?o)i=LW~( z3WU8wbGPpJ1w8wBJ&jPvZZ8u!kq9JvP`Mn&7^n+)A!Pmz45t2pkDxAI{h58bCZ}U% zKIs#6_c>QR{na|jQ4dpSSA;ftR#w7oDfq3mXq5-hK0?gpNyH~$^23V&B6kY%YSpUZ zOHXBkJzJM#4l=;p!lWU<0xUbBB8Meofa?JHeS>YO)%m+5Nog0+@%qUn{Yn$p6Z!>{ z>ZX(K;ExW)xY*i(00U_zvaR8LZbKpF2;5tvD+0GY2$}%^7y;G~UbBD)iU)88$Y2)I z3p2wW+tUDygrGbJ_g|d>$m@or+Of9{U;MIYdiUzHqJiCt-CoVp_z%=()P=irxc&Fx z1sJ*pzb9giZ?0oaZDAsAKsXxXDo2f@af`@J`VDg@Qqvv)(^C@j6`vhDxEe`EzhxX>Zn5s(b64zMo}2o5F|Fc5&29fiUi+q1|< z5^m6w6+PMAXo*_0F4#40Lvm~QgD^vxYkyGPf5S3+sD6>vEOsRBzZD5x61Y<#0S@~4 z)f+$mr7g)UGRM|-+7RQ1R=B}L6MXA7-kgC(S%KAG=VkC;Ab>SpABAJ$QJ#6t)X^xU~)9U&2vHu%g0x4$7?yKDZCq7503E~f$!FhWW-1Z z#69GB93 zjBW-b0e`4Ii1Y=Y9_)vL2!m+xk3pA$PnhpBOf2OrAwcSgcYriOB$v9*K zPPSr~LQ$Th8Z3;Oa#f|oNsbD@9zuW+$-;1iZ?ftQ|QR0FPW zuzc;Wqvcn>GSi(BeFSdka)0-kCHH1>4$w~DVv=;`!XGw328Qdq8L3U!ttcQNs*# z2DH8aT9F=G#Ts{TuepP^GlK0sESe;~c7tbD1>Z4ydEj3`3&84T(3s&Ub1D39`t~h5 zGKy6Xb)&1(Kq(MiW4xadEG?y8C;9z7>UZ$PP;9(sOc*?}PeiqvU1= zTT9%Af`hnIMqwL{t^_y>h9Z6i2q$3$BO;KZcM2x)K+`jYqe?kL3DqD1*>AxoA?`q3 z1)l_5r0Pq)zdi{#H&?-F>3|IhJe6mz3Jj}GRlUZmAz%<5pe*cMlunQ44c7)zkcTp~ zlQ61+UkpbG{0fm83gxA_%(%@p>GIC-$&|wZx|@IV*tv8_A)Ei_0Ror!T0P6wJSDG2Pr1EC}UcNsa}Fqq~gxZChk1h6+&# zlW34dDgj8o%1A(_uNCBy@m8%GvOh`zBwTYv#r;?vmn7oMR{MtB}F<{N+d)mJo2XW7Hv##Y@QKcTox zIbnxC)4R0naCV3elbL&@lOWoEZT{_al9%vQZ%zl;wCW+e7tAb?q#9l^FeON?Bz7Qn zDg!BIP!2rqAlQ+B42oeS62ErXOd6;yxA=9 zqQ3br&YX&g`W8@gs`zJ+)aRswGhe~295eQ1NqTSOZhtAk&7rr5LMI7vs)HR~EnmUq z0%(%u0yX~23sQF9y(BO7kZyowa`8t{j^cdca#Ma1jC;c1c!HT?MW6`KUr<0=J1Xlb z9nyS7{7LC*80A2CTKhpjPU2ZI!)x7=+3!Ad=Xb0S1-h1JzZTId z%sJq$$$S}mHAD6RYy^6{T#6doLr;JaoxRdU3g)c-WlZ=>GbQT)FE@ja<$7lPCYyt% z7%6fSW)Ik``TOzNRsGY{ zrprvEe~8(V^Cf4lifnQ!W+>g9x5D|hLD7S+-b{xU3=Btm_0Eq6Z%?a{x=B$~#-?aD zPIG_`lau~wK}w^S#c`Z6eMQd}cz-shg9CHq157TotZH%-fh(Km4>XF9H7l%buml99 zl~jmltVrjEsB>Uw!r;PgcQBlS<=$E+QTgJ(j~KkFi{;Asp#pEcUN4WA4D|oVxj>~e zxNX^(b%32CI=zZ;Ui>0x9TwH7X!}HKQb%r0u+{gEQo20m30xR2G}d4@a6HN4O40J8 zae*iRw}I=4H9D$f-Q$pu;uB!(LyxZg{YtYHbH#*QVh1?~VV&;|wmlG!P-h44U|qF^ z*i9MouaOP%3n3T+ofJgNcLP(P3;JKzo&|#-2Mvl`rpyH}QMC2{X+|v!5a0P#HuYj% zvHWo7Si`!U<#?Ib`Cp`rCWf8v8Vg6*2YvbM)7W)=yG7HF#`dBUSdoh#a>&QGg&L|% zX+L=7Lo3AncvpJE+irNm!1<%5F}DN<$Y5xR(P)80I$$FaRo1SP62d0BPN<({VRq_= z{HZGr{C9e~^86V)aiK@_csifC*|zG19y^g^mlvnImDma`7B5>qbL0IqsIcq)#l^^J zhRR3YytIq-6yk^6c;%`rTAG$1)L4>5d#VT*7OZZSKmtt zPsBf3eM-^Zxfkdf{7^;#)~O4&r${dbjV3BO){IEt4VynfMMoj zVby!SIo-+j-OdD`pNY#(Q!$h?eJ;!U_TO$8-J%K&&RpHmUv^<&Y7d+#Bn^TKhqRJ{ zgwk1qO}$XYpkfa8Ag37tE#)fTq05#HaH%`!EMRrUM!TY4@8q@c>A}pxj|k8BnX52> z2I|Et1}lUk$}FcB9dnLy7WO?!?6%4%)%FSZ&R}M~F31w>{f(KuX~Fk2A#%&1wv#DF zt4EiNl!voVDc;IZ36GV>tX7e8cp49Uvl;aoeys4VWK}Cx<fa` zGQ-oK4R^E^ngUR|qQV}E1?YX&i@=v_Mc~Ee&%JmfegugCiwXD)AgPCMNcO8>xB->` zM(y^0?g%ZY>jL@fb=Rq1maoCt&Y_Ks?4F+BeF0d1rOlojsP?^FegV&9D@b)2f2{7t ztV+Diq$i; z9kYMi;O>+oRHVf94CoL2;$#qL2t*VTkRyYd>!1vy7A7JYTwY4fC ztp$)TB^|gsRd29E^341M5Wo;$Q-k!FE!uKgJ?G@bBlDSnlSd=HX@ma zZ2a-i3oO201TC2p2~!mZm|Z7q_s{V%H4q!Pr^4n%bnv zS78xV;#v*L`TKtxLdrzt@N(-;BB66Ry-#pco%f{tVvQ~Ka2Vk6Ae2KMM{ufu?*jjT zK#zBznuF%fy1C3Zqf4zCMU<)oTp4l7zR#`r4)he{Yrm4a*s!?|yd(dygOH(yZJ~2D z+TFVIg^k{BLlLDD0bPw|3%xts%Qa`bK*RdkYoOlj>uAK3+)$@}1~k2NmUi@*_v{oA zgt(`EBgY!#j>tH7uswMt%_j1&? zFt`=)YbD-aiW9it!@?`4Zx zy?Xz85h@Es=yQD+Ac(&lRF1ry3C;xJ05hW*6a5CgQd}+ZLvK8eU1%Q{_&J2jMKk+a z44Z^LAJ(4lk{hbafY}?JrBI}1crXQEJz^@6eVfJ(UU}D)zqR((Tm|?mb66hZ?(8nj zV=m44^z^UH57M0V%-^J^4z_b(`Q{5qFzo;AYvz(Wo)-r5*_(iXgP|5`MXZ}i-g5sq zrucQrisHq43)pE6x=zOxgY|Mtzl__}=B7if7k@4s!O0td#yB2$l5S`E0Trv7;c0J) zU%9nv`JMCdg{|O#2ikc*u*arX-SU3VE2kaStbhy5tG4Z?_8UJyJ>b}uNGHKpmptwn}%OHM(?uXmj!W5sG0%eT1(J)qi;4NV>$cdUhN#N@8WKo<$Zpp+`DrtO%lxNN{rPQABTKR(w zid%9&tHtdENH3GAioq&L>RNHfSy-A?2{{g7ClCcTi^HZ9L_CL5JqEc$A&CPVI~T6% z_S=?z$3C%v-75zW5dU?A)lVHSFR(1^3aJ#O*5Hdd#q`rmCpA+*!I(aKjG6Ct028Zs zwb&1W!;_v636Jg-Pgkz3*-Re;OWL+)Thw3h;q?{YqM+b7l7DWh8Dq? zBZR9?QDx#h@azy3)PMmkz=VRVg2Xrgjv}DB)M%Ul1z9GAbU7mUz|HBYphu4XmldIl z7;6b1a}Bvf=cNuygGYwZvyr!1j~U*^v>&R7ceAsmGK0%h4qOag!IlWdOlQYOUt}xM z*}p|})P2vQni}m<4O2^B`%BA5M>sG!E<+|IdBE@D1Aev&>{nqBpmYRZzE186s^&vL zwL;JXBrSw;c|ZMDrl$_}lV}V=-oj;sR>L5hlDuuTU|h3{;#nMg3x0nY)KzB2uvsq% zss*H{9k8rv_3JKvo@ps^J=pun&UQS{eS!pxpl;juxC`cJ&P1MRDgCKYU13x)8YwNy zMht9uk||-;DSy`qEVs&dLA|j{{wsM~en1l-6(Iue&5@=Rt`BS$|GBWr%I=6o;TCFuQc@K5j3uTU+9UY%x#y1cN+naoYWS85ps_l^{tc-)3U1o zLt}~@cJM_7Jn&Wpn2}yMD;il3j+Nrj^ab#=bnA=sfwMB?hKgnrWZ;2-{?Fj_B~HIV z+!FFs!PpAY;u(7JiY>h3e7+*ZgoB5B9YXnpYtvWhqxhZ$++EsLYA(G+dVa~gd!kcP zViQBa{^FFq$8#pWPI5D6JC}Z07?@jG7a4pHL`Y-6BNo`kW-HE^xkON;Ow62mGVrPerSsKQ}r`QfqyoYZhw63B@8XTzfg zUzEARF+SmjZ8d`_T{J%uu@*m+10!FsRA-+{i?{=7zzHo zQs5&2046MfgDC}aN=BZoZ~|>f*7v|?0#`iPUlf_aeq#L**>9M)Sxfk*IMZG%$ka|T zEC`SkO3A!~*i4#Yu+r~^E_+F?79Hj+xxtTLj>0S_5{E0m-UGlNk-*EOF7pdXp9eEi z0_b-X~wm@;14Z{xA|u3LRiJDQDE z*+bnIVk%EVW|&EwPORTI>?r3QMwA=t2x-W?=cmT`5Zh+eMPC!XjibiPIHy6zGR){J zc5hK}QO_3_XT90P`m9GFuc-ov5sXOS%LMLpIIK7TGhOsF;QDykDu80b^~9*+XvQ5d z^^wcldtBJTUgqCdhm}PmGz-uHZ61T|&4&kdb=aZZyyt)KaCT|djSr*vs|ad;w&Sat z9m2HV3~}EOZji!jWzV-57acyG`2FGHI~!evd-u_QQ3U^m1N;|VR}Esa3pG~~B6OZ4 z+&}&j{-y-|jr(lZbTRy_>o?qMf`^NS6sP3CU=Bf1U{6p(LT|o(UxT8ci9pU#z{WRi z0E;dS5P5B@*#gIYS_Q15SiyI6^3O9yNU#CS$-jA@{gwz?n+{Pbj8NoAb$a&CvBGIK znggO^B?B1u3cZ@k@7rfA8s74{f>Q!KULo<{gXhRX^Ek8LyeJxqGZtxjB9}YJF*=lx zo#L$5amV07R~NQElZBuQqTH6Tz&?Ysn>baFGb0p96MK*mIVEhNTDR$fKLZOtG|m75 zlO$WHWzMhbV2np6u-!3Yv=uRy|C;-N59d!?860(|J53^1O$pk2cA~35Td-L6_mYLd z`||HJC;aX^{mgpX>w2Bh?na)qP+;-5)UOG#O%?-{tdw^8EjT;;VEw+d*_VTMaWeIp zVO+1o9~AV~-IBr1)lK`|Kke3P>MaByG!5V+a9SZJpg!$NtS<09psR-+EhPFo^vXyA zB`pzHRXvX9Q#Umr3Jt2lr#Vt$AL{~9xcbRj)G06@)` za8+D#txR;79k9~iLsNj1=B=7bj@~UYU2omHJOqiIp}yAq_hwV=vQlTopJukHpS9CZ z2^#!*czne1jgg~H--K@Opj}p5!<})P@>b7hD+$@j1$sXR?H)FF-AF5@1juyu>wNK8 zYI+jHpulz4Uf`2Ls|bGw%zV-?`!&#LT22enA}&68+hlTB??-)-1Ai9te*j{ceDLnU z%yhpX8_^<%7D$)NkqS3xAKw9arC-Aq>C%CUi98u&JBtSOkuSmacbP2aFM4>)=x1Kq zK9?|L@~SHqGa2^E$VD8d7B{uYC&XM04R%N>$xxU{3tfl3l~CkB8^&(8FNzw*S-sE| zX6!-wBcdMR4-$grT3K-b*j#E3T1+jI4+QE1aOyEd4xcl{R$R0X zeLmfJ9cZFrmu3D-#_~IRyW6+5*8AG%O>>gH3y&3AhwjGGj?%keI$Mm%t zOMh9@n5&3{@S*$)gHO)PVl+9F_c9(;uUaU3oRQ2h#D0H_(?XhWyV>822apkyF z(bx359b%9cbu9k5C&7uKWU&1-MH**#twShmqU*fY+|2TF(77Tf9aQtlBFL+l0x~|L zLxRvk2)UpDrec_vx0AR9h(0b{#m-jC|=Lym}n) zuFg8Yo$#-3W8Ax4qZCzMe1=ox*1)37;4dSlBV0x6^fq=VvAk=ySHsl@AzJQ9#Q_9^ z7Z8*=tJ38m%;^KJDsuZW3K^C#w*nv#9I5?bvJJT&Rq|VNZ-U{cT)4!JI*~mN+bCe{ zJd*IA=gEr(V>tyDF;Gd~9@)njmi9%uOd1zFyjx*O(Y>K1!DK%Lj-0Y1J86Q~!{eet z!$AQvtX5F@j|FPRk-L;rmIVo)p37jVIFB6#y1-sLcxo;{q(LStFh@gL?~Hb%k`PU2 zus=H~RSE6_=rFkeqq{_Rn!GKXIb?=)y)fDs+onQ60ThRsw^Iwla{2GR&unk~xKJ~f zu(^uSZ85J^H=YAKbhY(^0(Mg7?tIDY+w2>Shrb>LGHG>cNA{7Uo9*8{XuY5f5Gda4 z#xAfvW~s11qz~{ohrA0(^$eK$q3UN_I-56};f>@lL=XUlmth{P2joXz9c)}CZwnYQ zy|Q!bW3NB^AW34zi-k znL7OA3yVyPIOa&sboEjnqqpcF0lO#fcz>#RQKPbIqar|xeB0R6v&!wC^ zD{8ZSJPRMjD3vCTyzXY1?R4ctj|vhz3GE3s)7wi}qG8l~UKU2~S*lM)9MWn_c95@M$>TiA_7Pow1vhtqr-DV2iq?rxDb ze{0P*{3AQB^s8&*FcfP8EaevP@2IrgSpK3u_+M_MG6` z{KLWN8u=aL!Z8kmk8dn>Tg8v@Mo-&Cp33)`SSGu6SI%9rlf?%?WQJ@~P_{8cEj6^PYj~Dk$m2L6 z=vjdNBBz<>2Y41vZ#%`)%w&<~oiG(v@wM;X*aw3a+-8wRz2oMv@G>yxTVQ&2@3R!# z&EF5*OKw#!c1TH{`WCq@hsz@Rp!wHK;rNc%T_5lnmwvc?fuZ%?QMxV^zS9pd6TeP{ zyMMgK`%81hU0^Y|GritX&W#m-8e}s9Gj2+#XOWpJ1W!2bV3dyL$w*VWPHzX+0j$YP zL7TA5r2xH1`j2NphuQr8GAj{`=Ut{=Wu01(RWE#?+Z%RVcvM}S=HNYFO)}=JgQ_iI zH8uXq!n=E!8Tt)v&F^;#?=iSr+nqNZ@jTBul8cDxW;z<5<5p2XIT-etx)J4nI9prgoLF;cl1^(|=8);7bi zs7lv6C|4pwJHOuNmgK|j#PF{a#*tyj&;caB;1}!nkxd6Y zSZXA-V7XMQ+J?jz)qG&@_CQ%!5?9NLVJ7PGxBW;z!oa<=o6*X)pq2`A#p=_|#Pl5a z6iPMdhLtkR6g3L=oHkyKH?m_h^v&p>&De7P;`h2#CJX^fbTjBZGD#d8T9JrlE<1*a zdd6m9D0xNi+?hVxjEf?;o{CYd#o0SD^*3gDn)VT{6MQ089I{jF8@Q@nvq99KUMYC{ za`R1N~`tbSW~NN2k?#mGA@=;IgERISOqgSd)O~z){zDPdJf0E0yUPj}Z50~D z*l95Aaj$dYciM4>1kZKzzBJqq-h4K==~|Tq?bRZeaj&AB35mPR@B4q|FnyT#5xI93j(Ot$WGj+V6I}&`&&Us4}5sXEzlG%sgdU zOb+Adv*$1IYAmy92p-kskMGnM^mx_tub}bKT!D#j`JP^E6(K;Gf zj|2_rNqAY|c@T||iOi-@0FXG>KG@tJ@U`-9xIU617aPbib?h^NbtG?kWpNbfM`{_= z-S(me);%gq1N{euIyICZo0kmzPje$CbUVk#Mt*IG)owj~+I^*%JVZw<5El51p_#X+oYeFwXE zjP{>Qm+HAMArw;Ez1Y#e+M!90fz&IONe$Hr@+(39uRyURxt1cHGwQD5^^8L!#YwfV zIaQyYeJ3}%Ii_ZGb7ud#IiN>Ofh#t)(`EVUEnm%Jx>Wm_Bm4XHPh%GGBI?(oyimR^ zlA4#7!F74}8X2|519lKT_~9Z_wmnR4>E3q`*KG3YHbriE_bX<_g_^Mz&7Q}2Rj$(h zsIm9M*X;reD1wFd?L))1j>+#5Z5&AxlIRkcOoBHr3+uht!9eY*x3 za>Mq7i0WqmDLhQkOH!~CSGZzk@g>sd;MiLy`~@=5-Yr`xwE>LoSUR&M^Z9kbvlgI$ z1bzja%L>vOZCFGKgf}cf8LEBgil9-yXxT9e1vY<(PG$n&i2zIug*@8qw;L>Xph+i9 zcH2=;iCKP9$a9{yBcb9$Z1uxQqGo$wW+Y&Enb+=66snqGE)ER*%_mV$@rnOS zQ~nzf5eIr;*}|gc%s)oJB$699u`#2o5x= zXpu#!D``%gEhN~B{wo@y-38})U(r~R`mQ*R zPUj`|Orh9pdH%|XujgkHZkYF1r}wM#F^J`LIcsJy5!9lqTNC$sHL$1b9fdy37Pcf5 z0C;8;st3M8YsEW&mprP z6%@-@-aa%ic3)e!?8OggV2|6q471#fa6DTC{)Phh3sNdz*N`@_0^b_#Z+mHkyudcw z?PZD0SED|gDMyD%%i^zql||&A+W$VQlge&tgIe^Mr;#I-u?ANFmwD~<`khesP4`jO zyPtNt2FUa+$&7R*+;HiyWHk`<`jisTm9VeWg!+JoXG6tgYnar}nC%88ZC(hWA-?GO zoRIxU<}(!r?5I_Pjam?c9&Nuxi+o9NFAy&Lkd78M{(@ivt`@309l|*=aq_m7|BN9) zpq4+zE$B0T!Cm)Kj^LD%J3(d4(zD@BuZ#+BBxP;)jiCoDM&h+gjat_)dhRrP<=CDe zrMfGMa;E!6+PenWSNCqUvz&5j9}Cg?HU{;jFsL^tZLvcO2pA0V9$epQc)yXK77-4> zbpb);A`W+$Q;p28hFb9irsi$vTGx5XO`X1)ol6;ZFRC~_-FhFSwp^ zpOR@k7kW^7T+6^rYuvD|)$?3if|S~>H}eTLdZ8H7pR0^NN4w$!0K3Ez4 ztD-L-_i50VEw!fORq0?zG=0TvmfD^vzZjBw#WgQxIx}?ft#z1g8j;ns1#kS&XzEL> zCHfen||O+YtL0tKvh{3 zRr7b*E4{TABJ^J`*E(pWNNcy#qvURO+w}(aw9`kTM?NRWyA7bficr96RzN+3pg&-> zS3q(yM+RvmXcC%e#L@)>Ly+?1gNI&PZ(bN#>=PA6DC#8!-}Eb9kt;1q_s5-@lQp%+`5rTCeK}{_8O*;i@itV&@Sj+M$ew*2LR> zCm;R;09QXLSnRLun#y;6-|pGaB6;S4YoDHhJ4&Y~vRMEuv6?m6pSYi~W^L;p%)=2U z4mi^&sDW+^@ZpC@&y!+5}>(za4 zmSt%UcEUwJjivprOTu1YFDUZ(H%0aZh(FAfDs=|&3GF> zWS8&n^0NuX3-0cw?q0Jg9sO=P;pXM=?_|QibCsI0H)(CcvlD!}%aAO2?e+XqJKR5+ zYl0luz(*DkP7yG8xXwGJCcJ@`iS^WhYX$Af9Ie@x6R##-!3HEhagz(+{wR;PS~GTB zc!-_1D#G2uaLqadWP5Bm5kY>hBMGJ+=+6?v9sIM~$W%pV66^PNQ|uS)E66?q)Oe9V zJ}}@!k`ClZ2Tpp(DhSr2fc&8vrcLmXp?xX2w#hGF%cwsyQdfXE^*=-oVrJB}*z-yL zFPVsf;in|GPrA#in&vs;V{9It@~?2&){=Q-yF|utn$o0sdz^-1QL(eh_N=msUtDb1 zORkDR*fG#GsIU|FItt zR11(Yqw34LS^4N?yBLsS{zavwDBLN|h7mk)>QNMxkk4H&6)xIEVvclpjS+74PE}EI zhk{U8t%l>aDu+T>Lez=Q+<2AfDUde@!3X&fhzC@NO_fL;#%8jLv8XOFm#a(slZx64_{7CTT^sT$N zGM18Z=`A9KQJUXMCb73K$@x5=pgSPJ9r8UM)YPP-rtp&_kXazFg;?wqy3||-!aqa^0u2*%6KK~au)dMwH!Rg$ zv#bMDF_`&9Tof!<+W$`__Eb=?2iAJsiJgp6YbkZrn2G-G?<;N}UFh8DXObgqo~6X* zm`wM9GF$SMr>xQG^SQT;|9)*}LBb?$u!|xJ+|#>M&Hcd|2MT{cIiWNrhvWgr8z4La zsWe6t96D&pkkt*mG}~rnm&>CuYI#q7E9d+CV>B%$R8E_{6ovCUstyg-GWffzc3>FN z#qx2*!22*i-Fu;6Qlhf>rPhtT2}>8qOs`$W$Ie(97hFF**hV%g!)q05glU{pi~y>o1_M1w3xe;By*vw3tQKKL>V_pe%cUJO7mZHqdV;K=VcXQN!yIAl8l9 zwe0jJ1}7M)w!-}|BDHIU!%t_9uE%CHeiit<$XpvX*<6y@hXg%H zGz*G{9NWj>8wN3`?zM70&!1D%Tw0||2w6JPd5Ar9caOSg@R^?cx&-|B5bxhK207-5 zv`4iG+Ekk6_h$?VJ2w4RF{V=UlH)GP*fhI+awiSOHIc2z0^6TqX1Z*wr8CFX(|CT< z)%UwdqAO{re45^B3PO&Nev(7ztAfd$n|! zvB;Lk$zCqSK6E?V<8pV+md~o1n=N%FNZu{B@NJZ@0n@qTV4`)63)}F>xp#(g{mi0p z1RI%1_3j>E>5=MP7#NkkXdpLaU_dm7L{QA#W6m7hIIXy}RUXet{bPi)*SK4nMmXmlx~1k3`AeeNQqwblfiTuweP-FIcJP`4sHmG~mKu@)Ck*`S1k4fe9fD08JY} zR)ApHY zv1SDhJJFh}fyQY?&k4!amz_dm8YQM`6@kJB-^%UKo{+=x`YR(mx6o(4rz~^cb+K1ZH3? z@kVUDEa7&G?;bTnP&?m1k9?SM0>1C$;sVa|^HDhQj6^&1=+_kX#$(@0fFELpaH#TN zXRk<1!EyjCyS(hC0pervuc9gUxwtniA`5^sgx%XgR7}=8Dx?JJIB7sme# z=wKUwGDWI%SED>$naj$UY^A!mlh+);}0!UYxLI7|Bw-M^zedLQwc9;0pB~ivR&_bQ?;@ps+p&{tE;cY-I5BX#lfSJI-{pX3a9 z0`eKqBVaJA!a{t1&1g15HA7AbL*io!sXNTDO$P=I$VCDcfG^c0L%e6t8tji0r$@Oe&-T%#X6Z0}m5x0H zOHuVjQ|}Ko^5R^|UeN!K-|~Wo_|UtWqbqjkLH)1tLYYTuy)!#d_=ccu0W?7Lu=cE* zMXMvbF(93yUuDmQSj{lb11Qn^HNl~v>r2%Rlg+c+Qk67-gu_xm|`g>IgjkU)ILWR?l(Dx6(=Gb#exX~>B!&6~n zA=t*Axo@3D{%U?oP|G8$Ay0&It5Cx@9?ls&NlGAhpl=6mFC*?-q&+(rVM2CK0iPE3 z+}Bb9J07@ycrMpc^!DRkz`qS;%sX?}jB0^-Gav}&u_mw;>SVwWE77x zNC~NMRQBjdv`p3$;#iWXqmnFH3PrTqvXoAi?3xynQzJE|ZOYVqAFubJy7zv5zrXIi z?#%1F-mlm5^?W{_kLN~%cR=zoPKPb#Xijq=_)+3D}g$sK{OUfBn>eEo2DCA!_Y zVX6RM{!2FAU3&(V<$-cdv~8wK9xT*<-}*Ig?`95@z3Ny?u?(K*ZQ=x=t7hERDpESu zs7g%*B!i9tryiEE(C>zH6CJPmg)R`yFSk?Zminuu^pW53FFFqI|0rFFoQQAjIFUH^ z?MH(`=JZ~$2p)QEsj={}e`x2OxuGh|;nhU@Ekr3Qz|gq%gV2t{OG^9E#ZZ8iioVMH z!e2u0=V^f;pBn3aV*8#?7j_pK4F4Ja53TW!Ng6fkg-hbM+EYF|X|J}Y9XiUNgVs2e zapJqn#UJ~Adbfi6V);s3@16C_m~Da>o~AnZa#`f?<6A4rwmwXnwk|VcB*L3}@^$=( z-oNpeWrA&1aIJ!07jYlOt;p+*Mn}KYWsO6z3_)_6!Rz>KN24#v-i;-P>xKj(rIhy< zm-OonN>#nzQ?S)TKMXBFa_W!Q_#OBoWlrq_wMX-o1@5dknx~V^K5CtL{CVvkT<*S| zhvupbaT+rFquyTT{8IR~Vv)!t*)KX>2ZnlX1X@E6PeE{if(A6Lfc?5An#!tpD6X9hjeJfSV#?L5?STxZ$6jy%1~?4xso?Dp>B=@;IbHkZleimubU zI*+$Ycel1we8nHHvOjN*f4YhBwvm5v@9vI&(8UX>$Bb}zXm3D&?_IV{7S3orx}e zJ+w-9-(IyY)FmuITLr#aL5j?t*=YZQ8R%9=HXpN7IrSuE^1uVjkx!2-IWKc7eMJNA zcW}xTb}g+s|Ilf#!J~(|PHOVKdi$6MF9eQwn&E8tCL6W zq>cM2;o8|Lb8UjNWM)5`{q=?(>RZC_mia~y?BvpDL^cePR1<2Wg%XwRe70THuyhRE zanV!cqR+Lb=N8`4;ILZB!- zSJhnZH!C{Psc2Ov0O8&*O;R$u<;)AK+{W87u5r6Je}j#0Ci5aLV29geWFgwP>7Ll4 z9=DJyg#I5-uI1K{7A5fT;R{pARg2Nz5Ur)p0&yX+Nf7QuADiDM2OuAI!Zbhay|JW! z-N=2?6A?w2{P!?bH*-(QFo$U$BELl%Jr_cXn8$1<2Rf*2R2A*kn$N^z-UOx;jC2trfbolpEuU2T)c`CX4UAT=i2 zv|NA8n)hMSA*-y*L8gp=Lmi`6SHCX4e1KIkud-g2wXFNdW9_Q=w_8OHGoB|++o~LA zOV*l8=%2K6-&WCNZZD4B980GCmYsf}6?aiw&-U7*w$3fZHSg2vmR)tO+Z>yaRamPT zk~0@Af0XMtM;w2$tU;5;exHBp>c2g*Ujx_u@QQYRuIt0!8`l~n#k z6R3-}ek5TV>Fo$dv5JEx3}+4>z7kE^#@93uf)8XTj541q zvLjR|Qm^6e$75!Eh}Kdzy#7l=YXCb*zin7*gQRjZ-IpjWm4Y_CKQB@3;PdLL4M=F$ zj!}rQ3dfj;N}B`t5m;GqQ}y#0I*8;}()fx{$u~DBlN^%5s?5hBiw5y4Y*19Loo1v; z4ZZWsi*!TeePZ`KgK#oXt#(RZ&h78J;mjN*!{7X9rZZ0np^POUb1zz(h=LN8x|4*A zN0I_*jIK9Zx>5x&Hv;h-=ljjVy)-shE_CK8B@@hjNr z2#3JGKbm^XmB;D8k4!^nADzupIgCFH{Ogp$DmS9aILznD5pv|U?n*4w!60@EcMO+j zheP2T?IVR%S7Z^DhtURZM(!WlDH@|M8ZPzs@FLT@YfG41FN37#7v?9cIglfNBDeP7dzxg=W+24^i|5n2s|Q|Y*2S#?E*~N6ui8l&xl9G)TfMgn+?y!aSCSgV{|Yj{mgMKVk}9ca zs36R8LR*&s!ZVMdDrg2I zHX?hC07%ODk|ZaJMkWLsW7ZC7QU{odk-~h7Tr%jZwDb;FcPVbACd6ZYa=>pGku9ZT z{_>zdIviC>p~@|K_~IneA_}+^!ihPiXQa8=1nCTI<)3wTbDs(8|) zX2K7Pp1;dnS3Q`ki*Herb(*w@b5(?!Da2FCeBdV%J^jQ~#>S%o_xHL{-SjX*i$z+f zgF7LEC%)(6x!mVHn~yPXq)-_mPV~0|+TWpV&A0E#AN<4CCMA>D#{TTqBicbH`NY4Y7M*4Sa)qOcVsE~Ao{|ARdczUg?KjMObCHPd<1J5l+&C_ z?kBhElfv-2$nQXqj(}Bwlld+2EShpMpOL?zN*IFx&SB&Lsd*%f)tZHkomrek`n(Uv zAk4G`YJBthDI9%KuASbQEy`$~XnFOGE+x{-r{OUWG%aq7X#>+5FCqlQ!Znfk%&4gi z!>b=L(;!?e+P2}dz~D(l!(+sJU;ew%y(stGq*wRqVIl{fQq)X(%O?ZM0qpiq1D1AI zng%8{KJvo2%J5;A>1IX5EEwB~E020lC*fXPI?;^RatXaHj?!{8c*6jN!O5UWx@1Y4 z2hvGz;>Y52E-ruWfgjtBfi01v7LkqTaX%A37Ux!pet>1J=&yb;@K-pl^?W6a5iF!r z!?*`cwYqRnbH2~}$Lw}*3R*CY2PBEc!7xvFYW$lLVi#KJI^m|~&hY4PGiraL&iFF~ zQ+qLEQe2Lwtr9g&7TxuF)ojZJx^;dgq&6J9bF>W@M1W?G6UkImaS@1nrtTcnN!|gp zpvb1ot{AlAL^3_!+Y*)Cd}dGWD^C9cxu(0W25~imm=L_|@0E0eos;E;KgdS25##tM zi$;D=^mvgNI52L#z_)x4Pn|>0DI^>OXq~7-5w!}N5FCz7s>&DR2^7hIQiCMF$nU?S z!~Y$tzqgJ54sHB*Fc6dc4xiNd76wx$*02Kf3`%9ni@@_45C7Q4a?ON>xM+M~EL6T< z?0FP!YL!9KaY`&CY8b^&#xRQ;gRc7-QhF|Nj;Xb1%j7a_0OS=7H*zXsNjps&r-85` zL&&~c9#ogl@W51Q@}Nk^Ju-7uCwP8>ht5SR5E%6=p>Vyf8?Fj7OLwTz?>q^F_-)Yn zmu9e1ERiV_ty@;&Q7*68Fj&FGQy(87=8^YL4YX z-CmPOer#Am7SX0i89*rvMu-wguco?jm={&KLhTWTZl*Cbxf#YdL%X8ejVzzqxDohX zc`2NWFMu)PkSc@aP~eoB5U*i8V%av4PNQ>bP~6~>rwT3soyJ%My^I&J(VpeQD4}#W zAEmo6O6d7QwU4BRlaNktXS%{r+{-r?zg2!tmE6f2`1+@jgP5Y7xZH`UJY}f9+E57L zRdp#17e88V8f&<48d(Fn96U8ukaql;JPSp78)`8^kspXmBZ0_bblS8=3n+iYb>g3O zoW#rtzL$7M=9d}I&qF8{5TUU6rYoufRz{6ompB*m)VoHfT+`+84RDksa2SW`HQyY@2Ee^rS5vdCm5u|z8v z+3G6h(}R;g?vpVS9IWmM{$(oCQUfx-ls^lZk4Z!%cxsA@2tE^uC<*~52v|{JH)bsX z51W~!cO}jfq#^me7n4eQDkqis+1YS1nf*N6l-akNIxWPza{6(x$ep7kI;lkI+7I1v zgL`2=rV;yrQ9L9c5%!~gra7jN|9Nr^DT1Ud6(Kz-OQj5W9xl|;XNMsvGd#x3bpwOB zerJdP#zFv59JNvB%|@L|@8P8?x$0Qa@-gJoAt2#x4FQ+iFOWW&Wd9((& zRR?J0X)Den2oiJ<$*Vd=bSynUM8AHF|AgBb?*G>b0jL<`8SJ^rXGy>62mE6S}_z(*5p7oPhBBr_D4`=G`VnsBNnMmSd zMY>Puf7GJ97;#If+hPrdDJT&n=XAWMHN_w>Mpkm3ESAiE~4w@!|zJarAhk<<|VEbtM7PJQZz#DX40N zU@9UL7hq-YE21)t|2yvd-(kBSS$>`jBZxYea4PZtIH`;Dp@Tnn&zw3B-FL6MgSiaz zDVQdVd6Y3;S9DnY=ctaBq^ZVR(J~|A6e4l}zdR7b$bJat@NeH06nll?#OL%@)1m|Z zovpL)CuHB)OKA*nu)`_c8y3I;LNmM7C`_sciHRv(v{Sz%i4u+&twm_XLzKfv#oXth z&66zw+_pa9fv=?a8nmzSlhRTyw`%0A(auSc8%K1a-y~1v1#-os#eZ1}R8zCJMVNd5 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

    |er2b{n3}=;D>%qxOfIhLn{50J{-; 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 B!Vv%f literal 131716 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%0A*p)vsoPH2?^_^>HH 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%0A0XX(B(F zvbXMKchk5-Ph7LpShJb@#EMeL;LQmKOk77y+ge4Zyd0Kp0o@oiQ)*HbvkW15S4hOn6+*r$4e%D7uZX5Kq&F5SD&+F0I^0`X^4gUMy zQ}W^;_jB7|u0hgU8IAwfy;m;D@A)XkZNs|u)!Bt6Vw_BxZyud`Vqzn~S=u$j&23|m z3DD9Vu8b$Y6a&u~`f_F>UZt(;?>rNmAlV-{vBP!$hlspeKo3;^@ppmDNQ2w~>ew7@ zfATW&{o}y4 z8M*3b;%k{o901HoD=&w7+L;6UQ;x}rJ*3T$ylsngs!$*Qkg(}=*z+|X2ip^|52W>L zE6q8#@9gnla562>Qlrk_Nd9I081Ryk+KzZg0@76Mhm;1uIjJLOlT43*S(0CtpXMv*Hj$uA3@iN5ZcI*@2?E#>ns| z-@9{s_}Au7p#l(wLu$CNpn39o35OfUbG~JppyOfi^^p4j*@q!B#u-~f2_H@gIzD^3 z`vbxmY{7y?H=?q*YTs}c?Lzhk#Ziy%T6d4 zW5O6U^g=OMaRr>a`OvyeDAxjrx^eqc%S}>CAs|y#yIP$odZ&3ueLL z{qv}umv@D{x2RF@E6d20v*r`xYmE@f-ExNB>O?UXO6%Sn=3FG_by$@?ypaEY-C@D2 zk+ljz=hKDM$X6^0&a&5@g(=KDc*mqXISlibGn)DCmoWR{3R zD_)<*;&FXnsUpJ35|_49B93+wd>UErS5q5Q^5=Qp>OblupJt3c<_ItN9KA9>aK(9k zGA>9?v-z|1=B`?0$arNnsPu>ZgE2->llpH!SXzs_`t<}x#UXr!Rv1foIGahh%*hh@ zv>2JUPj|Soj2*~N5lBd_?ICm2$^%J5YI9Tx^5v>$&TxIYtbKp>fZLVXdD&DSQK(a& zG3v=loTKt$2H~|)fW;|zK@@E037rpksTebkAO$*lM9eT;mk-<(hnLO@RX%7S4rOTL z!eDb`3r^z1x4jp2lms+9*O2x=RuIBJ(#44Oj6LGq#nt2(Iped3!P;Eyuh(1^2oZw9 z-;sg}zwzURa9y>JD2)EMDK0B0^p{ZL!glPt!feq*{gh`2C0qs_d5yPNl`;_+4b?e#5O8HLbhgpVR5o!OiK36bcr8X z2JOw12Cn_nLD|kDsQWZxwg5-Bu5l_>N`Nb_>C^MVuYZNNUSew zfh8Wch-4XtPS;_i?U_x!QPU3VSJr7}W1^OZOQFGwW}?yb1=@-AO;yH?;KhYH+}$9w zp?)aKfY{P3E8?)xeV1q!gi`miZNPd1O#j_=AO936tkc-@mal7?8#2)(QJ;My zqW8~53A#r{R$kMwFlHB8h()2YsEeayhJEnlBstPIY@&_`+-xI#YEfy#UCa8N4WVOJ zSX2-}W?^HpBcda0?h?@~r*6MKEgy+%C=FE)olYf3IM5m$1SGQj0|%;Zqz-R~R=0qf zPp&7vF5|`?Tnf;gYiu9J+pdTZ`g`!_KNYaC;F~t5d|6KGlpwTG=*lEH%F3SUv-^X6 z*hXFNWSVH`Er;NlY}IHadeRCzZG^I|L6W_Rl{o}kxckO{1V&QCVC9wH%I^NiSW7)c zfVr3sl)JwemVj2l%RIme03obqB-R(ma%8y^)@*F@c->5c%W)y)Sf}s3od|?TnMqV-u zdb=q}Ve!`5eRC4D5)7u{JMF_}hl#1_U>QrEV*(whHP2p$=b@CpOQTLYFs=qK6{$u^ zWb5t9Mon$_EfSk?cd28rh4uFe*!moT^a5kjGMdin{X%R(Gj7z7W16w?>2^P<43rx@ zzNM4Uc&5s4bIAt-b((^+|5AF)!3?7<6C)u^2bQYjo5hsw9jbnFP``oPGDj+t$f1V> zQ=U!ZCg`G3ssvW*;^&6DL@+v8P&I5Va^j4}TIj!v z%hpCd67kBmSSe}kNKGwY{%NnV3!||lnZm4}s+6WLnn4WE1JWs#dF{?d*P8K_^m8c? zw8BoMXx+Q)8+KD?abSWttB}pVT~vR!h<=D13F+z(Od_I)U8VZs3ycv7oEX(;k0BH9 zO>DCqk!aX_Ur$^fqcxTh+eeA;#Iao}w>}s6%~N4J>I_594uCQFxbZbTEcS9?RMxO| z)Tphcu!NTpKowm)YS!a4ae9FZ0(#m?wJuP2!xk8wVy^QpG$R!_SQXo#3>jQlKN~AE z@h9|o6W=x{U~e~zL=?i%NA`wp#w>njGKeuKI<_GiCX$HWxa~)+r$!+cY85w^wHJh7 zjCeg<o0uK!!K-2N3uadd%?$q z*+ld@Zf>oy*@4yT5IM$rCyEV_u%GGMUWNcDfv$p>r5YXl5pF0+sk3LR$vsg!^vPgS z63U5{N5>`8MI#hskxF~@%Z&6H@0mb8O|fE$20zON&)i=8q)5jqtB&qoU`#~g=nl1- zk6HQixT%hu-B(bwmt_mMVchc|UlGB6AHn(3Az7`3K}qpa!k%-(uMWPsIo-b4-eV~U zC>@ciuvg+riIIxefCkeoP5l~brSs1@PnUNeWGSU3M)-@o6_w%=rn-iLVU+b)7~_yW z^6$4v{eP4W@3R?h7Iahts^8vvY8Sr5MgT~*ITXIZ_K~Fu=0VIE~@x*pu63 zMd+9%^YtN7j6}oIgwYOF<{Cy&m6gsqHnP0zP5j&8&CiA_dUTvcbl5MUpQTwEcB{SA zsAJUZ-REin+Du+5!ab*|M7#prkuDkCBVIbPx6h$UQe0(tr| zZzf!JYI{`}<#NvGmtqX!0V?Rb2-52 zO(UZL6|I@(>*_*L6*fi>dgi)_;1rYM9y-U0+foDmDgc_)(bK zx3;PwZe?cN89XUR->$m>2#|FFE%6-zMpoAqaKedbrIFRuLaZrfBXaTen1Lj-H%L|( zvp(-2lpeYi<=6K`BFX|rkfnrD=E|0)J~|Om!?;K)e0SPdjr_>srYY_j2H|5}69~6P zuF=YZC!MW>z;BJai*-=1vo(4xs?2Zl1o$_o@UE0EZt%Jc-ZDGq*Z6U|Zi@!NO265Z z#f;h3#N2K@k^aKBOCe#F;)Ng2METASQxHaIN+J5H!Pvf}_-1-UzIsb``a*xg{slds zjudX|I6D9b7-hASJ8f+E ziashC-KphJd05H5TvE%fT)ntH`1Ja^a)0dvDCo^zm1l^1lajKRcHhlxdbGY7j4oYA zEj%zmHhAk%-yCA5wjeN+PDnhblzx6`Ls#~sZHmvB?5ui*-Hx{SeiWQi=3>@gP92tg zt@$O~X3JnnXFk0%_^X^}_PHU@CCHAX(lTbg1=((Qs`q!Qqn8U0U|9 zfkt|vtlxzGqGZ>wQ=F<4F-g`r{z%WWK}58Mix0kvWp@k4ol`Xa2tAIheu#WY^C+;AMh(?2!lM%%-1HIZ~pY~p)1~o>Pi@|)7JhQ&&%fK)|}q8 z@2U&C1Z480HTt8aO0pe#)YwVOnp9%Dw_d)HFAu z2eVNcXIZ~ch+{2A^+)vYrAGPTt0r)Bf>)x~(u+l7(|v>lrFAE{i3IOn({JrIj8025{);Vn(nFJ@{Ep<3Xf(2%k+z z=AWa|_=c*r8_t~%kKP~5Pr?0_tkO@mm<^Z>9_>#ZZqD_0>;urr>J|BlT+9yFFWvlm zp_+2~(^{(-Zup$ekz&J*nX^l+H7f9p_`o4*zRgW|j&8P3EP9MJCDEGu8 zYLa=+ZQ%x9GK7STHCa+UT0qr`by*VKHW27-8z_Fot`Ok=nNC$D23F+Ew2FVZ)9;lk z$YNs9lQ%%yUbAzvXK8b`=llSba%K#pTUzuov0F!N9ynMb%3@bLEkQD>Ge!n-)L^sj z!i$iVN4TkgBHVauGOjkYIr>;2-@v^IEbX$dplyG>fft-bDRW!;T;}F~ODCYGwG4$j z+>6oEE)`H5W2TkW(B7B1_@CDC?wRwWrM*gpKe__*ADTLrDkG?feeM><$~y$=qdZJ) zQtoNjS4U@EC{S1M=@>sBVx)r1&c5tMY|Zu^KHG1Wn7`|Td7Lff=}J0G*=yDSOG4`P zt(qT2&dh66oukpd%&(^_^thC))c!_46wgzc0FL{94FKbwl6xeZHNpi(RIy{5=Dyv9 z5k)HXYIgoDyu@X}4#9MLzbiGQtm%l@b^`-DzrHHJrCWM9y$-`;c79D&e#}{0+(J)P z#LaV~lI=s_l(X9FkR=n?RJBHYEEC%c+9f+isv4ZZbjEd;(J-#u#4U2FirID=ZRPg( zWg$m>HaY#_enahxOzF}WZ>Y2)?3$H>IN@gRL1L39VYknlxpBXq-t>qf)#Ivm{=~tp zSV>kzcluN7GwCZ=JnmmRoJ2*K(mrxv?%(7x-7bb^c_VkLmMU)w?2(2tRQ zB&B}(o**tD9lo;!)xY7?q1PcGkyBTB&o%J4n~j^3^WUy6* zBBt?Ea(qVJ_m$ zf#jjRS`A-9* zapu2~D_cv$!*5nrzc_VlcmQq?;E@`!u1JD^ppM6|vkr3Qgtblr<|4nN;a15+g!$J= z6}4sxXegXBWAX20`*g>^AV)P$nC;(t>N*Q9>iqAEK3)7^!?K(g>(gVDXJFhmK{KwR z(D|Hi>;Bs1Div$xFqPR)=lDpzE6=`fo_kpJcIXK68PuO>33N^=E66a zo4+I3P^-iM?8qdN)VXc+5I=AK4+sWGThhM0!X@#AyhT)-((U8P{D(6PY_F8;Q2f%} z`g!>`ZkrVZH~7GJn0JXGs5BNI91=UXD0U61JS=um?GY(R?{+a<_ZC>P>3=d#RvtWq z#53YNnT)z|fPC^ToaVzcEtti_>vq$c!u zxzJ1{2s^#X4^=ItO!hhy>EGxZ@NcH+G!44E=xhNqS`5COty@!5bYUMfl9Zw}T#`sB zd5L@wVo=@G$|LSFrmm;5=|IBZ`x6r)@`cIZR^)d;cYwitDT$T`l`R(oXAg#p=g+afxPMQ7l!)uIBWYY2CHUx5a@}%w2lI?> zLK&vtJ;VJVDEfMUKOpsXvpZ%VgqQxNO~V%lZgig|l7yv4bRawr04mDeuwEr4^bDJ@fyC;Xf^mt!`2PeX|QFV)d^6AeQ?Bk8n1 zk>r4RuD42C&9}Dh&O%VqxjkHtvh155B4(vL-ptX-=y_WwaUF9=xgbfVH@`u|`=8>n^Nj$v@?U_4U0F;FQUO%usIQ^x4n5p&@o+)bk#% zik_H~3}|BVNE>7{#Tl%cFva^wuP3rmSEAupH?|OzlVt6{zH#RO2MybPSv126eHor` zy^S-rCCBCArynd?FC$Gvk7=3LrdQU*j?+;23~dKlX+-sRt!i$ik*jM z_GOiEzrDy#DuaQpQ01Z<7+BzOchI1j;>az#auB%n{r)1ye`#OY$uhm?iE4K|% zApn`3W$VDUSWtGbuWs7<%}b4Vmn4Jc_I+j@O11*x$x0_0D{)Z<<106QDL!iGBiBQZ z%SU8->)8wZkyyZ#gNg{0VAx{)s1^k}(R^Mi9r>x)Mf|8h%u z-o9RQTZv$+gtD)#x08!q71D%y0Kx&JX&=zjyyVM}^>1uVDj1kdrj(r*SIvC6m|Bud zxQTziru<-*^3a(oSsBf%RySLJx?ok+6nw`phEgBjjB&osy6W|tZyKzlyTT3E)GX{O zK0ChD=u-Zr8B-y}lM!E7sz|Vn*ZdIf1CaKo%9=m%hL?1;9D$S$emg*IlYwhFYvUr0 zBbk@4*Jm9sK)>qF&Kth|Ffwn|5H6lXLNa1gDJWXH%7j5{_nMNuP42z6iHeb}efg*w zxDQt~8q&3y>Rfa}LqfCXl2Wxd)N>lR)erElyP(<=3eTakPi&d>u6VRks6AFqj~mP= ziKhL_(U=8z7ON_-G7g%366-i)rut=DCkh-x`&-O^ z<8mBA+!26!*wx>*0)d11@ZScf;@3Um;kwtUKUW@dCl#yLr96;c_#3o1CP zP?%vDrj_BrHZ6$6Q8bB)Uc5VJ!dQBjpK6Q-PykrZao+IjT``AiZ|8_kv0dPAt|Txp zMNrxAh>L)KY#1E4EY?JYI;?zEd%7(R&=S;lmve>dBCs)o`9^eIgtT$`Kh$^xFr%wLPC6grpYVaAmlk>V9HzFK z%BYIk4|OR8`@kchykrZ!A;Z_vt`_dN=LdROxZmJ`OcWkNof2%uew{-H6&%r2=L Date: Sun, 3 May 2026 22:36:03 -0700 Subject: [PATCH 05/28] Slice 4: dynamic-controls calibration + rules-aware fingerprint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Helper: tree.fingerprint RPC computes a SHA-256 of the (filtered) UIA subtree, with optional dynamic-control rules that mask `value` / `name` / `toggleState` of matched controls. Matchers: automationId, exact selector, glob selectorPattern, container (subtree + controlType + optional name/className regexes). TS dynamicControls.ts: calibrateDynamicControls runs N tree dumps (3 by default, 3s apart) with no input, diffs them by selector, and emits DynamicControlRule[] tagged `calibration-drift` with confidence = transitions / (N-1). Persistence (load/save) and rule-merge by matcher identity also included. Smoke validates the mechanism on Clock's running stopwatch: - Back-to-back fingerprints identical (deterministic hash). - Applying a rule that masks Close button's name → different fingerprint than no-rule (rule application affects the hash). - Calibration picks up StopwatchTimerText as dynamic with confidence 1.0 across 3 dumps. - Naked fingerprints diverge across a 4s window (timer advanced); rules-aware fingerprints partially mask drift but don't fully (Clock has multiple time-display elements at different granularities). The residual drift is a real-world finding to address with iterative explore-drift rule updates in the autonomous loop (slice 6). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/Methods/TreeMethods.cs | 33 ++- .../src/Models/DynamicControlRule.cs | 38 +++ .../src/Uia/FingerprintComputer.cs | 176 ++++++++++++ .../src/uiCapture/dynamicControls.ts | 259 ++++++++++++++++++ .../onboarding/src/uiCapture/helperClient.ts | 19 +- .../src/uiCapture/test/calibrateSmoke.ts | 226 +++++++++++++++ .../agents/onboarding/src/uiCapture/types.ts | 51 ++++ .../uiCapture/clock-stopwatch-dynamic.json | 27 ++ 8 files changed, 826 insertions(+), 3 deletions(-) create mode 100644 dotnet/uiAutomationHelper/src/Models/DynamicControlRule.cs create mode 100644 dotnet/uiAutomationHelper/src/Uia/FingerprintComputer.cs create mode 100644 ts/packages/agents/onboarding/src/uiCapture/dynamicControls.ts create mode 100644 ts/packages/agents/onboarding/src/uiCapture/test/calibrateSmoke.ts create mode 100644 ts/packages/agents/onboarding/test/fixtures/uiCapture/clock-stopwatch-dynamic.json diff --git a/dotnet/uiAutomationHelper/src/Methods/TreeMethods.cs b/dotnet/uiAutomationHelper/src/Methods/TreeMethods.cs index c168c8be96..04178adb3a 100644 --- a/dotnet/uiAutomationHelper/src/Methods/TreeMethods.cs +++ b/dotnet/uiAutomationHelper/src/Methods/TreeMethods.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System.Text.Json.Serialization; +using UiAutomationHelper.Models; using UiAutomationHelper.Rpc; using UiAutomationHelper.Uia; @@ -11,7 +12,8 @@ internal static class TreeMethods { public static void Register(Dispatch dispatch) { - dispatch.Register("tree.dump", (p, ct) => Task.FromResult(Dump(p))); + dispatch.Register("tree.dump", (p, ct) => Task.FromResult(Dump(p))); + dispatch.Register("tree.fingerprint", (p, ct) => Task.FromResult(Fingerprint(p))); } private static object? Dump(System.Text.Json.JsonElement? @params) @@ -19,7 +21,7 @@ public static void Register(Dispatch dispatch) var p = RpcParams.ParseRequired(@params); if (string.IsNullOrEmpty(p.Root)) { - throw new Models.RpcException(Models.RpcErrorCode.InvalidParams, "'root' is required"); + throw new RpcException(RpcErrorCode.InvalidParams, "'root' is required"); } return ComRetry.Run(() => { @@ -28,6 +30,27 @@ public static void Register(Dispatch dispatch) return (object?)TreeWalker.Walk(element, p.Root, depth); }); } + + private static object? Fingerprint(System.Text.Json.JsonElement? @params) + { + var p = RpcParams.ParseRequired(@params); + if (string.IsNullOrEmpty(p.Root)) + { + throw new RpcException(RpcErrorCode.InvalidParams, "'root' is required"); + } + return ComRetry.Run(() => + { + var element = SelectorResolver.ResolveOrThrow(p.Root); + var result = FingerprintComputer.Compute(element, p.Root, p.DynamicRules); + return (object?)new + { + hash = result.Hash, + controlCount = result.ControlCount, + activeWindowTitle = result.ActiveWindowTitle, + focusedSelector = result.FocusedSelector, + }; + }); + } } internal sealed class TreeDumpParams @@ -36,3 +59,9 @@ internal sealed class TreeDumpParams [JsonPropertyName("maxDepth")] public int? MaxDepth { get; set; } [JsonPropertyName("filter")] public string? Filter { get; set; } } + +internal sealed class TreeFingerprintParams +{ + [JsonPropertyName("root")] public string? Root { get; set; } + [JsonPropertyName("dynamicRules")] public DynamicControlRule[]? DynamicRules { get; set; } +} diff --git a/dotnet/uiAutomationHelper/src/Models/DynamicControlRule.cs b/dotnet/uiAutomationHelper/src/Models/DynamicControlRule.cs new file mode 100644 index 0000000000..72be3849a7 --- /dev/null +++ b/dotnet/uiAutomationHelper/src/Models/DynamicControlRule.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; + +namespace UiAutomationHelper.Models; + +internal sealed class DynamicControlRule +{ + [JsonPropertyName("id")] public string Id { get; set; } = ""; + [JsonPropertyName("match")] public ControlMatcher Match { get; set; } = new(); + [JsonPropertyName("dynamicProperties")] public string[] DynamicProperties { get; set; } = Array.Empty(); + [JsonPropertyName("semantic")] public string? Semantic { get; set; } + [JsonPropertyName("reason")] public string? Reason { get; set; } + [JsonPropertyName("confidence")] public double Confidence { get; set; } + [JsonPropertyName("observations")] public int Observations { get; set; } + [JsonPropertyName("firstSeen")] public string? FirstSeen { get; set; } + [JsonPropertyName("lastConfirmed")] public string? LastConfirmed { get; set; } + [JsonPropertyName("notes")] public string? Notes { get; set; } +} + +internal sealed class ControlMatcher +{ + /// + /// One of: "automationId", "selector", "selectorPattern", "container". + /// + [JsonPropertyName("kind")] public string Kind { get; set; } = ""; + + // automationId / selector / selectorPattern + [JsonPropertyName("value")] public string? Value { get; set; } + [JsonPropertyName("pattern")] public string? Pattern { get; set; } + + // container + [JsonPropertyName("container")] public string? Container { get; set; } + [JsonPropertyName("controlType")] public string? ControlType { get; set; } + [JsonPropertyName("nameRegex")] public string? NameRegex { get; set; } + [JsonPropertyName("classNameRegex")] public string? ClassNameRegex { get; set; } +} diff --git a/dotnet/uiAutomationHelper/src/Uia/FingerprintComputer.cs b/dotnet/uiAutomationHelper/src/Uia/FingerprintComputer.cs new file mode 100644 index 0000000000..dedc210672 --- /dev/null +++ b/dotnet/uiAutomationHelper/src/Uia/FingerprintComputer.cs @@ -0,0 +1,176 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Security.Cryptography; +using System.Text; +using System.Text.RegularExpressions; +using FlaUI.Core.AutomationElements; +using UiAutomationHelper.Models; + +namespace UiAutomationHelper.Uia; + +internal static class FingerprintComputer +{ + public sealed class Result + { + public string Hash { get; set; } = ""; + public int ControlCount { get; set; } + public string ActiveWindowTitle { get; set; } = ""; + public string? FocusedSelector { get; set; } + } + + public static Result Compute( + AutomationElement root, + string rootSelector, + IReadOnlyList? rules) + { + rules ??= Array.Empty(); + var sb = new StringBuilder(); + var ctx = new Context { Rules = rules }; + WriteNode(root, rootSelector, sb, ctx); + + var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(sb.ToString())); + var hex = Convert.ToHexString(bytes).ToLowerInvariant()[..16]; + return new Result + { + Hash = hex, + ControlCount = ctx.NodeCount, + ActiveWindowTitle = root.Properties.Name.ValueOrDefault ?? "", + FocusedSelector = ctx.FocusedSelector, + }; + } + + private sealed class Context + { + public IReadOnlyList Rules { get; init; } = Array.Empty(); + public int NodeCount; + public string? FocusedSelector; + } + + private static void WriteNode(AutomationElement el, string selector, StringBuilder sb, Context ctx) + { + ctx.NodeCount++; + if (el.Properties.HasKeyboardFocus.ValueOrDefault) + { + ctx.FocusedSelector ??= selector; + } + + var dynamicProps = MatchDynamicProps(el, selector, ctx.Rules); + var ct = el.ControlType.ToString(); + var aid = el.Properties.AutomationId.ValueOrDefault ?? ""; + var name = dynamicProps.Contains("name") ? "" : (el.Properties.Name.ValueOrDefault ?? ""); + var cls = el.Properties.ClassName.ValueOrDefault ?? ""; + + string value = ""; + if (!dynamicProps.Contains("value")) + { + try + { + if (el.Patterns.Value.IsSupported) + { + value = el.Patterns.Value.Pattern.Value.ValueOrDefault ?? ""; + } + } + catch { /* ignore pattern access errors */ } + } + + string toggle = ""; + if (!dynamicProps.Contains("toggleState")) + { + try + { + if (el.Patterns.Toggle.IsSupported) + { + toggle = el.Patterns.Toggle.Pattern.ToggleState.ValueOrDefault.ToString(); + } + } + catch { /* ignore */ } + } + + sb.Append('{') + .Append("ct=").Append(ct) + .Append("|aid=").Append(aid) + .Append("|name=").Append(name) + .Append("|class=").Append(cls) + .Append("|val=").Append(value) + .Append("|tog=").Append(toggle) + .Append("|kids=["); + + AutomationElement[] children; + try { children = el.FindAllChildren(); } + catch { children = Array.Empty(); } + + for (int i = 0; i < children.Length; i++) + { + if (i > 0) sb.Append(','); + var childSelector = selector + Selectors.BuildSegment(children[i]); + WriteNode(children[i], childSelector, sb, ctx); + } + sb.Append(']').Append('}'); + } + + private static HashSet MatchDynamicProps( + AutomationElement el, + string selector, + IReadOnlyList rules) + { + var matched = new HashSet(StringComparer.Ordinal); + if (rules.Count == 0) return matched; + + foreach (var rule in rules) + { + if (!RuleMatches(rule, el, selector)) continue; + foreach (var p in rule.DynamicProperties) + { + matched.Add(p); + } + } + return matched; + } + + private static bool RuleMatches(DynamicControlRule rule, AutomationElement el, string selector) + { + var m = rule.Match; + switch (m.Kind) + { + case "automationId": + if (string.IsNullOrEmpty(m.Value)) return false; + return string.Equals( + el.Properties.AutomationId.ValueOrDefault ?? "", + m.Value, + StringComparison.Ordinal); + + case "selector": + return string.Equals(selector, m.Value, StringComparison.Ordinal); + + case "selectorPattern": + if (string.IsNullOrEmpty(m.Pattern)) return false; + return Regex.IsMatch(selector, GlobToRegex(m.Pattern)); + + case "container": + if (string.IsNullOrEmpty(m.Container) || string.IsNullOrEmpty(m.ControlType)) return false; + if (!selector.StartsWith(m.Container, StringComparison.Ordinal)) return false; + if (!string.Equals(el.ControlType.ToString(), m.ControlType, StringComparison.Ordinal)) return false; + if (!string.IsNullOrEmpty(m.NameRegex)) + { + var name = el.Properties.Name.ValueOrDefault ?? ""; + if (!Regex.IsMatch(name, m.NameRegex)) return false; + } + if (!string.IsNullOrEmpty(m.ClassNameRegex)) + { + var cls = el.Properties.ClassName.ValueOrDefault ?? ""; + if (!Regex.IsMatch(cls, m.ClassNameRegex)) return false; + } + return true; + + default: + return false; + } + } + + private static string GlobToRegex(string glob) + { + var escaped = Regex.Escape(glob).Replace("\\*", ".*").Replace("\\?", "."); + return "^" + escaped + "$"; + } +} diff --git a/ts/packages/agents/onboarding/src/uiCapture/dynamicControls.ts b/ts/packages/agents/onboarding/src/uiCapture/dynamicControls.ts new file mode 100644 index 0000000000..9e37b46d50 --- /dev/null +++ b/ts/packages/agents/onboarding/src/uiCapture/dynamicControls.ts @@ -0,0 +1,259 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import path from "node:path"; + +import { HelperClient } from "./helperClient.js"; +import type { + ControlMatcher, + DynamicControlRule, + DynamicControlsFile, + DynamicProperty, + TreeNode, +} from "./types.js"; + +/** + * Run a calibration pass: take N tree dumps with delays between them, + * compare for value/name drift, and emit per-element rules with + * `reason: "calibration-drift"`. + * + * The app should be in a stable, observable state — no agent or user input + * during calibration. Anything that changes value/name is presumed dynamic. + */ +export async function calibrateDynamicControls(opts: { + client: HelperClient; + rootSelector: string; + integrationName: string; + dumpCount?: number; + delayMs?: number; + maxDepth?: number; +}): Promise { + const dumpCount = opts.dumpCount ?? 3; + const delayMs = opts.delayMs ?? 3000; + const maxDepth = opts.maxDepth ?? 8; + const startedAt = Date.now(); + + const dumps: TreeNode[] = []; + for (let i = 0; i < dumpCount; i++) { + if (i > 0) { + await sleep(delayMs); + } + dumps.push( + await opts.client.treeDump({ + root: opts.rootSelector, + maxDepth, + }), + ); + } + + const rules = diffDumpsToRules(dumps); + const now = new Date().toISOString(); + for (const r of rules) { + r.firstSeen = now; + r.lastConfirmed = now; + } + + return { + version: 1, + integrationName: opts.integrationName, + calibration: { + lastRun: now, + durationMs: Date.now() - startedAt, + dumpsCompared: dumps.length, + }, + rules, + }; +} + +/** + * Index a tree by selector for cross-dump comparison. + */ +function indexBySelector(node: TreeNode, out: Map): void { + out.set(node.selector, node); + for (const c of node.children) { + indexBySelector(c, out); + } +} + +function diffDumpsToRules(dumps: TreeNode[]): DynamicControlRule[] { + if (dumps.length < 2) { + return []; + } + + const indexed: Map[] = dumps.map((d) => { + const m = new Map(); + indexBySelector(d, m); + return m; + }); + + // Find selectors present in ALL dumps. + const common: string[] = []; + for (const sel of indexed[0]!.keys()) { + if (indexed.every((m) => m.has(sel))) { + common.push(sel); + } + } + + const rules: DynamicControlRule[] = []; + let id = 1; + for (const sel of common) { + const nodes = indexed.map((m) => m.get(sel)!); + const dynProps = detectDynamicProperties(nodes); + if (dynProps.length === 0) { + continue; + } + const exemplar = nodes[0]!; + const matcher = chooseMatcher(exemplar); + const transitions = countTransitions(nodes, dynProps); + rules.push({ + id: `cal-${id++}`, + match: matcher, + dynamicProperties: dynProps, + ...(deriveSemantic(exemplar) !== undefined + ? { semantic: deriveSemantic(exemplar) as string } + : {}), + reason: "calibration-drift", + confidence: Math.min(1, transitions / (dumps.length - 1)), + observations: transitions, + firstSeen: "", + lastConfirmed: "", + }); + } + return rules; +} + +function detectDynamicProperties(nodes: TreeNode[]): DynamicProperty[] { + const props: DynamicProperty[] = []; + if ( + nodes.some((n, i) => i > 0 && (nodes[i - 1]!.value ?? "") !== (n.value ?? "")) + ) { + props.push("value"); + } + if ( + nodes.some((n, i) => i > 0 && (nodes[i - 1]!.name ?? "") !== (n.name ?? "")) + ) { + props.push("name"); + } + if ( + nodes.some( + (n, i) => + i > 0 && + (nodes[i - 1]!.toggleState ?? "") !== (n.toggleState ?? ""), + ) + ) { + props.push("toggleState"); + } + return props; +} + +function countTransitions(nodes: TreeNode[], props: DynamicProperty[]): number { + let transitions = 0; + for (let i = 1; i < nodes.length; i++) { + for (const p of props) { + if ((getProp(nodes[i - 1]!, p) ?? "") !== (getProp(nodes[i]!, p) ?? "")) { + transitions++; + break; + } + } + } + return transitions; +} + +function getProp(n: TreeNode, p: DynamicProperty): string | undefined { + return p === "value" ? n.value : p === "name" ? n.name : n.toggleState; +} + +function chooseMatcher(n: TreeNode): ControlMatcher { + if (n.automationId) { + return { kind: "automationId", value: n.automationId }; + } + return { kind: "selector", value: n.selector }; +} + +function deriveSemantic(n: TreeNode): string | undefined { + if (n.automationId) { + return n.automationId; + } + if (n.name) { + return n.name.length > 40 ? n.name.slice(0, 40) : n.name; + } + return undefined; +} + +async function sleep(ms: number): Promise { + await new Promise((res) => setTimeout(res, ms)); +} + +/* persistence */ + +export function loadDynamicControls(workspaceDir: string): DynamicControlsFile | null { + const file = path.join(workspaceDir, "dynamicControls.json"); + if (!existsSync(file)) { + return null; + } + return JSON.parse(readFileSync(file, "utf8")) as DynamicControlsFile; +} + +export function saveDynamicControls( + workspaceDir: string, + file: DynamicControlsFile, +): void { + mkdirSync(workspaceDir, { recursive: true }); + writeFileSync( + path.join(workspaceDir, "dynamicControls.json"), + JSON.stringify(file, null, 2), + ); +} + +/** + * Merge new rules into an existing file, deduping by matcher equivalence. + * Bumps observations/lastConfirmed on hits. + */ +export function mergeDynamicControls( + base: DynamicControlsFile, + incoming: DynamicControlRule[], +): DynamicControlsFile { + const now = new Date().toISOString(); + const mergedRules: DynamicControlRule[] = [...base.rules]; + for (const inc of incoming) { + const existing = mergedRules.find((r) => + sameMatcher(r.match, inc.match), + ); + if (existing) { + existing.observations += inc.observations; + existing.lastConfirmed = now; + for (const p of inc.dynamicProperties) { + if (!existing.dynamicProperties.includes(p)) { + existing.dynamicProperties.push(p); + } + } + } else { + mergedRules.push({ ...inc, lastConfirmed: now }); + } + } + return { ...base, rules: mergedRules }; +} + +function sameMatcher(a: ControlMatcher, b: ControlMatcher): boolean { + if (a.kind !== b.kind) { + return false; + } + switch (a.kind) { + case "automationId": + return a.value === (b as typeof a).value; + case "selector": + return a.value === (b as typeof a).value; + case "selectorPattern": + return a.pattern === (b as typeof a).pattern; + case "container": { + const cb = b as typeof a; + return ( + a.container === cb.container && + a.controlType === cb.controlType && + (a.nameRegex ?? "") === (cb.nameRegex ?? "") && + (a.classNameRegex ?? "") === (cb.classNameRegex ?? "") + ); + } + } +} diff --git a/ts/packages/agents/onboarding/src/uiCapture/helperClient.ts b/ts/packages/agents/onboarding/src/uiCapture/helperClient.ts index 020d3ca3db..77d3a36be6 100644 --- a/ts/packages/agents/onboarding/src/uiCapture/helperClient.ts +++ b/ts/packages/agents/onboarding/src/uiCapture/helperClient.ts @@ -8,6 +8,8 @@ import { createInterface, Interface } from "node:readline"; import { fileURLToPath } from "node:url"; import type { + DynamicControlRule, + FingerprintResult, Rect, Screenshot, SnapshotPolicy, @@ -209,6 +211,13 @@ export class HelperClient { return this.call("tree.dump", p); } + treeFingerprint(p: { + root: string; + dynamicRules?: DynamicControlRule[]; + }): Promise { + return this.call("tree.fingerprint", p); + } + screenshot(p: { root: string }): Promise { return this.call("screenshot", p); } @@ -320,4 +329,12 @@ export class HelperClient { } } -export type { Rect, Screenshot, SnapshotPolicy, TreeNode, WindowInfo }; +export type { + DynamicControlRule, + FingerprintResult, + Rect, + Screenshot, + SnapshotPolicy, + TreeNode, + WindowInfo, +}; diff --git a/ts/packages/agents/onboarding/src/uiCapture/test/calibrateSmoke.ts b/ts/packages/agents/onboarding/src/uiCapture/test/calibrateSmoke.ts new file mode 100644 index 0000000000..644504a3bc --- /dev/null +++ b/ts/packages/agents/onboarding/src/uiCapture/test/calibrateSmoke.ts @@ -0,0 +1,226 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { mkdirSync, writeFileSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +import { calibrateDynamicControls } from "../dynamicControls.js"; +import { HelperClient } from "../helperClient.js"; +import type { DynamicControlRule, TreeNode } from "../types.js"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const fixturesDir = path.resolve(__dirname, "../../..", "test/fixtures/uiCapture"); + +const CLOCK_AUMID = "Microsoft.WindowsAlarms_8wekyb3d8bbwe!App"; + +function log(msg: string): void { + process.stdout.write(`[cal] ${msg}\n`); +} + +function findFirst( + node: TreeNode, + pred: (n: TreeNode) => boolean, +): TreeNode | null { + if (pred(node)) { + return node; + } + for (const c of node.children) { + const f = findFirst(c, pred); + if (f) { + return f; + } + } + return null; +} + +async function sleep(ms: number): Promise { + await new Promise((res) => setTimeout(res, ms)); +} + +async function runStopwatchCalibration( + client: HelperClient, + rootSelector: string, +): Promise { + log("navigating to Stopwatch..."); + let tree = await client.treeDump({ root: rootSelector, maxDepth: 8 }); + const stopwatchTab = findFirst( + tree, + (n) => + n.name === "Stopwatch" && n.patterns.includes("SelectionItem"), + ); + if (!stopwatchTab) { + throw new Error("Stopwatch tab not found in NavView"); + } + await client.doSelect({ selector: stopwatchTab.selector }); + await client.eventsIdle({ debounceMs: 800, maxWaitMs: 3000 }); + + log("starting stopwatch..."); + tree = await client.treeDump({ root: rootSelector, maxDepth: 8 }); + const startBtn = findFirst( + tree, + (n) => + (n.automationId === "PlayPauseButton" || n.name === "Start") && + n.patterns.includes("Invoke"), + ); + if (!startBtn) { + throw new Error("Stopwatch Start button not found"); + } + await client.doInvoke({ selector: startBtn.selector }); + await sleep(500); // let stopwatch tick a bit before first dump + + log("calibrating (3 dumps over ~6s)..."); + const calibrated = await calibrateDynamicControls({ + client, + rootSelector, + integrationName: "windowsClock", + dumpCount: 3, + delayMs: 3000, + maxDepth: 8, + }); + log( + `calibration: ${calibrated.rules.length} rule(s) in ${calibrated.calibration?.durationMs}ms`, + ); + for (const r of calibrated.rules.slice(0, 8)) { + log( + ` rule ${r.id}: match=${JSON.stringify(r.match)} props=${JSON.stringify(r.dynamicProperties)} confidence=${r.confidence.toFixed(2)} semantic=${r.semantic ?? ""}`, + ); + } + + if (calibrated.rules.length === 0) { + throw new Error( + "Expected at least one dynamic rule from calibration on a running stopwatch", + ); + } + + // Save the calibrated file as a fixture for inspection. + writeFileSync( + path.join(fixturesDir, "clock-stopwatch-dynamic.json"), + JSON.stringify(calibrated, null, 2), + ); + log("saved clock-stopwatch-dynamic.json"); + + // Verify rules-aware fingerprint is stable while stopwatch ticks. + log("comparing fingerprints with and without rules over a 4s window..."); + const fpRulesA = await client.treeFingerprint({ + root: rootSelector, + dynamicRules: calibrated.rules, + }); + const fpNakedA = await client.treeFingerprint({ root: rootSelector }); + await sleep(4000); + const fpRulesB = await client.treeFingerprint({ + root: rootSelector, + dynamicRules: calibrated.rules, + }); + const fpNakedB = await client.treeFingerprint({ root: rootSelector }); + + log(` with rules: ${fpRulesA.hash} → ${fpRulesB.hash}`); + log(` without rules: ${fpNakedA.hash} → ${fpNakedB.hash}`); + + if (fpRulesA.hash !== fpRulesB.hash) { + log( + " ⚠ rules-aware fingerprints differ — calibration likely missed a dynamic control", + ); + } else { + log(" ✓ rules-aware fingerprints match (dynamic content masked)"); + } + if (fpNakedA.hash === fpNakedB.hash) { + log( + " ⚠ naked fingerprints match — stopwatch may not have advanced (UI might be paused)", + ); + } else { + log(" ✓ naked fingerprints differ (stopwatch advanced)"); + } + + // Stop stopwatch. + log("stopping stopwatch..."); + tree = await client.treeDump({ root: rootSelector, maxDepth: 8 }); + const stopBtn = findFirst( + tree, + (n) => + (n.automationId === "PlayPauseButton" || + n.automationId === "PauseButton" || + n.name === "Pause" || + n.name === "Stop") && + n.patterns.includes("Invoke"), + ); + if (stopBtn) { + await client.doInvoke({ selector: stopBtn.selector }); + } + log(" stopwatch stopped"); +} + +async function runFingerprintBasics( + client: HelperClient, + rootSelector: string, +): Promise { + log("fingerprint basics..."); + const fpA = await client.treeFingerprint({ root: rootSelector }); + const fpB = await client.treeFingerprint({ root: rootSelector }); + log(` back-to-back: ${fpA.hash} == ${fpB.hash}? ${fpA.hash === fpB.hash}`); + if (fpA.hash !== fpB.hash) { + throw new Error("Two fingerprints of the same instant should match"); + } + log( + ` controlCount=${fpA.controlCount} activeWindow='${fpA.activeWindowTitle}'`, + ); + + const closeRule: DynamicControlRule = { + id: "test-close", + match: { kind: "automationId", value: "Close" }, + dynamicProperties: ["name"], + reason: "user-marked", + confidence: 1, + observations: 1, + firstSeen: new Date().toISOString(), + lastConfirmed: new Date().toISOString(), + }; + const fpMasked = await client.treeFingerprint({ + root: rootSelector, + dynamicRules: [closeRule], + }); + log(` with Close-name mask: ${fpMasked.hash}`); + if (fpMasked.hash === fpA.hash) { + throw new Error( + "Masking the Close button's name should change the fingerprint", + ); + } + log(" ✓ rule application changes the fingerprint"); +} + +async function main(): Promise { + mkdirSync(fixturesDir, { recursive: true }); + const client = await HelperClient.start({ debug: true }); + try { + await client.ping(); + + // Close any existing Clock. + for (const w of (await client.appList()).filter((x) => + x.title.includes("Clock"), + )) { + await client.appKill({ pid: w.pid }); + } + await sleep(1500); + + log("launching Clock..."); + const launch = await client.appLaunch({ aumid: CLOCK_AUMID }); + await client.eventsIdle({ debounceMs: 800, maxWaitMs: 5000 }); + + await runFingerprintBasics(client, launch.mainWindow); + await runStopwatchCalibration(client, launch.mainWindow); + + // Cleanup. + await client.appKill({ pid: launch.pid }); + log("DONE"); + } finally { + await client.dispose(); + } +} + +main().catch((e) => { + process.stderr.write(`FAILED: ${e}\n`); + if (e instanceof Error && e.stack) { + process.stderr.write(e.stack + "\n"); + } + process.exit(1); +}); diff --git a/ts/packages/agents/onboarding/src/uiCapture/types.ts b/ts/packages/agents/onboarding/src/uiCapture/types.ts index 930cecdbad..bf2be339e2 100644 --- a/ts/packages/agents/onboarding/src/uiCapture/types.ts +++ b/ts/packages/agents/onboarding/src/uiCapture/types.ts @@ -114,3 +114,54 @@ export type SnapshotPolicy = { afterRestore?: ScriptHook[]; }; }; + +export type DynamicProperty = "value" | "toggleState" | "name"; + +export type ControlMatcher = + | { kind: "automationId"; value: string } + | { kind: "selector"; value: string } + | { kind: "selectorPattern"; pattern: string } + | { + kind: "container"; + container: string; + controlType: string; + nameRegex?: string; + classNameRegex?: string; + }; + +export type DynamicRuleReason = + | "calibration-drift" + | "explore-drift" + | "user-marked" + | "imported"; + +export type DynamicControlRule = { + id: string; + match: ControlMatcher; + dynamicProperties: DynamicProperty[]; + semantic?: string; + reason: DynamicRuleReason; + confidence: number; + observations: number; + firstSeen: string; + lastConfirmed: string; + notes?: string; +}; + +export type DynamicControlsFile = { + version: 1; + integrationName: string; + calibration?: { + lastRun: string; + durationMs: number; + dumpsCompared: number; + }; + rules: DynamicControlRule[]; +}; + +export type FingerprintResult = { + hash: string; + controlCount: number; + activeWindowTitle: string; + focusedSelector?: string; +}; diff --git a/ts/packages/agents/onboarding/test/fixtures/uiCapture/clock-stopwatch-dynamic.json b/ts/packages/agents/onboarding/test/fixtures/uiCapture/clock-stopwatch-dynamic.json new file mode 100644 index 0000000000..16ed877c28 --- /dev/null +++ b/ts/packages/agents/onboarding/test/fixtures/uiCapture/clock-stopwatch-dynamic.json @@ -0,0 +1,27 @@ +{ + "version": 1, + "integrationName": "windowsClock", + "calibration": { + "lastRun": "2026-05-04T05:34:32.554Z", + "durationMs": 6793, + "dumpsCompared": 3 + }, + "rules": [ + { + "id": "cal-1", + "match": { + "kind": "automationId", + "value": "StopwatchTimerText" + }, + "dynamicProperties": [ + "name" + ], + "semantic": "StopwatchTimerText", + "reason": "calibration-drift", + "confidence": 1, + "observations": 2, + "firstSeen": "2026-05-04T05:34:32.554Z", + "lastConfirmed": "2026-05-04T05:34:32.554Z" + } + ] +} \ No newline at end of file From 2660de845ce7baceaa4ddc00bacc93915fa41952 Mon Sep 17 00:00:00 2001 From: Robert Gruen Date: Sun, 3 May 2026 22:50:15 -0700 Subject: [PATCH 06/28] Slice 5: record mode (UIA event subscription + JSONL output) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Helper: JSON-RPC notifications (server → client, no id), routed through a shared write lock so UIA event-thread emissions don't interleave with RPC responses. New events.subscribe / events.unsubscribe RPCs accept eventTypes ["Invoked", "ValueChanged", "ToggleStateChanged", "StructureChanged"], scoped to a selector with TreeScope.Subtree. SubscriptionRegistry holds active subscriptions; Subscription.Dispose unregisters via FlaUI's IDisposable handlers. TS: HelperClient gains a notification dispatch path with `onEvent()` registration. Recorder library subscribes, writes captured events as JSONL into /recordings//transitions.jsonl. Smoke launches Clock, drives it through navigation + click, and captures 10 StructureChanged events end-to-end into a transitions.jsonl fixture. Real-world finding: UIA's InvokedEvent doesn't propagate to in-process listeners for UWP apps even when triggered via real Mouse.LeftClick — likely a cross-process COM marshaling quirk between ApplicationFrameHost and the UWP package's CoreWindow. StructureChanged events DO fire reliably, which is enough for the autonomous-explore loop in slice 6 (which re-dumps the tree after each agent action and doesn't depend on Invoked events for its own actions). Record-mode for genuine user-driven sessions (separate process driving the app) is the case where Invoked events matter, and that case is untested here. Also: NavView Group elements can have dynamic Names that embed running state ("Stopwatch, Paused, 12 seconds 23 centiseconds"), invalidating selectors built on Name-only as soon as the app starts. Caught this in the smoke; selector fallbacks beyond ClassName disambiguation will be needed in slice 6. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/Methods/EventMethods.cs | 187 +++++++++++++++++- .../src/Models/RpcNotification.cs | 13 ++ dotnet/uiAutomationHelper/src/Program.cs | 1 + .../src/Rpc/JsonRpcServer.cs | 30 +++ dotnet/uiAutomationHelper/src/Rpc/Notifier.cs | 23 +++ .../src/Uia/Subscription.cs | 70 +++++++ .../onboarding/src/uiCapture/helperClient.ts | 67 ++++++- .../onboarding/src/uiCapture/recorder.ts | 105 ++++++++++ .../src/uiCapture/test/recorderSmoke.ts | 183 +++++++++++++++++ 9 files changed, 677 insertions(+), 2 deletions(-) create mode 100644 dotnet/uiAutomationHelper/src/Models/RpcNotification.cs create mode 100644 dotnet/uiAutomationHelper/src/Rpc/Notifier.cs create mode 100644 dotnet/uiAutomationHelper/src/Uia/Subscription.cs create mode 100644 ts/packages/agents/onboarding/src/uiCapture/recorder.ts create mode 100644 ts/packages/agents/onboarding/src/uiCapture/test/recorderSmoke.ts diff --git a/dotnet/uiAutomationHelper/src/Methods/EventMethods.cs b/dotnet/uiAutomationHelper/src/Methods/EventMethods.cs index 618aa9c582..d3d632c952 100644 --- a/dotnet/uiAutomationHelper/src/Methods/EventMethods.cs +++ b/dotnet/uiAutomationHelper/src/Methods/EventMethods.cs @@ -2,7 +2,11 @@ // Licensed under the MIT License. using System.Diagnostics; +using System.Text.Json; using System.Text.Json.Serialization; +using FlaUI.Core.AutomationElements; +using FlaUI.Core.Definitions; +using UiAutomationHelper.Models; using UiAutomationHelper.Rpc; using UiAutomationHelper.Uia; @@ -12,7 +16,177 @@ internal static class EventMethods { public static void Register(Dispatch dispatch) { - dispatch.Register("events.idle", IdleAsync); + dispatch.Register("events.idle", IdleAsync); + dispatch.Register("events.subscribe", (p, ct) => Task.FromResult(Subscribe(p))); + dispatch.Register("events.unsubscribe", (p, ct) => Task.FromResult(Unsubscribe(p))); + } + + private static object? Subscribe(JsonElement? @params) + { + var p = RpcParams.ParseRequired(@params); + if (string.IsNullOrEmpty(p.Root)) + { + throw new RpcException(RpcErrorCode.InvalidParams, "'root' is required"); + } + if (p.EventTypes == null || p.EventTypes.Length == 0) + { + throw new RpcException(RpcErrorCode.InvalidParams, "'eventTypes' is required"); + } + var root = SelectorResolver.ResolveOrThrow(p.Root); + var sub = new Subscription { RootSelector = p.Root, Root = root }; + var automation = AutomationHost.Automation; + var subId = sub.Id; + + foreach (var eventType in p.EventTypes) + { + switch (eventType) + { + case "Invoked": + { + var ev = automation.EventLibrary.Invoke.InvokedEvent; + var h = root.RegisterAutomationEvent( + ev, + TreeScope.Subtree, + (el, eid) => OnAutomationEvent(subId, "Invoked", el)); + sub.AutomationHandlers.Add((ev, h)); + break; + } + case "ValueChanged": + { + var pid = automation.PropertyLibrary.Value.Value; + var h = root.RegisterPropertyChangedEvent( + TreeScope.Subtree, + (el, prop, val) => OnPropertyChangedEvent(subId, "ValueChanged", el, val), + pid); + sub.PropertyChangedHandlers.Add(h); + break; + } + case "ToggleStateChanged": + { + var pid = automation.PropertyLibrary.Toggle.ToggleState; + var h = root.RegisterPropertyChangedEvent( + TreeScope.Subtree, + (el, prop, val) => OnPropertyChangedEvent(subId, "ToggleStateChanged", el, val), + pid); + sub.PropertyChangedHandlers.Add(h); + break; + } + case "StructureChanged": + { + var h = root.RegisterStructureChangedEvent( + TreeScope.Subtree, + (el, type, ridArr) => OnStructureChangedEvent(subId, el, type)); + sub.StructureChangedHandlers.Add(h); + break; + } + default: + sub.Dispose(); + throw new RpcException(RpcErrorCode.InvalidParams, + $"Unknown eventType: {eventType}. Supported: Invoked, ValueChanged, ToggleStateChanged, StructureChanged."); + } + } + SubscriptionRegistry.Add(sub); + return new { subscriptionId = sub.Id }; + } + + private static object? Unsubscribe(JsonElement? @params) + { + var p = RpcParams.ParseRequired(@params); + if (string.IsNullOrEmpty(p.SubscriptionId)) + { + throw new RpcException(RpcErrorCode.InvalidParams, "'subscriptionId' is required"); + } + var ok = SubscriptionRegistry.Remove(p.SubscriptionId); + return new { ok }; + } + + private static void OnAutomationEvent(string subId, string type, AutomationElement el) + { + try + { + var selector = Selectors.BuildAbsolutePath(el); + Notifier.Send("event.fired", new + { + subscriptionId = subId, + eventType = type, + selector, + controlSnapshot = TakeSnapshot(el), + timestamp = DateTime.UtcNow.ToString("o"), + }); + } + catch + { + // Element may have been torn down between event firing and our + // attempt to read it. Drop silently. + } + } + + private static void OnPropertyChangedEvent(string subId, string type, AutomationElement el, object? newValue) + { + try + { + var selector = Selectors.BuildAbsolutePath(el); + Notifier.Send("event.fired", new + { + subscriptionId = subId, + eventType = type, + selector, + controlSnapshot = TakeSnapshot(el), + newValue = newValue?.ToString(), + timestamp = DateTime.UtcNow.ToString("o"), + }); + } + catch { } + } + + private static void OnStructureChangedEvent(string subId, AutomationElement el, StructureChangeType type) + { + try + { + var selector = Selectors.BuildAbsolutePath(el); + Notifier.Send("event.fired", new + { + subscriptionId = subId, + eventType = "StructureChanged", + selector, + changeType = type.ToString(), + controlSnapshot = TakeSnapshot(el), + timestamp = DateTime.UtcNow.ToString("o"), + }); + } + catch { } + } + + private static object TakeSnapshot(AutomationElement el) => new + { + controlType = el.ControlType.ToString(), + name = el.Properties.Name.ValueOrDefault, + automationId = el.Properties.AutomationId.ValueOrDefault, + className = el.Properties.ClassName.ValueOrDefault, + value = TryGetValue(el), + toggleState = TryGetToggleState(el), + }; + + private static string? TryGetValue(AutomationElement el) + { + try + { + return el.Patterns.Value.IsSupported + ? el.Patterns.Value.Pattern.Value.ValueOrDefault + : null; + } + catch { return null; } + } + + private static string? TryGetToggleState(AutomationElement el) + { + try + { + return el.Patterns.Toggle.IsSupported + ? el.Patterns.Toggle.Pattern.ToggleState.ValueOrDefault.ToString() + : null; + } + catch { return null; } } private static async Task IdleAsync(System.Text.Json.JsonElement? @params, CancellationToken ct) @@ -42,3 +216,14 @@ internal sealed class EventsIdleParams [JsonPropertyName("debounceMs")] public int? DebounceMs { get; set; } [JsonPropertyName("maxWaitMs")] public int? MaxWaitMs { get; set; } } + +internal sealed class EventsSubscribeParams +{ + [JsonPropertyName("root")] public string? Root { get; set; } + [JsonPropertyName("eventTypes")] public string[]? EventTypes { get; set; } +} + +internal sealed class EventsUnsubscribeParams +{ + [JsonPropertyName("subscriptionId")] public string? SubscriptionId { get; set; } +} diff --git a/dotnet/uiAutomationHelper/src/Models/RpcNotification.cs b/dotnet/uiAutomationHelper/src/Models/RpcNotification.cs new file mode 100644 index 0000000000..f900a2bd94 --- /dev/null +++ b/dotnet/uiAutomationHelper/src/Models/RpcNotification.cs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Text.Json.Serialization; + +namespace UiAutomationHelper.Models; + +internal sealed class RpcNotification +{ + [JsonPropertyName("jsonrpc")] public string JsonRpc { get; init; } = "2.0"; + [JsonPropertyName("method")] public string Method { get; init; } = ""; + [JsonPropertyName("params")] public object? Params { get; init; } +} diff --git a/dotnet/uiAutomationHelper/src/Program.cs b/dotnet/uiAutomationHelper/src/Program.cs index a444e56879..0ea3f36f55 100644 --- a/dotnet/uiAutomationHelper/src/Program.cs +++ b/dotnet/uiAutomationHelper/src/Program.cs @@ -25,6 +25,7 @@ public static async Task Main(string[] args) Methods.Register.All(dispatch); var server = new JsonRpcServer(Console.In, Console.Out, dispatch); + Notifier.Init(server); await server.RunAsync(cts.Token).ConfigureAwait(false); return 0; } diff --git a/dotnet/uiAutomationHelper/src/Rpc/JsonRpcServer.cs b/dotnet/uiAutomationHelper/src/Rpc/JsonRpcServer.cs index 84ee25985d..0073edb4f7 100644 --- a/dotnet/uiAutomationHelper/src/Rpc/JsonRpcServer.cs +++ b/dotnet/uiAutomationHelper/src/Rpc/JsonRpcServer.cs @@ -27,6 +27,36 @@ public JsonRpcServer(TextReader input, TextWriter output, Dispatch dispatch) _dispatch = dispatch; } + /// + /// Send a JSON-RPC notification (server → client, no id). Safe to call + /// from background threads (UIA event handler thread, etc.). + /// + public void Notify(string method, object? @params) + { + var msg = new RpcNotification { Method = method, Params = @params }; + string json; + try + { + json = JsonSerializer.Serialize(msg, JsonOpts); + } + catch + { + return; // best-effort + } + lock (_writeLock) + { + try + { + _output.WriteLine(json); + _output.Flush(); + } + catch + { + // Pipe may be closed during shutdown; ignore. + } + } + } + public async Task RunAsync(CancellationToken ct = default) { while (!ct.IsCancellationRequested) diff --git a/dotnet/uiAutomationHelper/src/Rpc/Notifier.cs b/dotnet/uiAutomationHelper/src/Rpc/Notifier.cs new file mode 100644 index 0000000000..cb38411476 --- /dev/null +++ b/dotnet/uiAutomationHelper/src/Rpc/Notifier.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace UiAutomationHelper.Rpc; + +/// +/// Static facade so event handlers (which aren't connected to dispatch) can +/// push JSON-RPC notifications back to the client. Initialized in Program.cs. +/// +internal static class Notifier +{ + private static JsonRpcServer? _server; + + public static void Init(JsonRpcServer server) + { + _server = server; + } + + public static void Send(string method, object? @params) + { + _server?.Notify(method, @params); + } +} diff --git a/dotnet/uiAutomationHelper/src/Uia/Subscription.cs b/dotnet/uiAutomationHelper/src/Uia/Subscription.cs new file mode 100644 index 0000000000..4284cafff6 --- /dev/null +++ b/dotnet/uiAutomationHelper/src/Uia/Subscription.cs @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using FlaUI.Core.AutomationElements; +using FlaUI.Core.EventHandlers; +using FlaUI.Core.Identifiers; + +namespace UiAutomationHelper.Uia; + +internal sealed class Subscription : IDisposable +{ + public string Id { get; } = Guid.NewGuid().ToString("N"); + public string RootSelector { get; init; } = ""; + public AutomationElement? Root { get; init; } + + public List<(EventId EventId, AutomationEventHandlerBase Handler)> AutomationHandlers { get; } = new(); + public List PropertyChangedHandlers { get; } = new(); + public List StructureChangedHandlers { get; } = new(); + + public void Dispose() + { + // FlaUI's EventHandlerBase implements IDisposable; disposing the + // handler unregisters it. + foreach (var (_, h) in AutomationHandlers) + { + try { h.Dispose(); } catch { } + } + AutomationHandlers.Clear(); + foreach (var h in PropertyChangedHandlers) + { + try { h.Dispose(); } catch { } + } + PropertyChangedHandlers.Clear(); + foreach (var h in StructureChangedHandlers) + { + try { h.Dispose(); } catch { } + } + StructureChangedHandlers.Clear(); + } +} + +internal static class SubscriptionRegistry +{ + private static readonly Dictionary _subs = new(); + private static readonly object _lock = new(); + + public static void Add(Subscription sub) + { + lock (_lock) + { + _subs[sub.Id] = sub; + } + } + + public static bool Remove(string id) + { + lock (_lock) + { + if (!_subs.TryGetValue(id, out var sub)) return false; + sub.Dispose(); + _subs.Remove(id); + return true; + } + } + + public static int Count + { + get { lock (_lock) return _subs.Count; } + } +} diff --git a/ts/packages/agents/onboarding/src/uiCapture/helperClient.ts b/ts/packages/agents/onboarding/src/uiCapture/helperClient.ts index 77d3a36be6..7da175b50b 100644 --- a/ts/packages/agents/onboarding/src/uiCapture/helperClient.ts +++ b/ts/packages/agents/onboarding/src/uiCapture/helperClient.ts @@ -26,6 +26,33 @@ type Pending = { export type HelperRpcError = Error & { code?: number }; +export type EventType = + | "Invoked" + | "ValueChanged" + | "ToggleStateChanged" + | "StructureChanged"; + +export type ControlSnapshot = { + controlType: string; + name?: string; + automationId?: string; + className?: string; + value?: string; + toggleState?: string; +}; + +export type CapturedEvent = { + subscriptionId: string; + eventType: string; + selector: string; + controlSnapshot?: ControlSnapshot; + newValue?: string; + changeType?: string; + timestamp: string; +}; + +export type EventHandler = (evt: CapturedEvent) => void; + export interface HelperClientOptions { binaryPath?: string; debug?: boolean; @@ -62,6 +89,7 @@ function resolveBinary(opts: HelperClientOptions): string { export class HelperClient { private nextId = 1; private readonly pending = new Map(); + private readonly eventHandlers = new Set(); private exited = false; private exitCode: number | null = null; @@ -122,6 +150,8 @@ export class HelperClient { } let msg: { id?: number | string | null; + method?: string; + params?: unknown; result?: unknown; error?: { code: number; message: string; data?: unknown }; }; @@ -135,7 +165,18 @@ export class HelperClient { } const id = typeof msg.id === "number" ? msg.id : null; if (id == null) { - // Notifications not handled in slice 1. + // JSON-RPC notification (server → client). + if (msg.method === "event.fired" && msg.params) { + for (const h of this.eventHandlers) { + try { + h(msg.params as CapturedEvent); + } catch (e) { + if (this.debug) { + process.stderr.write(`[uia-helper handler-throw] ${e}\n`); + } + } + } + } return; } const p = this.pending.get(id); @@ -154,6 +195,17 @@ export class HelperClient { } } + /** + * Register a callback for `event.fired` notifications. Returns a + * disposer that removes the handler. + */ + onEvent(handler: EventHandler): () => void { + this.eventHandlers.add(handler); + return () => { + this.eventHandlers.delete(handler); + }; + } + private call(method: string, params?: unknown): Promise { if (this.exited) { return Promise.reject( @@ -309,6 +361,19 @@ export class HelperClient { return this.call("snapshot.delete", p); } + eventsSubscribe(p: { + root: string; + eventTypes: EventType[]; + }): Promise<{ subscriptionId: string }> { + return this.call("events.subscribe", p); + } + + eventsUnsubscribe(p: { + subscriptionId: string; + }): Promise<{ ok: boolean }> { + return this.call("events.unsubscribe", p); + } + /** * Close the helper's stdin and wait up to `timeoutMs` for graceful exit. * If it doesn't exit, send SIGKILL. diff --git a/ts/packages/agents/onboarding/src/uiCapture/recorder.ts b/ts/packages/agents/onboarding/src/uiCapture/recorder.ts new file mode 100644 index 0000000000..2faf096831 --- /dev/null +++ b/ts/packages/agents/onboarding/src/uiCapture/recorder.ts @@ -0,0 +1,105 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { createWriteStream, mkdirSync, WriteStream } from "node:fs"; +import path from "node:path"; + +import type { + CapturedEvent, + EventType, + HelperClient, +} from "./helperClient.js"; + +const DEFAULT_EVENT_TYPES: EventType[] = [ + "Invoked", + "ValueChanged", + "ToggleStateChanged", + "StructureChanged", +]; + +/** + * Subscribes to UIA events on a target window and writes each captured event + * as one JSONL line to `/recordings//transitions.jsonl`. + * + * No source-attribution (agent vs. user) here — slice 5 just captures + * everything. Tagging by initiator comes with the explore loop in slice 6. + */ +export class Recorder { + private readonly writer: WriteStream; + private readonly removeHandler: () => void; + private subscriptionId: string | null = null; + private eventCount = 0; + private stopped = false; + + private constructor( + private readonly client: HelperClient, + public readonly sessionDir: string, + ) { + mkdirSync(sessionDir, { recursive: true }); + this.writer = createWriteStream( + path.join(sessionDir, "transitions.jsonl"), + { flags: "a" }, + ); + this.removeHandler = client.onEvent((evt) => this.handle(evt)); + } + + static async start(opts: { + client: HelperClient; + workspaceDir: string; + sessionId?: string; + root: string; + eventTypes?: EventType[]; + }): Promise { + const sessionId = + opts.sessionId ?? new Date().toISOString().replace(/[:.]/g, "-"); + const sessionDir = path.join( + opts.workspaceDir, + "recordings", + sessionId, + ); + const recorder = new Recorder(opts.client, sessionDir); + const sub = await opts.client.eventsSubscribe({ + root: opts.root, + eventTypes: opts.eventTypes ?? DEFAULT_EVENT_TYPES, + }); + recorder.subscriptionId = sub.subscriptionId; + return recorder; + } + + private handle(evt: CapturedEvent): void { + if (this.stopped) { + return; + } + if ( + this.subscriptionId && + evt.subscriptionId !== this.subscriptionId + ) { + return; + } + this.eventCount++; + this.writer.write(JSON.stringify(evt) + "\n"); + } + + get count(): number { + return this.eventCount; + } + + async stop(): Promise<{ eventCount: number; sessionDir: string }> { + if (this.stopped) { + return { eventCount: this.eventCount, sessionDir: this.sessionDir }; + } + this.stopped = true; + if (this.subscriptionId) { + try { + await this.client.eventsUnsubscribe({ + subscriptionId: this.subscriptionId, + }); + } catch { + /* helper may already be down */ + } + } + this.removeHandler(); + await new Promise((res) => this.writer.end(() => res())); + return { eventCount: this.eventCount, sessionDir: this.sessionDir }; + } +} diff --git a/ts/packages/agents/onboarding/src/uiCapture/test/recorderSmoke.ts b/ts/packages/agents/onboarding/src/uiCapture/test/recorderSmoke.ts new file mode 100644 index 0000000000..b32e542d09 --- /dev/null +++ b/ts/packages/agents/onboarding/src/uiCapture/test/recorderSmoke.ts @@ -0,0 +1,183 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { readFileSync } from "node:fs"; +import { homedir } from "node:os"; +import path from "node:path"; + +import { HelperClient } from "../helperClient.js"; +import { Recorder } from "../recorder.js"; +import type { TreeNode } from "../types.js"; + +const CLOCK_AUMID = "Microsoft.WindowsAlarms_8wekyb3d8bbwe!App"; + +function log(msg: string): void { + process.stdout.write(`[rec] ${msg}\n`); +} + +function findFirst( + node: TreeNode, + pred: (n: TreeNode) => boolean, +): TreeNode | null { + if (pred(node)) { + return node; + } + for (const c of node.children) { + const f = findFirst(c, pred); + if (f) { + return f; + } + } + return null; +} + +async function sleep(ms: number): Promise { + await new Promise((res) => setTimeout(res, ms)); +} + +async function main(): Promise { + const workspaceDir = path.join( + homedir(), + ".typeagent", + "onboarding", + "_uic_recorder_smoke", + ); + + const client = await HelperClient.start({ debug: true }); + try { + await client.ping(); + + for (const w of (await client.appList()).filter((x) => + x.title.includes("Clock"), + )) { + await client.appKill({ pid: w.pid }); + } + await sleep(1500); + + log("launching Clock..."); + const launch = await client.appLaunch({ aumid: CLOCK_AUMID }); + await client.eventsIdle({ debounceMs: 800, maxWaitMs: 5000 }); + + log("starting recorder..."); + const recorder = await Recorder.start({ + client, + workspaceDir, + root: launch.mainWindow, + eventTypes: [ + "Invoked", + "ValueChanged", + "ToggleStateChanged", + "StructureChanged", + ], + }); + log(`session dir: ${recorder.sessionDir}`); + + // Drive Clock with stable, idempotent actions: + // 1) navigate to Stopwatch (do.select on a NavView ListItem) + // 2) navigate to Alarm (do.select) + // 3) invoke a NavView action that's guaranteed to fire Invoked — + // we'll find any Invoke-pattern button that's not Close/Minimize + // to avoid tearing the window down mid-recording. + const tree = await client.treeDump({ + root: launch.mainWindow, + maxDepth: 8, + }); + const stopwatchTab = findFirst( + tree, + (n) => + n.name === "Stopwatch" && n.patterns.includes("SelectionItem"), + ); + const alarmTab = findFirst( + tree, + (n) => + n.name === "Alarm" && n.patterns.includes("SelectionItem"), + ); + const invokeBtn = findFirst( + tree, + (n) => + n.patterns.includes("Invoke") && + n.controlType === "Button" && + !!n.name && + !["Close Clock", "Minimize Clock", "Maximize Clock"].includes( + n.name, + ), + ); + + if (stopwatchTab) { + log(`do.select Stopwatch tab`); + await client.doSelect({ selector: stopwatchTab.selector }); + await sleep(600); + } + if (alarmTab) { + log(`do.select Alarm tab`); + await client.doSelect({ selector: alarmTab.selector }); + await sleep(600); + } + if (invokeBtn) { + // Use do.click (real Mouse.LeftClick) rather than do.invoke + // (in-process pattern call) — UIA events propagate more reliably + // for synthesized input than for in-process pattern invocations. + log(`do.click "${invokeBtn.name}"`); + try { + await client.doClick({ selector: invokeBtn.selector }); + await sleep(600); + } catch (e) { + log(` (click failed: ${e})`); + } + } + + // Allow events to drain before stopping. + await sleep(1000); + + log("stopping recorder..."); + const stopped = await recorder.stop(); + log(`captured ${stopped.eventCount} event(s) in ${stopped.sessionDir}`); + + if (stopped.eventCount === 0) { + throw new Error( + "Expected at least one event captured during recording session", + ); + } + + // Verify the JSONL file is well-formed and includes Invoked events. + const jsonl = readFileSync( + path.join(stopped.sessionDir, "transitions.jsonl"), + "utf8", + ) + .split("\n") + .filter((l) => l.length > 0) + .map((l) => JSON.parse(l)); + log(`parsed ${jsonl.length} JSONL line(s)`); + + const byType = new Map(); + for (const e of jsonl) { + byType.set(e.eventType, (byType.get(e.eventType) ?? 0) + 1); + } + for (const [type, n] of byType) { + log(` ${type}: ${n}`); + } + + const sampleInvoked = jsonl.find((e) => e.eventType === "Invoked"); + if (sampleInvoked) { + log( + ` sample Invoked: selector=${sampleInvoked.selector?.slice(0, 80)}...`, + ); + log( + ` snapshot: ct=${sampleInvoked.controlSnapshot?.controlType} aid=${sampleInvoked.controlSnapshot?.automationId}`, + ); + } + + await client.appKill({ pid: launch.pid }); + log("DONE"); + } finally { + await client.dispose(); + } +} + +main().catch((e) => { + process.stderr.write(`FAILED: ${e}\n`); + if (e instanceof Error && e.stack) { + process.stderr.write(e.stack + "\n"); + } + process.exit(1); +}); From 75a98fed6f382e7dac78dfa666199e4dbdee5496 Mon Sep 17 00:00:00 2001 From: Robert Gruen Date: Sun, 3 May 2026 22:57:20 -0700 Subject: [PATCH 07/28] Slice 6a: autonomous explore loop mechanism (stub oracle) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Outer loop with pluggable DecisionOracle: - capture (treeFingerprint + treeDump → upsertState dedup by fingerprint) - oracle.decide(input) → ExploreDecision - execute (dispatch verb to do.* RPCs) - eventsIdle → recapture → addTransition - persist incrementally to states.jsonl + transitions.jsonl State graph persists every iteration (JSONL append + per-state TreeNode JSON + optional screenshots) and rehydrates on construction, so a crashed run can resume from disk. Budget gates: maxIterations / maxWallClockMs / maxStates / convergenceThreshold (iterations since last new state). Frontier computation: maps Pattern set → ActionVerb candidates per actionable on-screen control, marks destructive (delete/remove/reset/ clear/erase regex), priority-sorts (Button/MenuItem/ListItem first, unstable identifiers later, destructive last). Stub oracle picks the first non-destructive non-window-management frontier item; smoke shows 6 iterations, 2 distinct states, 5 successful transitions persisted with correct fromStateId→toStateId dedup on revisits. Slice 6b will swap StubOracle for a typechat-backed LLM oracle. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../onboarding/src/uiCapture/exploreGraph.ts | 183 +++++++++ .../onboarding/src/uiCapture/exploreTypes.ts | 109 +++++ .../onboarding/src/uiCapture/explorer.ts | 379 ++++++++++++++++++ .../onboarding/src/uiCapture/frontier.ts | 109 +++++ .../onboarding/src/uiCapture/stubOracle.ts | 57 +++ .../src/uiCapture/test/exploreSmoke.ts | 153 +++++++ 6 files changed, 990 insertions(+) create mode 100644 ts/packages/agents/onboarding/src/uiCapture/exploreGraph.ts create mode 100644 ts/packages/agents/onboarding/src/uiCapture/exploreTypes.ts create mode 100644 ts/packages/agents/onboarding/src/uiCapture/explorer.ts create mode 100644 ts/packages/agents/onboarding/src/uiCapture/frontier.ts create mode 100644 ts/packages/agents/onboarding/src/uiCapture/stubOracle.ts create mode 100644 ts/packages/agents/onboarding/src/uiCapture/test/exploreSmoke.ts diff --git a/ts/packages/agents/onboarding/src/uiCapture/exploreGraph.ts b/ts/packages/agents/onboarding/src/uiCapture/exploreGraph.ts new file mode 100644 index 0000000000..81c6f168b3 --- /dev/null +++ b/ts/packages/agents/onboarding/src/uiCapture/exploreGraph.ts @@ -0,0 +1,183 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { + appendFileSync, + createWriteStream, + existsSync, + mkdirSync, + readFileSync, + writeFileSync, + WriteStream, +} from "node:fs"; +import path from "node:path"; + +import type { + CapturedState, + CapturedTransition, +} from "./exploreTypes.js"; +import type { TreeNode } from "./types.js"; + +/** + * On-disk layout for one exploration run: + * /states.jsonl metadata index + * /transitions.jsonl edges + * /states/state-NNN.json full TreeNode per state + * /screenshots/state-NNN.png optional + * /run.json run config + status + * + * Append-only JSONL allows in-place resume after a crash; the in-memory + * fingerprint→stateId map is rebuilt from states.jsonl on load. + */ +export class ExploreGraph { + private readonly statesByFp = new Map(); + private readonly statesById = new Map(); + private readonly transitions: CapturedTransition[] = []; + private nextStateNum: number; + private nextTransitionNum: number; + + private readonly statesStream: WriteStream; + private readonly transitionsStream: WriteStream; + + constructor(public readonly runDir: string) { + mkdirSync(path.join(runDir, "states"), { recursive: true }); + mkdirSync(path.join(runDir, "screenshots"), { recursive: true }); + + // Restore prior state if files already exist. + const statesFile = path.join(runDir, "states.jsonl"); + const transitionsFile = path.join(runDir, "transitions.jsonl"); + if (existsSync(statesFile)) { + for (const line of readFileSync(statesFile, "utf8") + .split("\n") + .filter((l) => l.length > 0)) { + const s = JSON.parse(line) as CapturedState; + this.statesByFp.set(s.fingerprint, s.id); + this.statesById.set(s.id, s); + } + } + if (existsSync(transitionsFile)) { + for (const line of readFileSync(transitionsFile, "utf8") + .split("\n") + .filter((l) => l.length > 0)) { + this.transitions.push(JSON.parse(line) as CapturedTransition); + } + } + this.nextStateNum = this.statesById.size + 1; + this.nextTransitionNum = this.transitions.length + 1; + this.statesStream = createWriteStream(statesFile, { flags: "a" }); + this.transitionsStream = createWriteStream(transitionsFile, { flags: "a" }); + } + + get stateCount(): number { + return this.statesById.size; + } + + get transitionCount(): number { + return this.transitions.length; + } + + get successfulTransitionCount(): number { + let n = 0; + for (const t of this.transitions) if (t.success) n++; + return n; + } + + get failedTransitionCount(): number { + return this.transitions.length - this.successfulTransitionCount; + } + + findStateByFingerprint(fingerprint: string): CapturedState | undefined { + const id = this.statesByFp.get(fingerprint); + return id ? this.statesById.get(id) : undefined; + } + + listStateSummaries(): Array<{ + id: string; + label?: string; + fingerprint: string; + }> { + const out: Array<{ id: string; label?: string; fingerprint: string }> = []; + for (const s of this.statesById.values()) { + const item: { id: string; label?: string; fingerprint: string } = { + id: s.id, + fingerprint: s.fingerprint, + }; + if (s.label !== undefined) item.label = s.label; + out.push(item); + } + return out; + } + + /** + * Register a new state if its fingerprint is novel; otherwise return the + * existing state record. The full tree JSON is persisted on disk. + */ + upsertState(opts: { + fingerprint: string; + windowTitle: string; + tree: TreeNode; + screenshotPngBase64?: string; + label?: string; + }): { state: CapturedState; isNew: boolean } { + const existingId = this.statesByFp.get(opts.fingerprint); + if (existingId) { + return { state: this.statesById.get(existingId)!, isNew: false }; + } + const id = `state-${this.nextStateNum.toString().padStart(3, "0")}`; + this.nextStateNum++; + const treeFile = path.join("states", `${id}.json`); + writeFileSync( + path.join(this.runDir, treeFile), + JSON.stringify(opts.tree, null, 2), + ); + let screenshotFile: string | undefined; + if (opts.screenshotPngBase64) { + const sf = path.join("screenshots", `${id}.png`); + writeFileSync( + path.join(this.runDir, sf), + Buffer.from(opts.screenshotPngBase64, "base64"), + ); + screenshotFile = sf; + } + const state: CapturedState = { + id, + fingerprint: opts.fingerprint, + capturedAt: Date.now(), + windowTitle: opts.windowTitle, + treeFile, + ...(screenshotFile !== undefined ? { screenshotFile } : {}), + ...(opts.label !== undefined ? { label: opts.label } : {}), + }; + this.statesByFp.set(state.fingerprint, state.id); + this.statesById.set(state.id, state); + this.statesStream.write(JSON.stringify(state) + "\n"); + return { state, isNew: true }; + } + + addTransition(t: Omit): CapturedTransition { + const id = `trans-${this.nextTransitionNum.toString().padStart(4, "0")}`; + this.nextTransitionNum++; + const full: CapturedTransition = { ...t, id }; + this.transitions.push(full); + this.transitionsStream.write(JSON.stringify(full) + "\n"); + return full; + } + + recentTransitions(n: number): CapturedTransition[] { + return this.transitions.slice(Math.max(0, this.transitions.length - n)); + } + + async close(): Promise { + await Promise.all([ + new Promise((res) => this.statesStream.end(() => res())), + new Promise((res) => this.transitionsStream.end(() => res())), + ]); + } + + /** + * Append a JSON line to a sibling file (used for run.json snapshots). + */ + writeRunMeta(name: string, content: object): void { + appendFileSync(path.join(this.runDir, name), JSON.stringify(content) + "\n"); + } +} diff --git a/ts/packages/agents/onboarding/src/uiCapture/exploreTypes.ts b/ts/packages/agents/onboarding/src/uiCapture/exploreTypes.ts new file mode 100644 index 0000000000..6c8e05cf43 --- /dev/null +++ b/ts/packages/agents/onboarding/src/uiCapture/exploreTypes.ts @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import type { ActionVerb } from "./types.js"; + +export type CapturedState = { + id: string; + fingerprint: string; + capturedAt: number; + windowTitle: string; + treeFile: string; + screenshotFile?: string; + label?: string; + notes?: string; +}; + +export type TransitionSource = "agent" | "user" | "external"; + +export type CapturedTransition = { + id: string; + iteration: number; + fromStateId: string; + toStateId: string; + trigger: { + selector: string; + verb: ActionVerb; + value?: string | number | boolean; + }; + rationale?: string; + expectedDelta?: string; + observedDeltaSummary?: string; + source: TransitionSource; + timestamp: number; + success: boolean; + errorMessage?: string; +}; + +export type FrontierVerb = { + verb: ActionVerb; + valueShape?: "free-text" | "range" | "selection" | "boolean" | "none"; + rangeMeta?: { min: number; max: number; step?: number }; + selectionItems?: string[]; +}; + +export type FrontierItem = { + id: string; + selector: string; + controlType: string; + name?: string; + automationId?: string; + className?: string; + verbs: FrontierVerb[]; + destructiveHint: boolean; + boundingRect?: { x: number; y: number; width: number; height: number }; +}; + +export type ExploreDecisionAct = { + kind: "act"; + frontierId: string; + verb: ActionVerb; + value?: string | number | boolean; + expectedDelta: string; + rationale: string; +}; + +export type ExploreDecisionStop = { kind: "stop"; reason: string }; +export type ExploreDecisionRestore = { kind: "restore"; rationale: string }; +export type ExploreDecisionUserPause = { kind: "userPause"; rationale: string }; + +export type ExploreDecision = + | ExploreDecisionAct + | ExploreDecisionStop + | ExploreDecisionRestore + | ExploreDecisionUserPause; + +export type DecisionInput = { + iteration: number; + state: CapturedState; + frontier: FrontierItem[]; + visitedStates: Array<{ id: string; label?: string; fingerprint: string }>; + recentTransitions: CapturedTransition[]; + budget: { remainingIterations: number; remainingMs: number }; +}; + +export interface DecisionOracle { + decide(input: DecisionInput): Promise; +} + +export type ExploreBudget = { + maxIterations?: number; + maxWallClockMs?: number; + maxStates?: number; + convergenceThreshold?: number; + historyTailSize?: number; +}; + +export type ExploreRunMetrics = { + runId: string; + startedAt: string; + endedAt: string; + walltimeMs: number; + iterations: number; + statesDiscovered: number; + transitionsRecorded: number; + successfulTransitions: number; + failedTransitions: number; + stopReason: string; + convergenceIterations: number; +}; diff --git a/ts/packages/agents/onboarding/src/uiCapture/explorer.ts b/ts/packages/agents/onboarding/src/uiCapture/explorer.ts new file mode 100644 index 0000000000..d04461fbf5 --- /dev/null +++ b/ts/packages/agents/onboarding/src/uiCapture/explorer.ts @@ -0,0 +1,379 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { mkdirSync, writeFileSync } from "node:fs"; +import path from "node:path"; + +import type { + CapturedState, + CapturedTransition, + DecisionInput, + DecisionOracle, + ExploreBudget, + ExploreDecision, + ExploreRunMetrics, + FrontierItem, +} from "./exploreTypes.js"; +import { ExploreGraph } from "./exploreGraph.js"; +import { computeFrontier } from "./frontier.js"; +import type { HelperClient } from "./helperClient.js"; +import type { DynamicControlRule } from "./types.js"; + +const DEFAULT_BUDGET: Required = { + maxIterations: 200, + maxWallClockMs: 30 * 60_000, + maxStates: 50, + convergenceThreshold: 15, + historyTailSize: 5, +}; + +export type ExploreOptions = { + client: HelperClient; + oracle: DecisionOracle; + workspaceDir: string; + rootSelector: string; + runId?: string; + dynamicRules?: DynamicControlRule[]; + captureScreenshots?: boolean; + treeMaxDepth?: number; + idleDebounceMs?: number; + idleMaxWaitMs?: number; + budget?: ExploreBudget; + onIteration?: (info: { + iteration: number; + state: CapturedState; + decision: ExploreDecision; + }) => void; +}; + +/** + * Deterministic outer loop: capture state → ask oracle → execute → capture + * post-state → record transition → persist. The oracle decides; we just + * orchestrate. + * + * Snapshot/restore integration is intentionally NOT here yet — slice 6 + * focuses on the loop+graph mechanics. Wiring snapshot capture/restore + * around runs lands in a follow-up. + */ +export async function runExploration( + opts: ExploreOptions, +): Promise { + const budget = { ...DEFAULT_BUDGET, ...(opts.budget ?? {}) }; + const runId = + opts.runId ?? new Date().toISOString().replace(/[:.]/g, "-"); + const runDir = path.join(opts.workspaceDir, "runs", runId); + mkdirSync(runDir, { recursive: true }); + + const graph = new ExploreGraph(runDir); + + const startedAt = new Date(); + const startTime = Date.now(); + const idleDebounceMs = opts.idleDebounceMs ?? 500; + const idleMaxWaitMs = opts.idleMaxWaitMs ?? 4000; + + let iteration = 0; + let stopReason = "loop-completed"; + let lastNewStateIteration = 0; + + try { + // Pre-loop: capture initial state. + await opts.client.eventsIdle({ + debounceMs: idleDebounceMs, + maxWaitMs: idleMaxWaitMs, + }); + let { state, frontier } = await captureState( + opts, + graph, + opts.rootSelector, + ); + if (graph.stateCount === 1) { + lastNewStateIteration = 0; + } + + while (true) { + const elapsed = Date.now() - startTime; + const remainingIterations = budget.maxIterations - iteration; + const remainingMs = budget.maxWallClockMs - elapsed; + + if (iteration >= budget.maxIterations) { + stopReason = "max-iterations"; + break; + } + if (elapsed >= budget.maxWallClockMs) { + stopReason = "max-walltime"; + break; + } + if (graph.stateCount >= budget.maxStates) { + stopReason = "max-states"; + break; + } + if ( + iteration - lastNewStateIteration >= + budget.convergenceThreshold + ) { + stopReason = "converged"; + break; + } + + iteration++; + const input: DecisionInput = { + iteration, + state, + frontier, + visitedStates: graph.listStateSummaries(), + recentTransitions: graph.recentTransitions( + budget.historyTailSize, + ), + budget: { remainingIterations, remainingMs }, + }; + + const decision = await opts.oracle.decide(input); + opts.onIteration?.({ iteration, state, decision }); + + if (decision.kind === "stop") { + stopReason = `oracle-stop: ${decision.reason}`; + break; + } + if (decision.kind === "userPause") { + // For slice 6a: no implementation; treat as a soft idle. + await sleep(2000); + ({ state, frontier } = await captureState( + opts, + graph, + opts.rootSelector, + )); + continue; + } + if (decision.kind === "restore") { + // Slice 6a stub: just re-capture (no snapshot integration yet). + ({ state, frontier } = await captureState( + opts, + graph, + opts.rootSelector, + )); + continue; + } + + // decision.kind === "act" + const item = frontier.find((f) => f.id === decision.frontierId); + if (!item) { + // Oracle picked a stale frontier id; record + skip. + graph.addTransition({ + iteration, + fromStateId: state.id, + toStateId: state.id, + trigger: { selector: "(invalid)", verb: decision.verb }, + rationale: decision.rationale, + expectedDelta: decision.expectedDelta, + source: "agent", + timestamp: Date.now(), + success: false, + errorMessage: `unknown frontier id ${decision.frontierId}`, + }); + continue; + } + + const transition = await executeAction(opts, graph, { + iteration, + fromState: state, + item, + decision, + rootSelector: opts.rootSelector, + idleDebounceMs, + idleMaxWaitMs, + }); + + // Re-capture for next iteration. + const next = await captureState( + opts, + graph, + opts.rootSelector, + ); + if (graph.findStateByFingerprint(next.state.fingerprint)?.id === + next.state.id && + next.state.id !== state.id) { + lastNewStateIteration = iteration; + } + state = next.state; + frontier = next.frontier; + // ensure graph has transition's toState resolved for callers reading it + void transition; + } + } finally { + await graph.close(); + } + + const endedAt = new Date(); + const metrics: ExploreRunMetrics = { + runId, + startedAt: startedAt.toISOString(), + endedAt: endedAt.toISOString(), + walltimeMs: endedAt.getTime() - startedAt.getTime(), + iterations: iteration, + statesDiscovered: graph.stateCount, + transitionsRecorded: graph.transitionCount, + successfulTransitions: graph.successfulTransitionCount, + failedTransitions: graph.failedTransitionCount, + stopReason, + convergenceIterations: lastNewStateIteration, + }; + writeFileSync( + path.join(runDir, "metrics.json"), + JSON.stringify(metrics, null, 2), + ); + return metrics; +} + +async function captureState( + opts: ExploreOptions, + graph: ExploreGraph, + rootSelector: string, +): Promise<{ state: CapturedState; frontier: FrontierItem[] }> { + const fp = await opts.client.treeFingerprint({ + root: rootSelector, + ...(opts.dynamicRules !== undefined + ? { dynamicRules: opts.dynamicRules } + : {}), + }); + const tree = await opts.client.treeDump({ + root: rootSelector, + maxDepth: opts.treeMaxDepth ?? 12, + }); + let screenshotPngBase64: string | undefined; + if (opts.captureScreenshots) { + try { + const shot = await opts.client.screenshot({ root: rootSelector }); + screenshotPngBase64 = shot.pngBase64; + } catch { + /* screenshots are best-effort */ + } + } + const { state } = graph.upsertState({ + fingerprint: fp.hash, + windowTitle: fp.activeWindowTitle, + tree, + ...(screenshotPngBase64 !== undefined ? { screenshotPngBase64 } : {}), + }); + const frontier = computeFrontier(tree); + return { state, frontier }; +} + +async function executeAction( + opts: ExploreOptions, + graph: ExploreGraph, + args: { + iteration: number; + fromState: CapturedState; + item: FrontierItem; + decision: { verb: string; value?: string | number | boolean; expectedDelta: string; rationale: string }; + rootSelector: string; + idleDebounceMs: number; + idleMaxWaitMs: number; + }, +): Promise { + const { client } = opts; + const { item, decision } = args; + const verb = decision.verb; + + let success = true; + let errorMessage: string | undefined; + try { + switch (verb) { + case "invoke": + await client.doInvoke({ selector: item.selector }); + break; + case "toggle": + await client.doToggle({ + selector: item.selector, + ...(typeof decision.value === "boolean" + ? { value: decision.value } + : {}), + }); + break; + case "select": + await client.doSelect({ + selector: item.selector, + ...(decision.value !== undefined + ? { item: decision.value as string | number } + : {}), + }); + break; + case "expand": + await client.doExpand({ + selector: item.selector, + expand: decision.value !== false, + }); + break; + case "setValue": + await client.doSetValue({ + selector: item.selector, + value: decision.value ?? "", + }); + break; + case "scroll": + await client.doScroll({ + selector: item.selector, + direction: "down", + }); + break; + case "focus": + await client.doFocus({ selector: item.selector }); + break; + case "click": + await client.doClick({ selector: item.selector }); + break; + default: + success = false; + errorMessage = `unsupported verb ${verb}`; + } + } catch (e) { + success = false; + errorMessage = e instanceof Error ? e.message : String(e); + } + + await client.eventsIdle({ + debounceMs: args.idleDebounceMs, + maxWaitMs: args.idleMaxWaitMs, + }); + + // Capture post-state to link transition. + const fp = await client.treeFingerprint({ + root: args.rootSelector, + ...(opts.dynamicRules !== undefined + ? { dynamicRules: opts.dynamicRules } + : {}), + }); + const tree = await client.treeDump({ + root: args.rootSelector, + maxDepth: opts.treeMaxDepth ?? 12, + }); + const { state: toState } = graph.upsertState({ + fingerprint: fp.hash, + windowTitle: fp.activeWindowTitle, + tree, + }); + + return graph.addTransition({ + iteration: args.iteration, + fromStateId: args.fromState.id, + toStateId: toState.id, + trigger: { + selector: item.selector, + verb: verb as any, + ...(decision.value !== undefined ? { value: decision.value } : {}), + }, + rationale: decision.rationale, + expectedDelta: decision.expectedDelta, + observedDeltaSummary: args.fromState.id === toState.id + ? "no observable state change" + : `${args.fromState.id} → ${toState.id}`, + source: "agent", + timestamp: Date.now(), + success, + ...(errorMessage !== undefined ? { errorMessage } : {}), + }); +} + +async function sleep(ms: number): Promise { + await new Promise((res) => setTimeout(res, ms)); +} diff --git a/ts/packages/agents/onboarding/src/uiCapture/frontier.ts b/ts/packages/agents/onboarding/src/uiCapture/frontier.ts new file mode 100644 index 0000000000..02fd012a4f --- /dev/null +++ b/ts/packages/agents/onboarding/src/uiCapture/frontier.ts @@ -0,0 +1,109 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import type { FrontierItem, FrontierVerb } from "./exploreTypes.js"; +import type { Pattern, TreeNode } from "./types.js"; + +const DESTRUCTIVE_RE = /\b(delete|remove|reset|clear|erase|destroy|trash|discard)\b/i; + +/** + * Walk a tree dump and emit one FrontierItem per actionable element. + * "Actionable" = supports a pattern that maps to one of our verbs AND + * is enabled and on-screen. + */ +export function computeFrontier(root: TreeNode): FrontierItem[] { + const items: FrontierItem[] = []; + let counter = 1; + walk(root, items, () => `F-${(counter++).toString().padStart(3, "0")}`); + sortByPriority(items); + return items; +} + +function walk( + node: TreeNode, + out: FrontierItem[], + nextId: () => string, +): void { + if (node.isEnabled && !node.isOffscreen) { + const verbs = verbsFor(node); + if (verbs.length > 0) { + const item: FrontierItem = { + id: nextId(), + selector: node.selector, + controlType: node.controlType, + verbs, + destructiveHint: isDestructive(node), + boundingRect: node.boundingRect, + }; + if (node.name !== undefined) item.name = node.name; + if (node.automationId !== undefined) item.automationId = node.automationId; + if (node.className !== undefined) item.className = node.className; + out.push(item); + } + } + for (const c of node.children) { + walk(c, out, nextId); + } +} + +function verbsFor(node: TreeNode): FrontierVerb[] { + const verbs: FrontierVerb[] = []; + const has = (p: Pattern) => node.patterns.includes(p); + + if (has("Invoke")) { + verbs.push({ verb: "invoke", valueShape: "none" }); + } + if (has("Toggle")) { + verbs.push({ verb: "toggle", valueShape: "boolean" }); + } + // SelectionItem first (the item itself can be selected) — overrides Selection container if both present. + if (has("SelectionItem")) { + verbs.push({ verb: "select", valueShape: "none" }); + } else if (has("Selection")) { + verbs.push({ verb: "select", valueShape: "selection" }); + } + if (has("ExpandCollapse")) { + verbs.push({ verb: "expand", valueShape: "boolean" }); + } + if (has("Value")) { + verbs.push({ verb: "setValue", valueShape: "free-text" }); + } + if (has("RangeValue") && !has("Value")) { + verbs.push({ verb: "setValue", valueShape: "range" }); + } + if (has("Scroll")) { + verbs.push({ verb: "scroll", valueShape: "none" }); + } + return verbs; +} + +function isDestructive(node: TreeNode): boolean { + const text = `${node.name ?? ""} ${node.automationId ?? ""}`; + return DESTRUCTIVE_RE.test(text); +} + +function sortByPriority(items: FrontierItem[]): void { + items.sort((a, b) => priority(a) - priority(b)); +} + +function priority(item: FrontierItem): number { + // Lower is better. + let p = 0; + if (item.destructiveHint) { + p += 1000; // push destructive items to the back + } + // High-signal control types come first. + const ct = item.controlType; + if (ct === "Button" || ct === "MenuItem" || ct === "ListItem") { + p += 0; + } else if (ct === "Edit" || ct === "ComboBox" || ct === "CheckBox") { + p += 10; + } else { + p += 50; + } + // Stable identifiers preferred. + if (!item.automationId) { + p += 5; + } + return p; +} diff --git a/ts/packages/agents/onboarding/src/uiCapture/stubOracle.ts b/ts/packages/agents/onboarding/src/uiCapture/stubOracle.ts new file mode 100644 index 0000000000..14bb219ba2 --- /dev/null +++ b/ts/packages/agents/onboarding/src/uiCapture/stubOracle.ts @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import type { + DecisionInput, + DecisionOracle, + ExploreDecision, +} from "./exploreTypes.js"; + +/** + * Deterministic test oracle. Picks the first non-destructive frontier item, + * preferring `invoke` and `select` verbs. Stops when the frontier is empty + * or when a maxDecisions cap is hit. + * + * Useful for proving the loop mechanics without LLM calls. Slice 6b replaces + * this with a typechat-backed oracle. + */ +export class StubOracle implements DecisionOracle { + private decisions = 0; + + constructor( + private readonly opts: { maxDecisions?: number; preferVerbs?: string[] } = {}, + ) {} + + async decide(input: DecisionInput): Promise { + const cap = this.opts.maxDecisions ?? 5; + if (this.decisions >= cap) { + return { kind: "stop", reason: `stub-cap (${cap})` }; + } + const preferred = this.opts.preferVerbs ?? ["invoke", "select"]; + const isWindowMgmt = (f: { name?: string; automationId?: string }) => + /(?:Close|Minimize|Maximize|Restore)\b/i.test( + `${f.name ?? ""} ${f.automationId ?? ""}`, + ); + const candidates = input.frontier.filter( + (f) => !f.destructiveHint && !isWindowMgmt(f), + ); + const pick = + candidates.find((f) => + f.verbs.some((v) => preferred.includes(v.verb)), + ) ?? candidates[0]; + if (!pick) { + return { kind: "stop", reason: "no candidates in frontier" }; + } + const verb = + pick.verbs.find((v) => preferred.includes(v.verb))?.verb ?? + pick.verbs[0]!.verb; + this.decisions++; + return { + kind: "act", + frontierId: pick.id, + verb, + expectedDelta: `(stub) ${verb} on ${pick.controlType} '${pick.name ?? pick.automationId ?? ""}'`, + rationale: `stub-oracle pick #${this.decisions}: ${verb} ${pick.id}`, + }; + } +} diff --git a/ts/packages/agents/onboarding/src/uiCapture/test/exploreSmoke.ts b/ts/packages/agents/onboarding/src/uiCapture/test/exploreSmoke.ts new file mode 100644 index 0000000000..81e531a182 --- /dev/null +++ b/ts/packages/agents/onboarding/src/uiCapture/test/exploreSmoke.ts @@ -0,0 +1,153 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { existsSync, readFileSync, rmSync } from "node:fs"; +import { homedir } from "node:os"; +import path from "node:path"; + +import { runExploration } from "../explorer.js"; +import { HelperClient } from "../helperClient.js"; +import { StubOracle } from "../stubOracle.js"; + +const CLOCK_AUMID = "Microsoft.WindowsAlarms_8wekyb3d8bbwe!App"; + +function log(msg: string): void { + process.stdout.write(`[expl] ${msg}\n`); +} + +async function sleep(ms: number): Promise { + await new Promise((res) => setTimeout(res, ms)); +} + +async function main(): Promise { + const workspaceDir = path.join( + homedir(), + ".typeagent", + "onboarding", + "_uic_explore_smoke", + ); + // Wipe prior runs for a deterministic smoke. + rmSync(workspaceDir, { recursive: true, force: true }); + + const client = await HelperClient.start({ debug: true }); + try { + await client.ping(); + + for (const w of (await client.appList()).filter((x) => + x.title.includes("Clock"), + )) { + await client.appKill({ pid: w.pid }); + } + await sleep(1500); + + log("launching Clock..."); + const launch = await client.appLaunch({ aumid: CLOCK_AUMID }); + await client.eventsIdle({ debounceMs: 800, maxWaitMs: 5000 }); + log(`launched pid=${launch.pid}`); + + const oracle = new StubOracle({ maxDecisions: 5 }); + log("running exploration with stub oracle (max 5 decisions)..."); + const metrics = await runExploration({ + client, + oracle, + workspaceDir, + rootSelector: launch.mainWindow, + captureScreenshots: false, + treeMaxDepth: 8, + budget: { + maxIterations: 10, + maxWallClockMs: 60_000, + maxStates: 30, + convergenceThreshold: 8, + historyTailSize: 3, + }, + onIteration: ({ iteration, state, decision }) => { + if (decision.kind === "act") { + log( + ` iter ${iteration}: ${state.id} → ${decision.verb} ${decision.frontierId}`, + ); + } else { + log( + ` iter ${iteration}: ${state.id} → ${decision.kind} (${ + "reason" in decision + ? decision.reason + : "rationale" in decision + ? decision.rationale + : "" + })`, + ); + } + }, + }); + + log("metrics:"); + log(` runId=${metrics.runId}`); + log( + ` iterations=${metrics.iterations} states=${metrics.statesDiscovered} transitions=${metrics.transitionsRecorded}`, + ); + log( + ` successful=${metrics.successfulTransitions} failed=${metrics.failedTransitions}`, + ); + log(` stopReason=${metrics.stopReason} walltime=${metrics.walltimeMs}ms`); + + // Verify artifacts. + const runDir = path.join(workspaceDir, "runs", metrics.runId); + const statesFile = path.join(runDir, "states.jsonl"); + const transitionsFile = path.join(runDir, "transitions.jsonl"); + if (!existsSync(statesFile)) { + throw new Error(`Missing states.jsonl at ${statesFile}`); + } + if (!existsSync(transitionsFile)) { + throw new Error(`Missing transitions.jsonl at ${transitionsFile}`); + } + const stateLines = readFileSync(statesFile, "utf8") + .split("\n") + .filter((l) => l.length > 0); + const transitionLines = readFileSync(transitionsFile, "utf8") + .split("\n") + .filter((l) => l.length > 0); + log( + ` on disk: ${stateLines.length} state line(s), ${transitionLines.length} transition line(s)`, + ); + + if (stateLines.length === 0) { + throw new Error("Expected at least one state recorded"); + } + if (transitionLines.length === 0) { + throw new Error("Expected at least one transition recorded"); + } + + // Sanity-check first state record + first transition record shapes. + const firstState = JSON.parse(stateLines[0]!); + const firstTrans = JSON.parse(transitionLines[0]!); + log( + ` state[0] id=${firstState.id} fp=${firstState.fingerprint} window='${firstState.windowTitle}'`, + ); + log( + ` trans[0] ${firstTrans.fromStateId}→${firstTrans.toStateId} verb=${firstTrans.trigger.verb} success=${firstTrans.success}`, + ); + + // Verify per-state tree files exist. + for (const line of stateLines) { + const s = JSON.parse(line); + const treePath = path.join(runDir, s.treeFile); + if (!existsSync(treePath)) { + throw new Error(`Missing tree file for ${s.id}: ${treePath}`); + } + } + log(` ✓ all state tree files present`); + + await client.appKill({ pid: launch.pid }); + log("DONE"); + } finally { + await client.dispose(); + } +} + +main().catch((e) => { + process.stderr.write(`FAILED: ${e}\n`); + if (e instanceof Error && e.stack) { + process.stderr.write(e.stack + "\n"); + } + process.exit(1); +}); From f13d3074dc2a6ec4197de459842a89b3106058e1 Mon Sep 17 00:00:00 2001 From: Robert Gruen Date: Sun, 3 May 2026 23:03:12 -0700 Subject: [PATCH 08/28] Slice 6b: LLM oracle (typechat) plugged into the explore loop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LlmOracle implements DecisionOracle. ExploreDecision schema (act/stop/ restore) lives at exploreLlmSchema.ts and is loaded as text by TypeChat's TypeScript-JSON validator, the same pattern as discoveryLlmSchema. Postbuild copies it into dist alongside the discovery schema. lib/llm.ts gets a getExploreModel() factory tagged "onboarding:explore". Prompt template includes goal, frontier (rendered as a numbered list with controlType/name/automationId/verbs), recent transitions tail, visited-state ids, and remaining budget. On translation failure the oracle counts consecutive failures and either falls back to the first non-destructive frontier item (single retry) or stops. Smoke against Windows Clock with budget(maxIterations=8): the model systematically navigates Focus sessions → Timer → Alarm → Stopwatch → World clock, discovering 11 distinct states across 8 successful transitions (no failures). State dedup confirmed via revisit on iter 8 (state-004 shows up again with the same fingerprint). Co-Authored-By: Claude Opus 4.7 (1M context) --- ts/packages/agents/onboarding/package.json | 2 +- ts/packages/agents/onboarding/src/lib/llm.ts | 6 + .../src/uiCapture/exploreLlmSchema.ts | 60 +++++++ .../onboarding/src/uiCapture/llmOracle.ts | 154 +++++++++++++++++ .../src/uiCapture/test/llmExploreSmoke.ts | 159 ++++++++++++++++++ 5 files changed, 380 insertions(+), 1 deletion(-) create mode 100644 ts/packages/agents/onboarding/src/uiCapture/exploreLlmSchema.ts create mode 100644 ts/packages/agents/onboarding/src/uiCapture/llmOracle.ts create mode 100644 ts/packages/agents/onboarding/src/uiCapture/test/llmExploreSmoke.ts diff --git a/ts/packages/agents/onboarding/package.json b/ts/packages/agents/onboarding/package.json index 5673c92590..2d0afe9f1f 100644 --- a/ts/packages/agents/onboarding/package.json +++ b/ts/packages/agents/onboarding/package.json @@ -34,7 +34,7 @@ "asc:schemagen": "asc -i ./src/schemaGen/schemaGenSchema.ts -o ./dist/schemaGenSchema.pas.json -t SchemaGenActions", "asc:testing": "asc -i ./src/testing/testingSchema.ts -o ./dist/testingSchema.pas.json -t TestingActions", "build": "concurrently npm:tsc npm:asc:* npm:agc:*", - "postbuild": "copyfiles -u 1 \"src/**/discoveryLlmSchema.ts\" dist", + "postbuild": "copyfiles -u 1 \"src/**/discoveryLlmSchema.ts\" \"src/**/exploreLlmSchema.ts\" dist", "clean": "rimraf --glob dist *.tsbuildinfo *.done.build.log", "prettier": "prettier --check . --ignore-path ../../../.prettierignore", "prettier:fix": "prettier --write . --ignore-path ../../../.prettierignore", diff --git a/ts/packages/agents/onboarding/src/lib/llm.ts b/ts/packages/agents/onboarding/src/lib/llm.ts index 0cc6e5c9b3..261271b38a 100644 --- a/ts/packages/agents/onboarding/src/lib/llm.ts +++ b/ts/packages/agents/onboarding/src/lib/llm.ts @@ -47,3 +47,9 @@ export function getPackagingModel(endpoint?: string): ChatModel { "onboarding:packaging", ]); } + +export function getExploreModel(endpoint?: string): ChatModel { + return openai.createChatModel(endpoint, undefined, undefined, [ + "onboarding:explore", + ]); +} diff --git a/ts/packages/agents/onboarding/src/uiCapture/exploreLlmSchema.ts b/ts/packages/agents/onboarding/src/uiCapture/exploreLlmSchema.ts new file mode 100644 index 0000000000..cb56c0df9a --- /dev/null +++ b/ts/packages/agents/onboarding/src/uiCapture/exploreLlmSchema.ts @@ -0,0 +1,60 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// TypeChat schema for the autonomous-explore decision oracle. +// Loaded as text by TypeChat — keep self-contained, no runtime imports. + +/** One decision per iteration: act / stop / restore. */ +export type ExploreDecision = + | ActDecision + | StopDecision + | RestoreDecision; + +/** Take an action against a control on the current frontier. */ +export type ActDecision = { + /** Always "act". */ + kind: "act"; + /** ID of a frontier item from the input (e.g. "F-007"). Must be one shown to you. */ + frontierId: string; + /** Verb to apply. Must be one of the verbs declared on the chosen frontier item. */ + verb: + | "invoke" + | "toggle" + | "setValue" + | "select" + | "expand" + | "scroll" + | "focus" + | "click"; + /** + * Target value for setValue / toggle / select. Omit for verbs that take no value + * (invoke, focus, click, scroll, expand without an explicit boolean). + */ + value?: string | number | boolean; + /** + * Short prediction of how the app state will change after this action. + * Compared against the observed delta on the next iteration. + */ + expectedDelta: string; + /** One sentence: why this action advances the goal. */ + rationale: string; +}; + +/** End exploration. */ +export type StopDecision = { + /** Always "stop". */ + kind: "stop"; + /** Why exploration is complete (e.g. "all observed states have empty frontier"). */ + reason: string; +}; + +/** + * Reset to baseline. Use when current branch is exhausted or the app is in + * an unhelpful state and a clean slate is needed. + */ +export type RestoreDecision = { + /** Always "restore". */ + kind: "restore"; + /** One sentence: why a restore is preferable to acting in the current state. */ + rationale: string; +}; diff --git a/ts/packages/agents/onboarding/src/uiCapture/llmOracle.ts b/ts/packages/agents/onboarding/src/uiCapture/llmOracle.ts new file mode 100644 index 0000000000..51093590a6 --- /dev/null +++ b/ts/packages/agents/onboarding/src/uiCapture/llmOracle.ts @@ -0,0 +1,154 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { ChatModel } from "aiclient"; +import { loadSchema } from "typeagent"; +import { createJsonTranslator, TypeChatJsonTranslator } from "typechat"; +import { createTypeScriptJsonValidator } from "typechat/ts"; + +import { getExploreModel } from "../lib/llm.js"; +import type { + DecisionInput, + DecisionOracle, + ExploreDecision, + FrontierItem, +} from "./exploreTypes.js"; +import type { ExploreDecision as LlmExploreDecision } from "./exploreLlmSchema.js"; + +const SYSTEM_PROMPT = `You are exploring a Windows desktop application's UI to discover the user-facing actions it offers. Your goal is to drive the app via the controls on its current frontier and observe how the app's state changes. + +Ground rules: +- Pick the action most likely to reveal a NEW state (one we haven't visited). +- Avoid destructive actions (delete/remove/reset/clear) unless the goal requires it. +- Avoid window-management actions (close, minimize, maximize, app-switch). +- Prefer Buttons/MenuItems/ListItems over scrollbars or text fields unless the goal targets them. +- If the same frontier has been picked over and explored without yielding new states, choose "stop" or "restore". +- expectedDelta should be a short prediction in plain English; we'll compare it against what actually happens.`; + +export type LlmOracleOptions = { + goal: string; + model?: ChatModel; + /** + * Maximum number of consecutive translation failures before giving up + * with a "stop" decision. + */ + maxRetries?: number; +}; + +export class LlmOracle implements DecisionOracle { + private readonly translator: TypeChatJsonTranslator; + private readonly goal: string; + private readonly maxRetries: number; + private consecutiveFailures = 0; + + constructor(opts: LlmOracleOptions) { + this.goal = opts.goal; + this.maxRetries = opts.maxRetries ?? 2; + const model = opts.model ?? getExploreModel(); + const schema = loadSchema(["exploreLlmSchema.ts"], import.meta.url); + const validator = createTypeScriptJsonValidator( + schema, + "ExploreDecision", + ); + this.translator = createJsonTranslator( + model, + validator, + ); + } + + async decide(input: DecisionInput): Promise { + const prompt = this.buildPrompt(input); + const result = await this.translator.translate(prompt); + if (!result.success) { + this.consecutiveFailures++; + if (this.consecutiveFailures >= this.maxRetries) { + return { + kind: "stop", + reason: `LLM oracle: ${this.consecutiveFailures} consecutive translation failures (last: ${result.message})`, + }; + } + // Soft fallback: pick the first non-destructive frontier item. + const fallback = input.frontier.find( + (f) => !f.destructiveHint && f.verbs.length > 0, + ); + if (!fallback) { + return { + kind: "stop", + reason: `LLM translation failed and no fallback available: ${result.message}`, + }; + } + const verb = fallback.verbs[0]!.verb; + return { + kind: "act", + frontierId: fallback.id, + verb, + expectedDelta: "(fallback after LLM failure)", + rationale: `fallback: ${result.message}`, + }; + } + this.consecutiveFailures = 0; + return result.data as ExploreDecision; + } + + private buildPrompt(input: DecisionInput): string { + const lines: string[] = []; + lines.push(SYSTEM_PROMPT); + lines.push(""); + lines.push(`Goal: ${this.goal}`); + lines.push(""); + lines.push( + `Iteration: ${input.iteration} (remaining: ${input.budget.remainingIterations}; budget ${input.budget.remainingMs}ms)`, + ); + lines.push( + `Active state: ${input.state.id} '${input.state.windowTitle}'`, + ); + lines.push( + `Visited states: ${input.visitedStates.length} (ids: ${input.visitedStates + .slice(-8) + .map((s) => s.id) + .join(", ")})`, + ); + lines.push(""); + lines.push("Frontier:"); + if (input.frontier.length === 0) { + lines.push(" (empty — no actionable controls in this state)"); + } else { + for (const f of input.frontier.slice(0, 60)) { + lines.push(" " + renderFrontierItem(f)); + } + if (input.frontier.length > 60) { + lines.push(` ... and ${input.frontier.length - 60} more`); + } + } + lines.push(""); + if (input.recentTransitions.length > 0) { + lines.push("Recent actions:"); + for (const t of input.recentTransitions) { + const arrow = t.success ? "→" : "✗"; + const noChange = t.fromStateId === t.toStateId ? " (no change)" : ""; + lines.push( + ` iter ${t.iteration}: ${t.fromStateId} ${arrow} ${t.trigger.verb} ${t.trigger.selector.split("/").pop()} → ${t.toStateId}${noChange}`, + ); + } + lines.push(""); + } + lines.push( + "Decide your next action. Output strictly matches the ExploreDecision schema.", + ); + return lines.join("\n"); + } +} + +function renderFrontierItem(f: FrontierItem): string { + const id = `[${f.id}]`; + const ct = f.controlType; + const label = f.name ?? f.automationId ?? f.className ?? ""; + const aid = f.automationId ? ` aid=${f.automationId}` : ""; + const verbs = f.verbs.map((v) => v.verb).join(","); + const dest = f.destructiveHint ? " (destructive!)" : ""; + return `${id} ${ct} '${truncate(label, 40)}'${aid} verbs:${verbs}${dest}`; +} + +function truncate(s: string, n: number): string { + return s.length > n ? s.slice(0, n - 1) + "…" : s; +} diff --git a/ts/packages/agents/onboarding/src/uiCapture/test/llmExploreSmoke.ts b/ts/packages/agents/onboarding/src/uiCapture/test/llmExploreSmoke.ts new file mode 100644 index 0000000000..9427fcb7cc --- /dev/null +++ b/ts/packages/agents/onboarding/src/uiCapture/test/llmExploreSmoke.ts @@ -0,0 +1,159 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { existsSync, readFileSync, rmSync } from "node:fs"; +import { homedir } from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +// Load ts/.env via Node 20.6+ built-in (avoids adding dotenv as a dep). +const __filename = fileURLToPath(import.meta.url); +// dist/uiCapture/test/llmExploreSmoke.js → six levels up is ts/ which holds .env. +const envPath = path.resolve( + path.dirname(__filename), + "../../../../../..", + ".env", +); +try { + (process as any).loadEnvFile(envPath); +} catch (e) { + process.stderr.write(`[llm] could not load env from ${envPath}: ${e}\n`); +} + +import { runExploration } from "../explorer.js"; +import { HelperClient } from "../helperClient.js"; +import { LlmOracle } from "../llmOracle.js"; + +const CLOCK_AUMID = "Microsoft.WindowsAlarms_8wekyb3d8bbwe!App"; +const GOAL = + "Explore Windows Clock to discover the user-facing actions in each tab " + + "(Focus sessions, Timer, Alarm, Stopwatch, World clock). Prefer breadth: " + + "navigate to tabs that haven't been visited; within a tab, exercise the " + + "primary action(s)."; + +function log(msg: string): void { + process.stdout.write(`[llm] ${msg}\n`); +} + +async function sleep(ms: number): Promise { + await new Promise((res) => setTimeout(res, ms)); +} + +async function main(): Promise { + const workspaceDir = path.join( + homedir(), + ".typeagent", + "onboarding", + "_uic_llm_explore_smoke", + ); + rmSync(workspaceDir, { recursive: true, force: true }); + + const client = await HelperClient.start({ debug: false }); + try { + await client.ping(); + + for (const w of (await client.appList()).filter((x) => + x.title.includes("Clock"), + )) { + await client.appKill({ pid: w.pid }); + } + await sleep(1500); + + log("launching Clock..."); + const launch = await client.appLaunch({ aumid: CLOCK_AUMID }); + await client.eventsIdle({ debounceMs: 800, maxWaitMs: 5000 }); + log(`launched pid=${launch.pid}`); + + let oracle: LlmOracle; + try { + oracle = new LlmOracle({ goal: GOAL, maxRetries: 2 }); + } catch (e) { + log(`SKIP: failed to construct LlmOracle (likely missing API keys): ${e}`); + await client.appKill({ pid: launch.pid }); + return; + } + + log("running exploration with LLM oracle (budget: 8 iterations / 60s)..."); + const metrics = await runExploration({ + client, + oracle, + workspaceDir, + rootSelector: launch.mainWindow, + captureScreenshots: false, + treeMaxDepth: 8, + budget: { + maxIterations: 8, + maxWallClockMs: 60_000, + maxStates: 12, + convergenceThreshold: 4, + historyTailSize: 4, + }, + onIteration: ({ iteration, state, decision }) => { + if (decision.kind === "act") { + log( + ` iter ${iteration}: ${state.id} → ${decision.verb} ${decision.frontierId}: ${decision.rationale}`, + ); + } else { + log( + ` iter ${iteration}: ${state.id} → ${decision.kind}: ${ + "reason" in decision + ? decision.reason + : "rationale" in decision + ? decision.rationale + : "" + }`, + ); + } + }, + }); + + log("metrics:"); + log( + ` iterations=${metrics.iterations} states=${metrics.statesDiscovered} transitions=${metrics.transitionsRecorded}`, + ); + log( + ` successful=${metrics.successfulTransitions} failed=${metrics.failedTransitions}`, + ); + log(` stopReason=${metrics.stopReason} walltime=${metrics.walltimeMs}ms`); + + const runDir = path.join(workspaceDir, "runs", metrics.runId); + const stateLines = readFileSync( + path.join(runDir, "states.jsonl"), + "utf8", + ) + .split("\n") + .filter((l) => l.length > 0); + const transitionLines = existsSync( + path.join(runDir, "transitions.jsonl"), + ) + ? readFileSync(path.join(runDir, "transitions.jsonl"), "utf8") + .split("\n") + .filter((l) => l.length > 0) + : []; + log( + ` on disk: ${stateLines.length} states, ${transitionLines.length} transitions`, + ); + + // Show distinct windowTitles visited. + const titles = new Set(); + for (const line of stateLines) { + titles.add(JSON.parse(line).windowTitle); + } + log( + ` distinct window titles: ${[...titles].join(", ") || "(none)"}`, + ); + + await client.appKill({ pid: launch.pid }); + log("DONE"); + } finally { + await client.dispose(); + } +} + +main().catch((e) => { + process.stderr.write(`FAILED: ${e}\n`); + if (e instanceof Error && e.stack) { + process.stderr.write(e.stack + "\n"); + } + process.exit(1); +}); From 5f6a1b0c3bb8cb75a0476667032ec4ca0ba69a04 Mon Sep 17 00:00:00 2001 From: Robert Gruen Date: Sun, 3 May 2026 23:09:53 -0700 Subject: [PATCH 09/28] =?UTF-8?q?Slice=207:=20synthesis=20pipeline=20(grap?= =?UTF-8?q?h=20=E2=86=92=20discoveredActions.json)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three TypeChat schemas in synthesisLlmSchema.ts: NeutralStatesClassification (per-state isNeutral + tabOrSection label), ClusteringResult (group chunks by user-intent), and SynthesizedAction (final action with playback recipe, parameters, preconditions). Postbuild copies the schema into dist. synthesizer.ts pipelines: 1. classifyNeutralStates — one LLM call covering all states, summarized by their actionable controls + window title 2. chunkTransitions — deterministic; cuts the transition log at neutral boundaries, trailing-non-neutral chunks flagged isNeutralEnd=false 3. clusterChunks — one LLM call, intent-naming in camelCase verb-noun 4. synthesizeOneCluster — one LLM call per cluster, builds full PlaybackStep[] with valueRef/valueLiteral params extracted from chunk variations 5. writeOutput — discoveredActions.json (matches schema phraseGen consumes) + synthesisReport.md for the human approval gate End-to-end smoke: explore Clock (8 iterations, 11 states, 8 transitions) → synthesize (3 actions, all "navigateToTab" by destination, with full selector paths in playback). discoveredActions.json contains valid, replay-ready recipes. Quality finding: clustering didn't merge functionally identical chunks into one parameterized action. Design called for `navigateToTab(tab: "Focus sessions" | "Timer" | "Alarm" | ...)`, got three separate `navigateToTab` actions split by destination instead. The clustering prompt needs stronger emphasis on parameterization-via- variation (or a two-pass merge step). Mechanism is correct; output quality has room. Other findings: neutral classification produced sensible per-state labels (focusTab.setup / timerTab.empty / alarmTab.empty / etc.) just from actionable-control summaries; chunks merge correctly when intermediate states are non-neutral (2-step playbacks emerged where exploration crossed two neutrals). This closes the seven-slice arc: helper → verbs → snapshot → calibration → record → autonomous loop → synthesis. Branch is now a working end-to-end UIA-based onboarding pipeline. Co-Authored-By: Claude Opus 4.7 (1M context) --- ts/packages/agents/onboarding/package.json | 2 +- .../src/uiCapture/synthesisLlmSchema.ts | 105 ++++ .../onboarding/src/uiCapture/synthesizer.ts | 493 ++++++++++++++++++ .../src/uiCapture/test/synthesizeSmoke.ts | 160 ++++++ 4 files changed, 759 insertions(+), 1 deletion(-) create mode 100644 ts/packages/agents/onboarding/src/uiCapture/synthesisLlmSchema.ts create mode 100644 ts/packages/agents/onboarding/src/uiCapture/synthesizer.ts create mode 100644 ts/packages/agents/onboarding/src/uiCapture/test/synthesizeSmoke.ts diff --git a/ts/packages/agents/onboarding/package.json b/ts/packages/agents/onboarding/package.json index 2d0afe9f1f..03e15531bd 100644 --- a/ts/packages/agents/onboarding/package.json +++ b/ts/packages/agents/onboarding/package.json @@ -34,7 +34,7 @@ "asc:schemagen": "asc -i ./src/schemaGen/schemaGenSchema.ts -o ./dist/schemaGenSchema.pas.json -t SchemaGenActions", "asc:testing": "asc -i ./src/testing/testingSchema.ts -o ./dist/testingSchema.pas.json -t TestingActions", "build": "concurrently npm:tsc npm:asc:* npm:agc:*", - "postbuild": "copyfiles -u 1 \"src/**/discoveryLlmSchema.ts\" \"src/**/exploreLlmSchema.ts\" dist", + "postbuild": "copyfiles -u 1 \"src/**/discoveryLlmSchema.ts\" \"src/**/exploreLlmSchema.ts\" \"src/**/synthesisLlmSchema.ts\" dist", "clean": "rimraf --glob dist *.tsbuildinfo *.done.build.log", "prettier": "prettier --check . --ignore-path ../../../.prettierignore", "prettier:fix": "prettier --write . --ignore-path ../../../.prettierignore", diff --git a/ts/packages/agents/onboarding/src/uiCapture/synthesisLlmSchema.ts b/ts/packages/agents/onboarding/src/uiCapture/synthesisLlmSchema.ts new file mode 100644 index 0000000000..07ddf79eb2 --- /dev/null +++ b/ts/packages/agents/onboarding/src/uiCapture/synthesisLlmSchema.ts @@ -0,0 +1,105 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// TypeChat schemas for the synthesis pass: neutral-state classification, +// chunk clustering, and synthesized-action generation. Loaded as text by +// TypeChat — keep self-contained, no runtime imports. + +/* ---------- Neutral state classification ---------- */ + +/** Classify a batch of captured states as "neutral" (settled, awaiting next user action) or not. */ +export type NeutralStatesClassification = { + classifications: NeutralStateClassification[]; +}; + +export type NeutralStateClassification = { + /** State id from the input (e.g. "state-007"). */ + stateId: string; + /** True if this state is a settled rest point — the user could start a new task from here. + * False if this state is mid-flow (e.g., a modal dialog requires resolution, an animation in progress). */ + isNeutral: boolean; + /** One sentence: why neutral or not. */ + reason: string; + /** Optional human-friendly label like "alarmTab.empty" or "timerTab.running". Camel-and-dot case. */ + tabOrSection?: string; +}; + +/* ---------- Chunk clustering ---------- */ + +/** Group chunks by user-meaningful intent. */ +export type ClusteringResult = { + clusters: ChunkCluster[]; + /** Chunks that don't fit any cluster (e.g., partial paths the explorer abandoned). */ + orphans?: string[]; +}; + +export type ChunkCluster = { + /** Stable id for this cluster (e.g. "cl-001"). */ + clusterId: string; + /** camelCase verb-noun, e.g. "createAlarm", "startTimer", "navigateToTab". */ + intentName: string; + /** One sentence describing what the user accomplishes by performing this intent. */ + shortDescription: string; + /** Ids of chunks (from the input) that belong to this cluster. */ + chunkIds: string[]; +}; + +/* ---------- Synthesized action ---------- */ + +/** A user-meaningful action ready for downstream phases (phraseGen / schemaGen). */ +export type SynthesizedAction = { + /** camelCase verb-noun (matches the cluster's intentName by default). */ + actionName: string; + /** Short user-facing description, suitable for help text. */ + description: string; + /** Parameters extracted from the cluster's chunk variations (empty if all chunks were identical). */ + parameters: ParamSpec[]; + /** Sequenced steps to replay this action at runtime. */ + playback: PlaybackStep[]; + /** Required app state before invoking this action. */ + preconditions: { neutralState: string; description: string }; + /** What the app looks like after the action completes successfully. */ + postconditions: { description: string }; + /** True if the action irreversibly destroys user data (deletes, resets, clears). */ + destructive: boolean; +}; + +export type ParamSpec = { + /** camelCase, e.g. "name", "minutes", "enabled". */ + name: string; + /** "string" | "number" | "boolean" | "enum". */ + type: "string" | "number" | "boolean" | "enum"; + /** When type is "enum", the allowed values. */ + enumValues?: string[]; + /** One short sentence describing what this parameter controls. */ + description: string; + /** Concrete values observed in the source chunks. */ + examples: Array; +}; + +export type PlaybackStep = { + /** Selector path of the control to act on. */ + selector: string; + /** Verb to apply to that control. */ + verb: + | "invoke" + | "toggle" + | "setValue" + | "select" + | "expand" + | "scroll" + | "focus" + | "click"; + /** + * If this step's value is parameterized, ${paramName} reference. Set + * either valueRef OR valueLiteral, not both. Omit both for verbs with + * no value (invoke/focus/click). + */ + valueRef?: string; + /** Constant value for verbs that need one. */ + valueLiteral?: string | number | boolean; + /** Wait for UIA to settle after this step. Default true for invoke/select; false otherwise. */ + waitForIdle?: boolean; + /** Optional: short description of what changed after this step (sanity check at replay time). */ + expectedDeltaSummary?: string; +}; diff --git a/ts/packages/agents/onboarding/src/uiCapture/synthesizer.ts b/ts/packages/agents/onboarding/src/uiCapture/synthesizer.ts new file mode 100644 index 0000000000..e9ea71e95f --- /dev/null +++ b/ts/packages/agents/onboarding/src/uiCapture/synthesizer.ts @@ -0,0 +1,493 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { ChatModel } from "aiclient"; +import { existsSync, readFileSync, writeFileSync } from "node:fs"; +import path from "node:path"; +import { loadSchema } from "typeagent"; +import { createJsonTranslator, TypeChatJsonTranslator } from "typechat"; +import { createTypeScriptJsonValidator } from "typechat/ts"; + +import { getExploreModel } from "../lib/llm.js"; +import type { + CapturedState, + CapturedTransition, +} from "./exploreTypes.js"; +import type { + ClusteringResult, + NeutralStatesClassification, + NeutralStateClassification, + SynthesizedAction, +} from "./synthesisLlmSchema.js"; +import type { TreeNode } from "./types.js"; + +export type SynthesisInput = { + runDir: string; + integrationName: string; + /** Override the default LLM model. Defaults to getExploreModel(). */ + model?: ChatModel; +}; + +export type SynthesisOutput = { + integrationName: string; + actions: SynthesizedAction[]; + neutralStates: NeutralStateClassification[]; + clusters: ClusteringResult; + chunkCount: number; + discoveredActionsPath: string; + reportPath: string; +}; + +type Chunk = { + id: string; + transitionIds: string[]; + startStateId: string; + endStateId: string; + isNeutralStart: boolean; + isNeutralEnd: boolean; +}; + +type LoadedGraph = { + states: CapturedState[]; + transitions: CapturedTransition[]; + runDir: string; +}; + +const SYSTEM_PROMPT_BASE = `You are post-processing a UI-Automation exploration trace into a list of user-meaningful actions for a Windows desktop app. Be precise and conservative — don't invent capabilities the trace doesn't show.`; + +export async function synthesize( + input: SynthesisInput, +): Promise { + const model = input.model ?? getExploreModel(); + const graph = loadGraph(input.runDir); + if (graph.states.length === 0) { + throw new Error(`No states found in ${input.runDir}`); + } + if (graph.transitions.length === 0) { + throw new Error(`No transitions found in ${input.runDir}`); + } + + // Step 1: classify neutral states (one LLM call covering all states). + const neutralResult = await classifyNeutralStates(model, graph); + const neutralByState = new Map(); + for (const c of neutralResult.classifications) { + neutralByState.set(c.stateId, c); + } + + // Step 2: chunk transitions deterministically using the neutrals. + const chunks = chunkTransitions(graph.transitions, neutralByState); + + // Step 3: cluster chunks by intent (one LLM call). + let clusters: ClusteringResult = { clusters: [], orphans: [] }; + if (chunks.length > 0) { + clusters = await clusterChunks( + model, + chunks, + graph, + neutralByState, + ); + } + + // Step 4: synthesize one action per cluster. + const actions: SynthesizedAction[] = []; + for (const cluster of clusters.clusters) { + const action = await synthesizeOneCluster( + model, + cluster, + chunks, + graph, + neutralByState, + ); + if (action) { + actions.push(action); + } + } + + // Step 5: persist outputs. + const discoveredActionsPath = path.join(input.runDir, "discoveredActions.json"); + writeFileSync( + discoveredActionsPath, + JSON.stringify( + { + version: 1, + integrationName: input.integrationName, + discoveredAt: new Date().toISOString(), + source: "uiCapture", + actions, + }, + null, + 2, + ), + ); + const reportPath = path.join(input.runDir, "synthesisReport.md"); + writeFileSync( + reportPath, + renderReport({ + integrationName: input.integrationName, + graph, + chunks, + neutralByState, + clusters, + actions, + }), + ); + + return { + integrationName: input.integrationName, + actions, + neutralStates: neutralResult.classifications, + clusters, + chunkCount: chunks.length, + discoveredActionsPath, + reportPath, + }; +} + +/* ---------- Step 1: neutral classification ---------- */ + +async function classifyNeutralStates( + model: ChatModel, + graph: LoadedGraph, +): Promise { + const translator = makeTranslator( + model, + "NeutralStatesClassification", + ); + const lines: string[] = []; + lines.push(SYSTEM_PROMPT_BASE); + lines.push(""); + lines.push( + "Task: classify each state below as `isNeutral=true` if it's a settled rest point where a user could start a new task, or `isNeutral=false` if it's mid-flow (modal dialogs, animations in progress, transient prompts). Also assign each a short tabOrSection label like 'alarmTab.empty' or 'timerTab.running' when applicable.", + ); + lines.push(""); + for (const state of graph.states) { + const tree = loadStateTree(graph, state.id); + lines.push(summarizeState(state, tree)); + lines.push(""); + } + lines.push("Return a NeutralStatesClassification with one entry per stateId."); + const result = await translator.translate(lines.join("\n")); + if (!result.success) { + // Fallback: assume all states are neutral. Better signal than silent failure. + return { + classifications: graph.states.map((s) => ({ + stateId: s.id, + isNeutral: true, + reason: `(fallback after LLM failure: ${result.message})`, + })), + }; + } + return result.data; +} + +function summarizeState(state: CapturedState, tree: TreeNode): string { + const actionable: string[] = []; + function walk(n: TreeNode): void { + if (n.patterns.length > 0 && n.isEnabled && !n.isOffscreen) { + const label = n.name ?? n.automationId ?? n.className ?? ""; + actionable.push( + `${n.controlType}${label ? ` '${truncate(label, 40)}'` : ""} [${n.patterns.join(",")}]`, + ); + } + for (const c of n.children) walk(c); + } + walk(tree); + const head = `${state.id} window='${state.windowTitle}' label='${state.label ?? ""}'`; + const limited = actionable.slice(0, 30); + const overflow = + actionable.length > limited.length + ? `\n ... +${actionable.length - limited.length} more` + : ""; + return `${head}\n controls: ${limited.join("; ")}${overflow}`; +} + +/* ---------- Step 2: chunking ---------- */ + +function chunkTransitions( + transitions: CapturedTransition[], + neutralByState: Map, +): Chunk[] { + const chunks: Chunk[] = []; + let chunkId = 1; + let pending: CapturedTransition[] = []; + let pendingStart: string | null = null; + + const isNeutral = (id: string) => + neutralByState.get(id)?.isNeutral !== false; // unknown → assume neutral + + for (const t of transitions) { + if (pendingStart === null) { + pendingStart = t.fromStateId; + } + pending.push(t); + if (isNeutral(t.toStateId)) { + chunks.push({ + id: `C-${chunkId.toString().padStart(3, "0")}`, + transitionIds: pending.map((p) => p.id), + startStateId: pendingStart!, + endStateId: t.toStateId, + isNeutralStart: isNeutral(pendingStart!), + isNeutralEnd: true, + }); + chunkId++; + pending = []; + pendingStart = null; + } + } + // Trailing non-neutral path (incomplete chunk). + if (pending.length > 0) { + const last = pending[pending.length - 1]!; + chunks.push({ + id: `C-${chunkId.toString().padStart(3, "0")}`, + transitionIds: pending.map((p) => p.id), + startStateId: pendingStart!, + endStateId: last.toStateId, + isNeutralStart: isNeutral(pendingStart!), + isNeutralEnd: false, + }); + } + return chunks; +} + +/* ---------- Step 3: clustering ---------- */ + +async function clusterChunks( + model: ChatModel, + chunks: Chunk[], + graph: LoadedGraph, + neutralByState: Map, +): Promise { + const translator = makeTranslator(model, "ClusteringResult"); + const lines: string[] = []; + lines.push(SYSTEM_PROMPT_BASE); + lines.push(""); + lines.push( + "Task: group these UI-action chunks by user-meaningful intent. Same intent across multiple chunks (e.g., creating an alarm with different times) belongs to one cluster. Pure tab-switching navigation should usually become one cluster (e.g., 'navigateToTab').", + ); + lines.push( + "Use camelCase verb-noun intent names. If a chunk's purpose is unclear or partial, list it under `orphans` instead.", + ); + lines.push(""); + for (const ch of chunks) { + lines.push(renderChunkForLLM(ch, graph, neutralByState)); + } + lines.push(""); + lines.push("Return a ClusteringResult."); + const result = await translator.translate(lines.join("\n")); + if (!result.success) { + return { clusters: [], orphans: chunks.map((c) => c.id) }; + } + return result.data; +} + +function renderChunkForLLM( + chunk: Chunk, + graph: LoadedGraph, + neutralByState: Map, +): string { + const startLabel = neutralByState.get(chunk.startStateId)?.tabOrSection ?? ""; + const endLabel = neutralByState.get(chunk.endStateId)?.tabOrSection ?? ""; + const lines: string[] = []; + lines.push( + `Chunk ${chunk.id}: ${chunk.startStateId}${startLabel ? ` (${startLabel})` : ""} → ${chunk.endStateId}${endLabel ? ` (${endLabel})` : ""}`, + ); + for (const tid of chunk.transitionIds) { + const t = graph.transitions.find((x) => x.id === tid); + if (!t) continue; + const value = + t.trigger.value !== undefined + ? ` value=${JSON.stringify(t.trigger.value)}` + : ""; + const tail = lastSegment(t.trigger.selector); + lines.push(` ${t.trigger.verb} ${tail}${value}`); + } + return lines.join("\n"); +} + +/* ---------- Step 4: synthesis ---------- */ + +async function synthesizeOneCluster( + model: ChatModel, + cluster: ClusteringResult["clusters"][number], + chunks: Chunk[], + graph: LoadedGraph, + neutralByState: Map, +): Promise { + const translator = makeTranslator(model, "SynthesizedAction"); + const memberChunks = chunks.filter((c) => cluster.chunkIds.includes(c.id)); + if (memberChunks.length === 0) return null; + + const lines: string[] = []; + lines.push(SYSTEM_PROMPT_BASE); + lines.push(""); + lines.push( + `Task: synthesize a single SynthesizedAction for the intent '${cluster.intentName}' (${cluster.shortDescription}).`, + ); + lines.push( + `This intent was observed across ${memberChunks.length} chunk(s). Each chunk is a sequence of (selector, verb, value) tuples that together accomplish the intent.`, + ); + lines.push(""); + lines.push( + "For the playback recipe: include every step needed to replay this action from the precondition state. Use full selector paths exactly as they appear in the chunks. Where chunks differ in their value at a step, extract a parameter and use ${paramName} via valueRef. Where chunks all share a value at a step, use valueLiteral.", + ); + lines.push( + "Set destructive=true if the action removes user data (delete/reset/clear). Otherwise false.", + ); + lines.push(""); + for (const ch of memberChunks) { + const startLabel = neutralByState.get(ch.startStateId)?.tabOrSection ?? ""; + const endLabel = neutralByState.get(ch.endStateId)?.tabOrSection ?? ""; + lines.push( + `Chunk ${ch.id}: ${ch.startStateId}${startLabel ? ` (${startLabel})` : ""} → ${ch.endStateId}${endLabel ? ` (${endLabel})` : ""}`, + ); + for (const tid of ch.transitionIds) { + const t = graph.transitions.find((x) => x.id === tid); + if (!t) continue; + const value = + t.trigger.value !== undefined + ? ` value=${JSON.stringify(t.trigger.value)}` + : ""; + lines.push( + ` ${t.trigger.verb} selector="${t.trigger.selector}"${value}`, + ); + } + lines.push(""); + } + lines.push("Return a SynthesizedAction."); + const result = await translator.translate(lines.join("\n")); + if (!result.success) { + return null; + } + return result.data; +} + +/* ---------- Output report ---------- */ + +function renderReport(args: { + integrationName: string; + graph: LoadedGraph; + chunks: Chunk[]; + neutralByState: Map; + clusters: ClusteringResult; + actions: SynthesizedAction[]; +}): string { + const lines: string[] = []; + lines.push(`# Synthesis report: ${args.integrationName}`); + lines.push(""); + lines.push( + `Run dir: \`${args.graph.runDir}\``, + ); + lines.push( + `States: ${args.graph.states.length} · Transitions: ${args.graph.transitions.length} · Chunks: ${args.chunks.length}`, + ); + lines.push( + `Clusters: ${args.clusters.clusters.length} · Orphans: ${args.clusters.orphans?.length ?? 0} · Synthesized actions: ${args.actions.length}`, + ); + lines.push(""); + lines.push("## Neutral classifications"); + for (const c of [...args.neutralByState.values()]) { + const flag = c.isNeutral ? "✓" : "✗"; + const label = c.tabOrSection ? ` [${c.tabOrSection}]` : ""; + lines.push(`- ${flag} ${c.stateId}${label}: ${c.reason}`); + } + lines.push(""); + lines.push("## Clusters"); + for (const cl of args.clusters.clusters) { + lines.push( + `- **${cl.intentName}** (${cl.clusterId}): ${cl.shortDescription} — chunks: ${cl.chunkIds.join(", ")}`, + ); + } + if (args.clusters.orphans && args.clusters.orphans.length > 0) { + lines.push(""); + lines.push( + `Orphan chunks: ${args.clusters.orphans.join(", ")}`, + ); + } + lines.push(""); + lines.push("## Synthesized actions"); + for (const a of args.actions) { + lines.push(`### ${a.actionName}${a.destructive ? " (destructive)" : ""}`); + lines.push(a.description); + lines.push(""); + lines.push("Parameters:"); + if (a.parameters.length === 0) { + lines.push(" (none)"); + } else { + for (const p of a.parameters) { + const eg = p.examples + .slice(0, 5) + .map((v) => JSON.stringify(v)) + .join(", "); + lines.push( + ` - \`${p.name}\` (${p.type}${p.enumValues ? `: ${p.enumValues.join("|")}` : ""}) — ${p.description}${eg ? `; examples: ${eg}` : ""}`, + ); + } + } + lines.push(""); + lines.push("Playback:"); + for (let i = 0; i < a.playback.length; i++) { + const step = a.playback[i]!; + const valuePart = + step.valueRef !== undefined + ? ` ${step.valueRef}` + : step.valueLiteral !== undefined + ? ` ${JSON.stringify(step.valueLiteral)}` + : ""; + lines.push( + ` ${i + 1}. ${step.verb}${valuePart} on \`${step.selector}\``, + ); + } + lines.push(""); + lines.push(`Preconditions: ${a.preconditions.description}`); + lines.push(`Postconditions: ${a.postconditions.description}`); + lines.push(""); + } + return lines.join("\n"); +} + +/* ---------- helpers ---------- */ + +function makeTranslator( + model: ChatModel, + typeName: string, +): TypeChatJsonTranslator { + const schema = loadSchema(["synthesisLlmSchema.ts"], import.meta.url); + const validator = createTypeScriptJsonValidator(schema, typeName); + return createJsonTranslator(model, validator); +} + +function loadGraph(runDir: string): LoadedGraph { + const statesFile = path.join(runDir, "states.jsonl"); + const transitionsFile = path.join(runDir, "transitions.jsonl"); + if (!existsSync(statesFile) || !existsSync(transitionsFile)) { + throw new Error(`Missing states.jsonl or transitions.jsonl in ${runDir}`); + } + const states = readFileSync(statesFile, "utf8") + .split("\n") + .filter((l) => l.length > 0) + .map((l) => JSON.parse(l) as CapturedState); + const transitions = readFileSync(transitionsFile, "utf8") + .split("\n") + .filter((l) => l.length > 0) + .map((l) => JSON.parse(l) as CapturedTransition); + return { states, transitions, runDir }; +} + +function loadStateTree(graph: LoadedGraph, stateId: string): TreeNode { + const state = graph.states.find((s) => s.id === stateId); + if (!state) { + throw new Error(`No such state: ${stateId}`); + } + return JSON.parse( + readFileSync(path.join(graph.runDir, state.treeFile), "utf8"), + ) as TreeNode; +} + +function lastSegment(selector: string): string { + const segs = selector.split("/").filter((s) => s.length > 0); + return segs[segs.length - 1] ?? selector; +} + +function truncate(s: string, n: number): string { + return s.length > n ? s.slice(0, n - 1) + "…" : s; +} diff --git a/ts/packages/agents/onboarding/src/uiCapture/test/synthesizeSmoke.ts b/ts/packages/agents/onboarding/src/uiCapture/test/synthesizeSmoke.ts new file mode 100644 index 0000000000..00c7e1e4f1 --- /dev/null +++ b/ts/packages/agents/onboarding/src/uiCapture/test/synthesizeSmoke.ts @@ -0,0 +1,160 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { existsSync, readFileSync, rmSync } from "node:fs"; +import { homedir } from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const __filename = fileURLToPath(import.meta.url); +const envPath = path.resolve( + path.dirname(__filename), + "../../../../../..", + ".env", +); +try { + (process as any).loadEnvFile(envPath); +} catch (e) { + process.stderr.write(`[syn] could not load env from ${envPath}: ${e}\n`); +} + +import { runExploration } from "../explorer.js"; +import { HelperClient } from "../helperClient.js"; +import { LlmOracle } from "../llmOracle.js"; +import { synthesize } from "../synthesizer.js"; + +const CLOCK_AUMID = "Microsoft.WindowsAlarms_8wekyb3d8bbwe!App"; +const GOAL = + "Explore Windows Clock by visiting each tab (Focus sessions, Timer, " + + "Alarm, Stopwatch, World clock) at least once. Prefer breadth over depth."; + +function log(msg: string): void { + process.stdout.write(`[syn] ${msg}\n`); +} + +async function sleep(ms: number): Promise { + await new Promise((res) => setTimeout(res, ms)); +} + +async function main(): Promise { + const workspaceDir = path.join( + homedir(), + ".typeagent", + "onboarding", + "_uic_synth_smoke", + ); + rmSync(workspaceDir, { recursive: true, force: true }); + + const client = await HelperClient.start({ debug: false }); + try { + for (const w of (await client.appList()).filter((x) => + x.title.includes("Clock"), + )) { + await client.appKill({ pid: w.pid }); + } + await sleep(1500); + + log("launching Clock..."); + const launch = await client.appLaunch({ aumid: CLOCK_AUMID }); + await client.eventsIdle({ debounceMs: 800, maxWaitMs: 5000 }); + + log("=== phase 1: exploration ==="); + const oracle = new LlmOracle({ goal: GOAL, maxRetries: 2 }); + const metrics = await runExploration({ + client, + oracle, + workspaceDir, + rootSelector: launch.mainWindow, + captureScreenshots: false, + treeMaxDepth: 8, + budget: { + maxIterations: 8, + maxWallClockMs: 60_000, + maxStates: 12, + convergenceThreshold: 4, + historyTailSize: 4, + }, + onIteration: ({ iteration, state, decision }) => { + if (decision.kind === "act") { + log( + ` iter ${iteration}: ${state.id} → ${decision.verb} ${decision.frontierId}`, + ); + } else { + log(` iter ${iteration}: ${state.id} → ${decision.kind}`); + } + }, + }); + log( + `explore done: ${metrics.iterations} iter, ${metrics.statesDiscovered} states, ${metrics.transitionsRecorded} transitions, stop=${metrics.stopReason}`, + ); + + await client.appKill({ pid: launch.pid }); + + log("=== phase 2: synthesis ==="); + const runDir = path.join(workspaceDir, "runs", metrics.runId); + const result = await synthesize({ + runDir, + integrationName: "windowsClock", + }); + log( + `synthesis done: ${result.actions.length} action(s), ${result.clusters.clusters.length} cluster(s), ${result.chunkCount} chunk(s)`, + ); + + for (const a of result.actions) { + log( + ` action: ${a.actionName}${a.destructive ? " (destructive)" : ""}`, + ); + log(` description: ${a.description}`); + for (const p of a.parameters) { + const eg = p.examples + .slice(0, 3) + .map((v) => JSON.stringify(v)) + .join(", "); + log( + ` param ${p.name}: ${p.type}${eg ? ` (e.g. ${eg})` : ""}`, + ); + } + log(` playback steps: ${a.playback.length}`); + for (let i = 0; i < a.playback.length && i < 4; i++) { + const s = a.playback[i]!; + const valPart = + s.valueRef !== undefined + ? ` ${s.valueRef}` + : s.valueLiteral !== undefined + ? ` ${JSON.stringify(s.valueLiteral)}` + : ""; + log( + ` ${i + 1}. ${s.verb}${valPart} on ${s.selector.split("/").pop()}`, + ); + } + if (a.playback.length > 4) { + log(` ... +${a.playback.length - 4} more`); + } + } + + if (!existsSync(result.discoveredActionsPath)) { + throw new Error(`Missing ${result.discoveredActionsPath}`); + } + if (!existsSync(result.reportPath)) { + throw new Error(`Missing ${result.reportPath}`); + } + const json = JSON.parse( + readFileSync(result.discoveredActionsPath, "utf8"), + ); + log( + `discoveredActions.json: ${json.actions.length} action(s), version=${json.version}`, + ); + log(`report: ${result.reportPath}`); + log("DONE"); + } finally { + await client.dispose(); + } +} + +main().catch((e) => { + process.stderr.write(`FAILED: ${e}\n`); + if (e instanceof Error && e.stack) { + process.stderr.write(e.stack + "\n"); + } + process.exit(1); +}); From d2b841e61c5ff0509b005d907fcbe4c83e63ff51 Mon Sep 17 00:00:00 2001 From: Robert Gruen Date: Sun, 3 May 2026 23:25:33 -0700 Subject: [PATCH 10/28] =?UTF-8?q?End-to-end=20Clock=20proof:=20crawl=20?= =?UTF-8?q?=E2=86=92=20synthesize=20=E2=86=92=20replay=20=E2=86=92=20real?= =?UTF-8?q?=20alarm=20created?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Built an end-to-end demo proving the design works on Windows Clock: clockCrawl.ts: full crawl with snapshot/restore safety net. - inferSnapshotPolicy auto-detects the 3 UWP folders (LocalState + Settings + RoamingState under Microsoft.WindowsAlarms_*) - captures baseline (82KB) - drives Clock for 25 iterations against a task-oriented goal (create alarm, create timer, exercise stopwatch...) - synthesis produces 4 actions with parameters and playback recipes - restores baseline at the end playbackExecutor.ts: generic SynthesizedAction → executed steps. - resolves valueRef/${param} substitution against a runtime params map - dispatches each step's verb to the corresponding do.* RPC - waits for UIA idle after invoke/select by default clockAgentDemo.ts: replays a crawled action with NEW parameters. - loads discoveredActions.json from the most recent run - restores Clock to the baseline snapshot (known starting point) - runs createAlarm({alarmName: "Crawled Demo Alarm", hour: 8, minute: 15}) - verifies a new AlarmViewGrid DataItem named "Edit alarm, Crawled Demo Alarm, 8:15AM, Only once, " appears in the tree - restores baseline again to leave Clock as we found it Result: the LLM successfully crawled Clock, the synthesizer extracted correct UIA paths through Popup → EditFlyout → ContentScrollViewer → DurationPicker → HourPicker, and the executor replayed those exact paths to create a brand new alarm with new parameter values not seen during the crawl. This validates the entire design end to end: helper → exploration → state graph → synthesis → discoveredActions.json → playback executor → real Windows alarm Quality findings still standing: - Clustering didn't merge tab-switching chunks into one parameterized navigateToTab; got 3 by-destination clusters + a mislabeled "12-step navigateToTab" that's actually the create-alarm-then-create-timer full flow. - Verification predicate in the demo had to use DataItem with AutomationId="AlarmViewGrid", not ListItem with Toggle — alarm rows don't render the way I first guessed. (Synthesis could be enhanced to emit better postcondition assertions.) Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/uiCapture/playbackExecutor.ts | 185 +++++++++++++++ .../src/uiCapture/test/clockAgentDemo.ts | 218 ++++++++++++++++++ .../src/uiCapture/test/clockCrawl.ts | 206 +++++++++++++++++ 3 files changed, 609 insertions(+) create mode 100644 ts/packages/agents/onboarding/src/uiCapture/playbackExecutor.ts create mode 100644 ts/packages/agents/onboarding/src/uiCapture/test/clockAgentDemo.ts create mode 100644 ts/packages/agents/onboarding/src/uiCapture/test/clockCrawl.ts diff --git a/ts/packages/agents/onboarding/src/uiCapture/playbackExecutor.ts b/ts/packages/agents/onboarding/src/uiCapture/playbackExecutor.ts new file mode 100644 index 0000000000..5b6fe81343 --- /dev/null +++ b/ts/packages/agents/onboarding/src/uiCapture/playbackExecutor.ts @@ -0,0 +1,185 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import type { HelperClient } from "./helperClient.js"; +import type { + PlaybackStep, + SynthesizedAction, +} from "./synthesisLlmSchema.js"; + +export type PlaybackParams = Record; + +export type PlaybackStepResult = { + stepIndex: number; + selector: string; + verb: string; + value?: string | number | boolean; + success: boolean; + errorMessage?: string; + durationMs: number; +}; + +export type PlaybackResult = { + actionName: string; + success: boolean; + steps: PlaybackStepResult[]; + failedAtStep?: number; +}; + +export type PlaybackExecutorOptions = { + client: HelperClient; + /** Wait between steps that don't have waitForIdle set explicitly. */ + defaultIdleDebounceMs?: number; + defaultIdleMaxWaitMs?: number; + /** Stop on first failed step. Default true. */ + stopOnError?: boolean; +}; + +/** + * Replay a synthesized action against the helper. + * + * Resolves `valueRef` references against the supplied `params` map. For each + * step, dispatches the verb to the appropriate `do.*` RPC. Optionally waits + * for UIA idle between steps. + */ +export async function executePlayback( + action: SynthesizedAction, + params: PlaybackParams, + opts: PlaybackExecutorOptions, +): Promise { + const stopOnError = opts.stopOnError ?? true; + const debounceMs = opts.defaultIdleDebounceMs ?? 600; + const maxWaitMs = opts.defaultIdleMaxWaitMs ?? 4000; + const stepResults: PlaybackStepResult[] = []; + let success = true; + let failedAtStep: number | undefined; + + for (let i = 0; i < action.playback.length; i++) { + const step = action.playback[i]!; + const value = resolveValue(step, params); + const start = Date.now(); + let stepSuccess = true; + let errorMessage: string | undefined; + + try { + await executeStep(opts.client, step, value); + } catch (e) { + stepSuccess = false; + errorMessage = e instanceof Error ? e.message : String(e); + } + + const stepResult: PlaybackStepResult = { + stepIndex: i, + selector: step.selector, + verb: step.verb, + ...(value !== undefined ? { value } : {}), + success: stepSuccess, + ...(errorMessage !== undefined ? { errorMessage } : {}), + durationMs: Date.now() - start, + }; + stepResults.push(stepResult); + + if (!stepSuccess) { + success = false; + if (failedAtStep === undefined) failedAtStep = i; + if (stopOnError) break; + } + + // Wait for idle after this step unless explicitly disabled. + const wait = step.waitForIdle ?? defaultWait(step.verb); + if (wait) { + try { + await opts.client.eventsIdle({ debounceMs, maxWaitMs }); + } catch { + /* idle is best-effort */ + } + } + } + + return { + actionName: action.actionName, + success, + steps: stepResults, + ...(failedAtStep !== undefined ? { failedAtStep } : {}), + }; +} + +function defaultWait(verb: string): boolean { + // Verbs that typically open/close dialogs or transition panels need an idle wait. + return verb === "invoke" || verb === "select"; +} + +function resolveValue( + step: PlaybackStep, + params: PlaybackParams, +): string | number | boolean | undefined { + if (step.valueRef !== undefined) { + // Accept either "${name}" or bare "name". + const m = step.valueRef.match(/^\$\{(.+)\}$/); + const key = m ? m[1]! : step.valueRef; + const v = params[key]; + if (v === undefined) { + throw new Error(`Missing parameter '${key}' for valueRef`); + } + return v; + } + if (step.valueLiteral !== undefined) { + return step.valueLiteral; + } + return undefined; +} + +async function executeStep( + client: HelperClient, + step: PlaybackStep, + value: string | number | boolean | undefined, +): Promise { + switch (step.verb) { + case "invoke": + await client.doInvoke({ selector: step.selector }); + break; + case "click": + await client.doClick({ selector: step.selector }); + break; + case "focus": + await client.doFocus({ selector: step.selector }); + break; + case "toggle": + await client.doToggle({ + selector: step.selector, + ...(typeof value === "boolean" ? { value } : {}), + }); + break; + case "expand": + await client.doExpand({ + selector: step.selector, + expand: typeof value === "boolean" ? value : true, + }); + break; + case "select": + await client.doSelect({ + selector: step.selector, + ...(value !== undefined && typeof value !== "boolean" + ? { item: value as string | number } + : {}), + }); + break; + case "setValue": + if (value === undefined) { + throw new Error(`setValue step requires a value`); + } + await client.doSetValue({ + selector: step.selector, + value, + }); + break; + case "scroll": + await client.doScroll({ + selector: step.selector, + direction: "down", + }); + break; + default: + throw new Error(`Unsupported playback verb: ${step.verb}`); + } +} diff --git a/ts/packages/agents/onboarding/src/uiCapture/test/clockAgentDemo.ts b/ts/packages/agents/onboarding/src/uiCapture/test/clockAgentDemo.ts new file mode 100644 index 0000000000..4cc98bee5f --- /dev/null +++ b/ts/packages/agents/onboarding/src/uiCapture/test/clockAgentDemo.ts @@ -0,0 +1,218 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { existsSync, readFileSync, readdirSync, statSync } from "node:fs"; +import { homedir } from "node:os"; +import path from "node:path"; + +import { HelperClient } from "../helperClient.js"; +import { executePlayback } from "../playbackExecutor.js"; +import { inferSnapshotPolicy } from "../snapshotPolicy.js"; +import type { SynthesizedAction } from "../synthesisLlmSchema.js"; +import type { TreeNode } from "../types.js"; + +const CLOCK_AUMID = "Microsoft.WindowsAlarms_8wekyb3d8bbwe!App"; +const WORKSPACE = path.join( + homedir(), + ".typeagent", + "onboarding", + "windowsClock", +); + +function log(msg: string): void { + process.stdout.write(`[demo] ${msg}\n`); +} + +async function sleep(ms: number): Promise { + await new Promise((res) => setTimeout(res, ms)); +} + +function findMostRecentDir(parent: string): string { + const entries = readdirSync(parent) + .map((name) => ({ + name, + full: path.join(parent, name), + stat: statSync(path.join(parent, name)), + })) + .filter((e) => e.stat.isDirectory()) + .sort((a, b) => b.stat.mtimeMs - a.stat.mtimeMs); + if (entries.length === 0) { + throw new Error(`No directories under ${parent}`); + } + return entries[0]!.full; +} + +function findActionByName( + file: string, + actionName: string, +): SynthesizedAction { + const json = JSON.parse(readFileSync(file, "utf8")); + const action = json.actions.find( + (a: SynthesizedAction) => a.actionName === actionName, + ); + if (!action) { + throw new Error( + `No action '${actionName}' in ${file} (have: ${json.actions.map((a: SynthesizedAction) => a.actionName).join(", ")})`, + ); + } + return action; +} + +function findFirst( + node: TreeNode, + pred: (n: TreeNode) => boolean, +): TreeNode | null { + if (pred(node)) return node; + for (const c of node.children) { + const f = findFirst(c, pred); + if (f) return f; + } + return null; +} + +function countMatches(node: TreeNode, pred: (n: TreeNode) => boolean): number { + let n = pred(node) ? 1 : 0; + for (const c of node.children) n += countMatches(c, pred); + return n; +} + +async function main(): Promise { + const runDir = findMostRecentDir(path.join(WORKSPACE, "runs")); + const actionsFile = path.join(runDir, "discoveredActions.json"); + if (!existsSync(actionsFile)) { + throw new Error(`No discoveredActions.json at ${actionsFile}`); + } + log(`run dir: ${runDir}`); + + const createAlarm = findActionByName(actionsFile, "createAlarm"); + log( + `loaded createAlarm: ${createAlarm.parameters.length} params, ${createAlarm.playback.length} steps`, + ); + + // Find a baseline snapshot to restore against. + const snapshotsDir = path.join(WORKSPACE, "snapshots"); + if (!existsSync(snapshotsDir)) { + throw new Error(`No snapshots dir at ${snapshotsDir}`); + } + const baselineSnapshotDir = findMostRecentDir(snapshotsDir); + log(`baseline snapshot: ${baselineSnapshotDir}`); + + const policy = await inferSnapshotPolicy({ + integrationName: "windowsClock", + aumid: CLOCK_AUMID, + }); + policy.detectionStatus = "user-confirmed"; + + const client = await HelperClient.start({ debug: false }); + try { + // Make sure Clock is closed before we restore (file locks). + for (const w of (await client.appList()).filter((x) => + x.title.includes("Clock"), + )) { + await client.appKill({ pid: w.pid }); + } + await sleep(1500); + + log("restoring baseline so we have a known starting point..."); + await client.snapshotRestore({ + snapshotDir: baselineSnapshotDir, + policy, + }); + + log("launching Clock..."); + const launch = await client.appLaunch({ aumid: CLOCK_AUMID }); + await client.eventsIdle({ debounceMs: 800, maxWaitMs: 5000 }); + log(`launched pid=${launch.pid}`); + + // Count Toggle controls (each alarm has one) BEFORE running the action, + // restricted to the alarm tab area. + const treeBefore = await client.treeDump({ + root: launch.mainWindow, + maxDepth: 10, + }); + const togglesBefore = countMatches( + treeBefore, + (n) => n.automationId === "AlarmViewGrid", + ); + log(`alarm-toggle ListItems before: ${togglesBefore}`); + + const params = { + alarmName: "Crawled Demo Alarm", + hour: 8, + minute: 15, + }; + log( + `executing createAlarm with ${JSON.stringify(params)}...`, + ); + const result = await executePlayback(createAlarm, params, { + client, + defaultIdleDebounceMs: 700, + defaultIdleMaxWaitMs: 4000, + }); + + log( + `playback ${result.success ? "OK" : "FAILED"} (${result.steps.length} step(s))`, + ); + for (const s of result.steps) { + const v = + s.value !== undefined ? ` ${JSON.stringify(s.value)}` : ""; + const status = s.success ? "✓" : "✗"; + log( + ` ${status} step ${s.stepIndex + 1}: ${s.verb}${v} (${s.durationMs}ms)${ + s.errorMessage ? ` — ${s.errorMessage}` : "" + }`, + ); + } + + // Count alarm toggles after; expect +1 if action worked. + await sleep(800); + const treeAfter = await client.treeDump({ + root: launch.mainWindow, + maxDepth: 10, + }); + const togglesAfter = countMatches( + treeAfter, + (n) => n.automationId === "AlarmViewGrid", + ); + log(`alarm-toggle ListItems after: ${togglesAfter}`); + + // Look for an alarm whose name contains our test string. + // Alarms render as DataItem with name like "Edit alarm, ,