copilot-liku-cli 0.0.2 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # GitHub Copilot CLI: Liku Edition (Public Preview)
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/copilot-liku-cli.svg)](https://www.npmjs.com/package/copilot-liku-cli)
4
+ [![Node.js](https://img.shields.io/badge/node-%3E%3D22.0.0-brightgreen.svg)](https://nodejs.org/)
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE.md)
6
+
3
7
  The power of GitHub Copilot, now with visual-spatial awareness and advanced automation.
4
8
 
5
9
  GitHub Copilot-Liku CLI brings AI-powered coding assistance and UI automation directly to your terminal. This "Liku Edition" extends the standard Copilot experience with an ultra-thin Electron overlay, allowing the agent to "see" and interact with your screen through a coordinated grid system and native UI automation.
@@ -89,7 +93,7 @@ The Liku Edition moves beyond single-turn responses with a specialized team of a
89
93
 
90
94
  #### Global Installation (Recommended for Users)
91
95
 
92
- Once published to npm, install globally with:
96
+ Install globally from npm:
93
97
  ```bash
94
98
  npm install -g copilot-liku-cli
95
99
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "copilot-liku-cli",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "description": "GitHub Copilot CLI with headless agent + ultra-thin overlay architecture",
5
5
  "main": "src/main/index.js",
6
6
  "bin": {
package/src/cli/liku.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
  /**
3
3
  * liku - Copilot-Liku CLI
4
4
  *
@@ -10,9 +10,16 @@ const https = require('https');
10
10
  const http = require('http');
11
11
  const fs = require('fs');
12
12
  const path = require('path');
13
+ const os = require('os');
13
14
  const { shell } = require('electron');
14
15
  const systemAutomation = require('./system-automation');
15
16
 
17
+ // ===== ENVIRONMENT DETECTION =====
18
+ const PLATFORM = process.platform; // 'win32', 'darwin', 'linux'
19
+ const OS_NAME = PLATFORM === 'win32' ? 'Windows' : PLATFORM === 'darwin' ? 'macOS' : 'Linux';
20
+ const OS_VERSION = os.release();
21
+ const ARCHITECTURE = process.arch;
22
+
16
23
  // Lazy-load inspect service to avoid circular dependencies
17
24
  let inspectService = null;
18
25
  function getInspectService() {
@@ -22,6 +29,16 @@ function getInspectService() {
22
29
  return inspectService;
23
30
  }
24
31
 
32
+ // Lazy-load UI watcher for live UI context
33
+ let uiWatcher = null;
34
+ function getUIWatcher() {
35
+ if (!uiWatcher) {
36
+ const { UIWatcher } = require('./ui-watcher');
37
+ uiWatcher = new UIWatcher();
38
+ }
39
+ return uiWatcher;
40
+ }
41
+
25
42
  // ===== CONFIGURATION =====
26
43
 
27
44
  // Available models for GitHub Copilot (based on Copilot CLI changelog)
@@ -106,61 +123,118 @@ let visualContextBuffer = [];
106
123
  const MAX_VISUAL_CONTEXT = 5;
107
124
 
108
125
  // ===== SYSTEM PROMPT =====
126
+ // Generate platform-specific context dynamically
127
+ function getPlatformContext() {
128
+ if (PLATFORM === 'win32') {
129
+ return `
130
+ ## Platform: Windows ${OS_VERSION}
131
+
132
+ ### Windows-Specific Keyboard Shortcuts (USE THESE!)
133
+ - **Open new terminal**: \`win+x\` then \`i\` (opens Windows Terminal) OR \`win+r\` then type \`wt\` then \`enter\`
134
+ - **Open Run dialog**: \`win+r\`
135
+ - **Open Start menu/Search**: \`win\` (Windows key alone)
136
+ - **Switch windows**: \`alt+tab\`
137
+ - **Show desktop**: \`win+d\`
138
+ - **File Explorer**: \`win+e\`
139
+ - **Settings**: \`win+i\`
140
+ - **Lock screen**: \`win+l\`
141
+ - **Clipboard history**: \`win+v\`
142
+ - **Screenshot**: \`win+shift+s\`
143
+
144
+ ### Windows Terminal Shortcuts
145
+ - **New tab**: \`ctrl+shift+t\`
146
+ - **Close tab**: \`ctrl+shift+w\`
147
+ - **Split pane**: \`alt+shift+d\`
148
+
149
+ ### IMPORTANT: On Windows, NEVER use:
150
+ - \`cmd+space\` (that's macOS Spotlight)
151
+ - \`ctrl+alt+t\` (that's Linux terminal shortcut)`;
152
+ } else if (PLATFORM === 'darwin') {
153
+ return `
154
+ ## Platform: macOS ${OS_VERSION}
155
+
156
+ ### macOS-Specific Keyboard Shortcuts
157
+ - **Open terminal**: \`cmd+space\` then type "Terminal" then \`enter\`
158
+ - **Spotlight search**: \`cmd+space\`
159
+ - **Switch windows**: \`cmd+tab\`
160
+ - **Switch windows same app**: \`cmd+\`\`
161
+ - **Show desktop**: \`f11\` or \`cmd+mission control\`
162
+ - **Finder**: \`cmd+shift+g\`
163
+ - **Force quit**: \`cmd+option+esc\`
164
+ - **Screenshot**: \`cmd+shift+4\``;
165
+ } else {
166
+ return `
167
+ ## Platform: Linux ${OS_VERSION}
168
+
169
+ ### Linux-Specific Keyboard Shortcuts
170
+ - **Open terminal**: \`ctrl+alt+t\` (most distros)
171
+ - **Application menu**: \`super\` (Windows key)
172
+ - **Switch windows**: \`alt+tab\`
173
+ - **Show desktop**: \`super+d\`
174
+ - **File manager**: \`super+e\`
175
+ - **Screenshot**: \`print\` or \`shift+print\``;
176
+ }
177
+ }
178
+
109
179
  const SYSTEM_PROMPT = `You are Liku, an intelligent AGENTIC AI assistant integrated into a desktop overlay system with visual screen awareness AND the ability to control the user's computer.
110
180
 
181
+ ${getPlatformContext()}
182
+
111
183
  ## Your Core Capabilities
112
184
 
113
185
  1. **Screen Vision**: When the user captures their screen, you receive it as an image. ALWAYS analyze visible content immediately.
114
186
 
115
- 2. **Grid Coordinate System**: The screen has a dot grid overlay:
187
+ 2. **SEMANTIC ELEMENT ACTIONS (PREFERRED!)**: You can interact with UI elements by their text/name - MORE RELIABLE than coordinates:
188
+ - \`{"type": "click_element", "text": "Submit", "reason": "Click Submit button"}\` - Finds and clicks element by text
189
+ - \`{"type": "find_element", "text": "Save", "reason": "Locate Save button"}\` - Finds element info
190
+
191
+ 3. **Grid Coordinate System**: The screen has a dot grid overlay:
116
192
  - **Columns**: Letters A, B, C, D... (left to right), spacing 100px
117
193
  - **Rows**: Numbers 0, 1, 2, 3... (top to bottom), spacing 100px
118
194
  - **Start**: Grid is centered, so A0 is at (50, 50)
119
- - **Format**: "C3" = column C (index 2), row 3 = pixel (250, 350)
120
- - **Formula**: x = 50 + col_index * 100, y = 50 + row_index * 100
121
- - A0 ≈ (50, 50), B0 ≈ (150, 50), A1 ≈ (50, 150)
122
195
  - **Fine Grid**: Sub-labels like C3.12 refer to 25px subcells inside C3
123
196
 
124
- 3. **SYSTEM CONTROL - AGENTIC ACTIONS**: You can execute actions on the user's computer:
125
- - **Click**: Click at coordinates
197
+ 4. **SYSTEM CONTROL - AGENTIC ACTIONS**: You can execute actions on the user's computer:
198
+ - **Click**: Click at coordinates (use click_element when possible!)
126
199
  - **Type**: Type text into focused fields
127
- - **Press Keys**: Press keyboard shortcuts (ctrl+c, enter, etc.)
200
+ - **Press Keys**: Press keyboard shortcuts (platform-specific - see above!)
128
201
  - **Scroll**: Scroll up/down
129
202
  - **Drag**: Drag from one point to another
130
203
 
131
204
  ## ACTION FORMAT - CRITICAL
132
205
 
133
- When the user asks you to DO something (click, type, interact), respond with a JSON action block:
206
+ When the user asks you to DO something, respond with a JSON action block:
134
207
 
135
208
  \`\`\`json
136
209
  {
137
210
  "thought": "Brief explanation of what I'm about to do",
138
211
  "actions": [
139
- {"type": "click", "x": 300, "y": 200, "reason": "Click the input field"},
140
- {"type": "type", "text": "Hello world", "reason": "Type the requested text"},
141
- {"type": "key", "key": "enter", "reason": "Submit the form"}
212
+ {"type": "key", "key": "win+x", "reason": "Open Windows power menu"},
213
+ {"type": "wait", "ms": 300},
214
+ {"type": "key", "key": "i", "reason": "Select Terminal option"}
142
215
  ],
143
- "verification": "After these actions, the text field should show 'Hello world'"
216
+ "verification": "A new Windows Terminal window should open"
144
217
  }
145
218
  \`\`\`
146
219
 
147
220
  ### Action Types:
148
- - \`{"type": "click", "x": <number>, "y": <number>}\` - Left click at pixel coordinates
221
+ - \`{"type": "click_element", "text": "<button text>"}\` - **PREFERRED**: Click element by text (uses Windows UI Automation)
222
+ - \`{"type": "find_element", "text": "<search text>"}\` - Find element and return its info
223
+ - \`{"type": "click", "x": <number>, "y": <number>}\` - Left click at pixel coordinates (use as fallback)
149
224
  - \`{"type": "double_click", "x": <number>, "y": <number>}\` - Double click
150
225
  - \`{"type": "right_click", "x": <number>, "y": <number>}\` - Right click
151
226
  - \`{"type": "type", "text": "<string>"}\` - Type text (types into currently focused element)
152
- - \`{"type": "key", "key": "<key combo>"}\` - Press key (e.g., "enter", "ctrl+c", "alt+tab", "f5")
153
- - \`{"type": "scroll", "direction": "up|down", "amount": <number>}\` - Scroll (amount = clicks)
227
+ - \`{"type": "key", "key": "<key combo>"}\` - Press key (e.g., "enter", "ctrl+c", "win+r", "alt+tab")
228
+ - \`{"type": "scroll", "direction": "up|down", "amount": <number>}\` - Scroll
154
229
  - \`{"type": "drag", "fromX": <n>, "fromY": <n>, "toX": <n>, "toY": <n>}\` - Drag
155
- - \`{"type": "wait", "ms": <number>}\` - Wait milliseconds
230
+ - \`{"type": "wait", "ms": <number>}\` - Wait milliseconds (IMPORTANT: add waits between multi-step actions!)
156
231
  - \`{"type": "screenshot"}\` - Take screenshot to verify result
157
232
 
158
233
  ### Grid to Pixel Conversion:
159
234
  - A0 → (50, 50), B0 → (150, 50), C0 → (250, 50)
160
235
  - A1 → (50, 150), B1 → (150, 150), C1 → (250, 150)
161
236
  - Formula: x = 50 + col_index * 100, y = 50 + row_index * 100
162
- - Column A=0, B=1, C=2... so C3 = x: 50 + 2*100 = 250, y: 50 + 3*100 = 350
163
- - Fine labels: C3.12 = x: 12.5 + (2*4+1)*25 = 237.5, y: 12.5 + (3*4+2)*25 = 362.5
237
+ - Fine labels: C3.12 = x: 12.5 + (2*4+1)*25 = 237.5, y: 12.5 + (3*4+2)*25 = 362.5
164
238
 
165
239
  ## Response Guidelines
166
240
 
@@ -170,21 +244,27 @@ When the user asks you to DO something (click, type, interact), respond with a J
170
244
 
171
245
  **For ACTION requests** (click here, type this, open that):
172
246
  - ALWAYS respond with the JSON action block
173
- - Include your thought process
174
- - Calculate coordinates precisely
247
+ - Use PLATFORM-SPECIFIC shortcuts (see above!)
248
+ - Prefer \`click_element\` over coordinate clicks when targeting named UI elements
249
+ - Add \`wait\` actions between steps that need UI to update
175
250
  - Add verification step to confirm success
176
251
 
177
- **When executing a sequence**:
178
- 1. First action: click to focus the target element
179
- 2. Second action: perform the main task (type, etc.)
180
- 3. Optional: verify with screenshot
181
-
182
- **IMPORTANT**: When asked to interact with something visible in the screenshot:
183
- 1. Identify the element's approximate position
184
- 2. Convert to pixel coordinates
185
- 3. Return the action JSON
186
-
187
- Be precise, efficient, and execute actions confidently based on visual information.`;
252
+ **Common Task Patterns**:
253
+ ${PLATFORM === 'win32' ? `
254
+ - **Open new terminal**: Use \`win+x\` then \`i\` (or \`win+r\` → type "wt" → \`enter\`)
255
+ - **Open application**: Use \`win\` key, type app name, press \`enter\`
256
+ - **Save file**: \`ctrl+s\`
257
+ - **Copy/Paste**: \`ctrl+c\` / \`ctrl+v\`` : PLATFORM === 'darwin' ? `
258
+ - **Open terminal**: \`cmd+space\`, type "Terminal", \`enter\`
259
+ - **Open application**: \`cmd+space\`, type app name, \`enter\`
260
+ - **Save file**: \`cmd+s\`
261
+ - **Copy/Paste**: \`cmd+c\` / \`cmd+v\`` : `
262
+ - **Open terminal**: \`ctrl+alt+t\`
263
+ - **Open application**: \`super\` key, type name, \`enter\`
264
+ - **Save file**: \`ctrl+s\`
265
+ - **Copy/Paste**: \`ctrl+c\` / \`ctrl+v\``}
266
+
267
+ Be precise, use platform-correct shortcuts, and execute actions confidently!`;
188
268
 
189
269
  /**
190
270
  * Set the AI provider
@@ -331,8 +411,23 @@ ${inspectContext.regions.slice(0, 20).map((r, i) =>
331
411
  console.warn('[AI] Could not get inspect context:', e.message);
332
412
  }
333
413
 
334
- const enhancedMessage = inspectContextText
335
- ? `${userMessage}${inspectContextText}`
414
+ // Get live UI context from the UI watcher (always-on mirror)
415
+ let liveUIContextText = '';
416
+ try {
417
+ const watcher = getUIWatcher();
418
+ if (watcher && watcher.isRunning) {
419
+ const uiContext = watcher.getContextForAI();
420
+ if (uiContext && uiContext.trim()) {
421
+ liveUIContextText = `\n\n${uiContext}`;
422
+ console.log('[AI] Including live UI context from watcher');
423
+ }
424
+ }
425
+ } catch (e) {
426
+ console.warn('[AI] Could not get live UI context:', e.message);
427
+ }
428
+
429
+ const enhancedMessage = inspectContextText || liveUIContextText
430
+ ? `${userMessage}${inspectContextText}${liveUIContextText}`
336
431
  : userMessage;
337
432
 
338
433
  if (latestVisual && (currentProvider === 'copilot' || currentProvider === 'openai')) {
package/src/main/index.js CHANGED
@@ -25,6 +25,9 @@ const aiService = require('./ai-service.js');
25
25
  // Visual awareness for advanced screen analysis
26
26
  const visualAwareness = require('./visual-awareness.js');
27
27
 
28
+ // Live UI Watcher for continuous UI monitoring
29
+ const { UIWatcher } = require('./ui-watcher.js');
30
+
28
31
  // Multi-agent system for advanced AI orchestration
29
32
  const { createAgentSystem } = require('./agents/index.js');
30
33
 
@@ -58,6 +61,9 @@ let overlayWindow = null;
58
61
  let chatWindow = null;
59
62
  let tray = null;
60
63
 
64
+ // Live UI watcher instance
65
+ let uiWatcher = null;
66
+
61
67
  // State management
62
68
  let overlayMode = 'selection'; // start in selection so the grid is visible immediately
63
69
  let isChatVisible = false;
@@ -112,9 +118,12 @@ function createOverlayWindow() {
112
118
  });
113
119
 
114
120
  // Pipe renderer console to main for debugging without DevTools
115
- overlayWindow.webContents.on('console-message', (event) => {
116
- const { level, message, line, sourceId } = event;
117
- console.log(`[overlay console] (${level}) ${sourceId}:${line} - ${message}`);
121
+ overlayWindow.webContents.on('console-message', (event, level, message, line, sourceId) => {
122
+ const levelNames = ['verbose', 'info', 'warn', 'error'];
123
+ const levelStr = levelNames[level] || `level-${level}`;
124
+ const lineStr = line !== undefined ? `:${line}` : '';
125
+ const source = sourceId ? sourceId.split('/').pop() : 'overlay';
126
+ console.log(`[overlay console] (${levelStr}) ${source}${lineStr} - ${message}`);
118
127
  });
119
128
 
120
129
  // Prevent overlay from appearing in Dock/Taskbar
@@ -1068,6 +1077,17 @@ function setupIPC() {
1068
1077
  total: actionData.actions.length
1069
1078
  });
1070
1079
 
1080
+ // CRITICAL: Blur chat window before executing actions so keyboard/mouse
1081
+ // input reaches the desktop instead of staying within Electron
1082
+ if (chatWindow && !chatWindow.isDestroyed()) {
1083
+ chatWindow.blur();
1084
+ }
1085
+ if (overlayWindow && !overlayWindow.isDestroyed()) {
1086
+ overlayWindow.blur();
1087
+ // Temporarily lower overlay z-index so popups (like Run dialog) appear above
1088
+ overlayWindow.setAlwaysOnTop(true, 'pop-up-menu');
1089
+ }
1090
+
1071
1091
  try {
1072
1092
  const results = await aiService.executeActions(
1073
1093
  actionData,
@@ -1167,6 +1187,11 @@ function setupIPC() {
1167
1187
  actionsCount: actionData.actions ? actionData.actions.length : 0,
1168
1188
  error: error.message
1169
1189
  });
1190
+ } finally {
1191
+ // Restore overlay z-index after action execution
1192
+ if (overlayWindow && !overlayWindow.isDestroyed()) {
1193
+ overlayWindow.setAlwaysOnTop(true, 'screen-saver');
1194
+ }
1170
1195
  }
1171
1196
 
1172
1197
  pendingActions = null;
@@ -2148,6 +2173,25 @@ app.whenReady().then(() => {
2148
2173
  registerShortcuts();
2149
2174
  setupIPC();
2150
2175
 
2176
+ // Start the UI watcher for live UI monitoring
2177
+ try {
2178
+ uiWatcher = new UIWatcher({
2179
+ pollInterval: 400,
2180
+ maxElements: 60,
2181
+ includeInvisible: false
2182
+ });
2183
+ uiWatcher.on('ui-changed', (diff) => {
2184
+ // Forward UI changes to overlay for live mirror updates
2185
+ if (overlayWindow && !overlayWindow.isDestroyed()) {
2186
+ overlayWindow.webContents.send('ui-watcher-update', diff);
2187
+ }
2188
+ });
2189
+ uiWatcher.start();
2190
+ console.log('[Main] UI Watcher started for live UI monitoring');
2191
+ } catch (e) {
2192
+ console.warn('[Main] Could not start UI watcher:', e.message);
2193
+ }
2194
+
2151
2195
  // Set up Copilot OAuth callback to notify chat on auth completion
2152
2196
  aiService.setOAuthCallback((result) => {
2153
2197
  if (chatWindow && !chatWindow.isDestroyed()) {
@@ -2197,9 +2241,13 @@ app.on('window-all-closed', () => {
2197
2241
  }
2198
2242
  });
2199
2243
 
2200
- // Clean up shortcuts on quit
2244
+ // Clean up shortcuts and UI watcher on quit
2201
2245
  app.on('will-quit', () => {
2202
2246
  globalShortcut.unregisterAll();
2247
+ if (uiWatcher) {
2248
+ uiWatcher.stop();
2249
+ console.log('[Main] UI Watcher stopped');
2250
+ }
2203
2251
  });
2204
2252
 
2205
2253
  // Prevent app from quitting when closing chat window
@@ -88,6 +88,33 @@ function executePowerShell(command) {
88
88
  });
89
89
  }
90
90
 
91
+ /**
92
+ * Focus the desktop / unfocus Electron windows before sending keyboard input
93
+ * This is critical for SendKeys/SendInput to reach the correct target
94
+ */
95
+ async function focusDesktop() {
96
+ const script = `
97
+ Add-Type @"
98
+ using System;
99
+ using System.Runtime.InteropServices;
100
+ public class FocusHelper {
101
+ [DllImport("user32.dll")]
102
+ public static extern IntPtr GetDesktopWindow();
103
+ [DllImport("user32.dll")]
104
+ public static extern bool SetForegroundWindow(IntPtr hWnd);
105
+ [DllImport("user32.dll")]
106
+ public static extern IntPtr GetShellWindow();
107
+ }
108
+ "@
109
+ # Focus shell window (explorer desktop)
110
+ $shell = [FocusHelper]::GetShellWindow()
111
+ [FocusHelper]::SetForegroundWindow($shell)
112
+ Start-Sleep -Milliseconds 50
113
+ `;
114
+ await executePowerShell(script);
115
+ console.log('[AUTOMATION] Focused desktop before input');
116
+ }
117
+
91
118
  /**
92
119
  * Move mouse to coordinates (Windows)
93
120
  */
@@ -443,15 +470,125 @@ Add-Type -AssemblyName System.Windows.Forms
443
470
  }
444
471
 
445
472
  /**
446
- * Press a key or key combination (e.g., "ctrl+c", "enter", "alt+tab")
473
+ * Press a key or key combination (e.g., "ctrl+c", "enter", "alt+tab", "win+r")
474
+ * Now supports Windows key using SendInput with virtual key codes
447
475
  */
448
476
  async function pressKey(keyCombo) {
449
- let sendKeysStr = '';
450
-
451
- // Parse key combo
452
477
  const parts = keyCombo.toLowerCase().split('+').map(k => k.trim());
453
478
 
454
- // Build SendKeys string
479
+ // Check if Windows key is involved - requires special handling
480
+ const hasWinKey = parts.includes('win') || parts.includes('windows') || parts.includes('super');
481
+
482
+ if (hasWinKey) {
483
+ // Use SendInput for Windows key combos
484
+ const otherKeys = parts.filter(p => p !== 'win' && p !== 'windows' && p !== 'super');
485
+ const hasCtrl = otherKeys.includes('ctrl') || otherKeys.includes('control');
486
+ const hasAlt = otherKeys.includes('alt');
487
+ const hasShift = otherKeys.includes('shift');
488
+ const mainKey = otherKeys.find(p => !['ctrl', 'control', 'alt', 'shift'].includes(p)) || '';
489
+
490
+ // Virtual key codes for common keys
491
+ const vkCodes = {
492
+ 'a': 0x41, 'b': 0x42, 'c': 0x43, 'd': 0x44, 'e': 0x45, 'f': 0x46, 'g': 0x47, 'h': 0x48,
493
+ 'i': 0x49, 'j': 0x4A, 'k': 0x4B, 'l': 0x4C, 'm': 0x4D, 'n': 0x4E, 'o': 0x4F, 'p': 0x50,
494
+ 'q': 0x51, 'r': 0x52, 's': 0x53, 't': 0x54, 'u': 0x55, 'v': 0x56, 'w': 0x57, 'x': 0x58,
495
+ 'y': 0x59, 'z': 0x5A,
496
+ '0': 0x30, '1': 0x31, '2': 0x32, '3': 0x33, '4': 0x34, '5': 0x35, '6': 0x36, '7': 0x37, '8': 0x38, '9': 0x39,
497
+ 'enter': 0x0D, 'return': 0x0D, 'tab': 0x09, 'escape': 0x1B, 'esc': 0x1B,
498
+ 'space': 0x20, 'backspace': 0x08, 'delete': 0x2E, 'del': 0x2E,
499
+ 'up': 0x26, 'down': 0x28, 'left': 0x25, 'right': 0x27,
500
+ 'home': 0x24, 'end': 0x23, 'pageup': 0x21, 'pagedown': 0x22,
501
+ 'f1': 0x70, 'f2': 0x71, 'f3': 0x72, 'f4': 0x73, 'f5': 0x74, 'f6': 0x75,
502
+ 'f7': 0x76, 'f8': 0x77, 'f9': 0x78, 'f10': 0x79, 'f11': 0x7A, 'f12': 0x7B,
503
+ };
504
+
505
+ const mainKeyCode = mainKey ? (vkCodes[mainKey] || mainKey.charCodeAt(0)) : 0;
506
+
507
+ const script = `
508
+ Add-Type -TypeDefinition @"
509
+ using System;
510
+ using System.Runtime.InteropServices;
511
+
512
+ public class WinKeyPress {
513
+ [StructLayout(LayoutKind.Sequential)]
514
+ public struct INPUT {
515
+ public uint type;
516
+ public InputUnion U;
517
+ }
518
+
519
+ [StructLayout(LayoutKind.Explicit)]
520
+ public struct InputUnion {
521
+ [FieldOffset(0)] public MOUSEINPUT mi;
522
+ [FieldOffset(0)] public KEYBDINPUT ki;
523
+ }
524
+
525
+ [StructLayout(LayoutKind.Sequential)]
526
+ public struct MOUSEINPUT {
527
+ public int dx, dy;
528
+ public uint mouseData, dwFlags, time;
529
+ public IntPtr dwExtraInfo;
530
+ }
531
+
532
+ [StructLayout(LayoutKind.Sequential)]
533
+ public struct KEYBDINPUT {
534
+ public ushort wVk;
535
+ public ushort wScan;
536
+ public uint dwFlags;
537
+ public uint time;
538
+ public IntPtr dwExtraInfo;
539
+ }
540
+
541
+ public const uint INPUT_KEYBOARD = 1;
542
+ public const uint KEYEVENTF_KEYUP = 0x0002;
543
+ public const ushort VK_LWIN = 0x5B;
544
+ public const ushort VK_CONTROL = 0x11;
545
+ public const ushort VK_SHIFT = 0x10;
546
+ public const ushort VK_MENU = 0x12; // Alt
547
+
548
+ [DllImport("user32.dll", SetLastError = true)]
549
+ public static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize);
550
+
551
+ public static void KeyDown(ushort vk) {
552
+ INPUT[] inputs = new INPUT[1];
553
+ inputs[0].type = INPUT_KEYBOARD;
554
+ inputs[0].U.ki.wVk = vk;
555
+ inputs[0].U.ki.dwFlags = 0;
556
+ SendInput(1, inputs, Marshal.SizeOf(typeof(INPUT)));
557
+ }
558
+
559
+ public static void KeyUp(ushort vk) {
560
+ INPUT[] inputs = new INPUT[1];
561
+ inputs[0].type = INPUT_KEYBOARD;
562
+ inputs[0].U.ki.wVk = vk;
563
+ inputs[0].U.ki.dwFlags = KEYEVENTF_KEYUP;
564
+ SendInput(1, inputs, Marshal.SizeOf(typeof(INPUT)));
565
+ }
566
+ }
567
+ "@
568
+
569
+ # Press modifiers
570
+ [WinKeyPress]::KeyDown([WinKeyPress]::VK_LWIN)
571
+ ${hasCtrl ? '[WinKeyPress]::KeyDown([WinKeyPress]::VK_CONTROL)' : ''}
572
+ ${hasAlt ? '[WinKeyPress]::KeyDown([WinKeyPress]::VK_MENU)' : ''}
573
+ ${hasShift ? '[WinKeyPress]::KeyDown([WinKeyPress]::VK_SHIFT)' : ''}
574
+
575
+ # Press main key if any
576
+ ${mainKeyCode ? `[WinKeyPress]::KeyDown(${mainKeyCode})
577
+ Start-Sleep -Milliseconds 50
578
+ [WinKeyPress]::KeyUp(${mainKeyCode})` : 'Start-Sleep -Milliseconds 100'}
579
+
580
+ # Release modifiers in reverse order
581
+ ${hasShift ? '[WinKeyPress]::KeyUp([WinKeyPress]::VK_SHIFT)' : ''}
582
+ ${hasAlt ? '[WinKeyPress]::KeyUp([WinKeyPress]::VK_MENU)' : ''}
583
+ ${hasCtrl ? '[WinKeyPress]::KeyUp([WinKeyPress]::VK_CONTROL)' : ''}
584
+ [WinKeyPress]::KeyUp([WinKeyPress]::VK_LWIN)
585
+ `;
586
+ await executePowerShell(script);
587
+ console.log(`[AUTOMATION] Pressed Windows key combo: ${keyCombo} (using SendInput)`);
588
+ return;
589
+ }
590
+
591
+ // Non-Windows key combos use SendKeys
455
592
  let modifiers = '';
456
593
  let mainKey = '';
457
594
 
@@ -470,7 +607,7 @@ async function pressKey(keyCombo) {
470
607
  }
471
608
  }
472
609
 
473
- sendKeysStr = modifiers + (mainKey ? `(${mainKey})` : '');
610
+ const sendKeysStr = modifiers + (mainKey ? `(${mainKey})` : '');
474
611
 
475
612
  if (!sendKeysStr) {
476
613
  throw new Error(`Invalid key combo: ${keyCombo}`);
@@ -0,0 +1,503 @@
1
+ /**
2
+ * UI Watcher Service - Live UI Mirror for AI Awareness
3
+ *
4
+ * Provides continuous background monitoring of the Windows UI tree,
5
+ * enabling the AI to have real-time awareness without manual screenshots.
6
+ *
7
+ * Architecture:
8
+ * - Polls Windows UI Automation every 300-500ms
9
+ * - Maintains an element cache with bounds, text, and roles
10
+ * - Sends incremental diffs to the overlay
11
+ * - Provides instant context to AI for every message
12
+ */
13
+
14
+ const { exec, spawn } = require('child_process');
15
+ const os = require('os');
16
+ const path = require('path');
17
+ const fs = require('fs');
18
+ const EventEmitter = require('events');
19
+
20
+ class UIWatcher extends EventEmitter {
21
+ constructor(options = {}) {
22
+ super();
23
+
24
+ this.options = {
25
+ pollInterval: options.pollInterval || 400, // ms between polls
26
+ focusedWindowOnly: options.focusedWindowOnly ?? true, // only scan active window
27
+ maxElements: options.maxElements || 200, // limit results for performance
28
+ minConfidence: options.minConfidence || 0.3, // filter low-confidence elements
29
+ enabled: false,
30
+ ...options
31
+ };
32
+
33
+ // Element cache
34
+ this.cache = {
35
+ elements: [],
36
+ activeWindow: null,
37
+ lastUpdate: 0,
38
+ updateCount: 0
39
+ };
40
+
41
+ // Polling state
42
+ this.pollTimer = null;
43
+ this.isPolling = false;
44
+ this.pollInProgress = false;
45
+
46
+ // Performance tracking
47
+ this.metrics = {
48
+ avgPollTime: 0,
49
+ pollCount: 0,
50
+ lastPollTime: 0,
51
+ errorCount: 0
52
+ };
53
+
54
+ // Persistent PowerShell process for performance
55
+ this.psProcess = null;
56
+ this.psQueue = [];
57
+ this.psReady = false;
58
+ }
59
+
60
+ /**
61
+ * Start continuous UI monitoring
62
+ */
63
+ start() {
64
+ if (this.isPolling) return;
65
+
66
+ console.log('[UI-WATCHER] Starting continuous monitoring (interval:', this.options.pollInterval, 'ms)');
67
+ this.isPolling = true;
68
+ this.options.enabled = true;
69
+
70
+ // Initial poll
71
+ this.poll();
72
+
73
+ // Start polling loop
74
+ this.pollTimer = setInterval(() => {
75
+ if (!this.pollInProgress) {
76
+ this.poll();
77
+ }
78
+ }, this.options.pollInterval);
79
+
80
+ this.emit('started');
81
+ }
82
+
83
+ /**
84
+ * Stop monitoring
85
+ */
86
+ stop() {
87
+ if (!this.isPolling) return;
88
+
89
+ console.log('[UI-WATCHER] Stopping monitoring');
90
+ this.isPolling = false;
91
+ this.options.enabled = false;
92
+
93
+ if (this.pollTimer) {
94
+ clearInterval(this.pollTimer);
95
+ this.pollTimer = null;
96
+ }
97
+
98
+ this.killPsProcess();
99
+ this.emit('stopped');
100
+ }
101
+
102
+ /**
103
+ * Perform a single poll of the UI tree
104
+ */
105
+ async poll() {
106
+ if (this.pollInProgress) return;
107
+ this.pollInProgress = true;
108
+
109
+ const startTime = Date.now();
110
+
111
+ try {
112
+ // Get active window info
113
+ const activeWindow = await this.getActiveWindow();
114
+
115
+ // Get UI elements (focused window only for performance)
116
+ const elements = await this.detectElements(activeWindow);
117
+
118
+ // Calculate diff
119
+ const diff = this.calculateDiff(elements);
120
+
121
+ // Update cache
122
+ const oldCache = { ...this.cache };
123
+ this.cache = {
124
+ elements,
125
+ activeWindow,
126
+ lastUpdate: Date.now(),
127
+ updateCount: this.cache.updateCount + 1
128
+ };
129
+
130
+ // Track metrics
131
+ const pollTime = Date.now() - startTime;
132
+ this.metrics.pollCount++;
133
+ this.metrics.lastPollTime = pollTime;
134
+ this.metrics.avgPollTime = (this.metrics.avgPollTime * (this.metrics.pollCount - 1) + pollTime) / this.metrics.pollCount;
135
+
136
+ // Emit events
137
+ if (diff.hasChanges) {
138
+ this.emit('ui-changed', {
139
+ added: diff.added,
140
+ removed: diff.removed,
141
+ changed: diff.changed,
142
+ activeWindow,
143
+ elementCount: elements.length
144
+ });
145
+ }
146
+
147
+ this.emit('poll-complete', {
148
+ elements,
149
+ activeWindow,
150
+ pollTime,
151
+ hasChanges: diff.hasChanges
152
+ });
153
+
154
+ } catch (error) {
155
+ this.metrics.errorCount++;
156
+ console.error('[UI-WATCHER] Poll error:', error.message);
157
+ this.emit('error', error);
158
+ } finally {
159
+ this.pollInProgress = false;
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Get the currently active/focused window
165
+ */
166
+ async getActiveWindow() {
167
+ const script = `
168
+ Add-Type @"
169
+ using System;
170
+ using System.Runtime.InteropServices;
171
+ using System.Text;
172
+ public class ActiveWindow {
173
+ [DllImport("user32.dll")] public static extern IntPtr GetForegroundWindow();
174
+ [DllImport("user32.dll")] public static extern int GetWindowText(IntPtr hWnd, StringBuilder text, int count);
175
+ [DllImport("user32.dll")] public static extern int GetWindowThreadProcessId(IntPtr hWnd, out int processId);
176
+ [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
177
+ [StructLayout(LayoutKind.Sequential)] public struct RECT { public int Left, Top, Right, Bottom; }
178
+ }
179
+ "@
180
+ $hwnd = [ActiveWindow]::GetForegroundWindow()
181
+ $sb = New-Object System.Text.StringBuilder 256
182
+ [ActiveWindow]::GetWindowText($hwnd, $sb, 256) | Out-Null
183
+ $processId = 0
184
+ [ActiveWindow]::GetWindowThreadProcessId($hwnd, [ref]$processId) | Out-Null
185
+ $rect = New-Object ActiveWindow+RECT
186
+ [ActiveWindow]::GetWindowRect($hwnd, [ref]$rect) | Out-Null
187
+ $proc = Get-Process -Id $processId -ErrorAction SilentlyContinue
188
+ @{
189
+ hwnd = [long]$hwnd
190
+ title = $sb.ToString()
191
+ processId = $processId
192
+ processName = if($proc){$proc.ProcessName}else{""}
193
+ bounds = @{ x = $rect.Left; y = $rect.Top; width = $rect.Right - $rect.Left; height = $rect.Bottom - $rect.Top }
194
+ } | ConvertTo-Json -Compress
195
+ `;
196
+
197
+ return new Promise((resolve, reject) => {
198
+ exec(`powershell -NoProfile -Command "${script.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`,
199
+ { encoding: 'utf8', timeout: 2000 },
200
+ (error, stdout, stderr) => {
201
+ if (error) {
202
+ resolve(null);
203
+ return;
204
+ }
205
+ try {
206
+ resolve(JSON.parse(stdout.trim()));
207
+ } catch (e) {
208
+ resolve(null);
209
+ }
210
+ }
211
+ );
212
+ });
213
+ }
214
+
215
+ /**
216
+ * Detect UI elements using Windows UI Automation
217
+ */
218
+ async detectElements(activeWindow) {
219
+ // Build scope filter based on active window
220
+ const windowFilter = this.options.focusedWindowOnly && activeWindow
221
+ ? `$targetWindow = "${(activeWindow.title || '').replace(/"/g, '\\"')}"`
222
+ : '$targetWindow = ""';
223
+
224
+ const script = `
225
+ Add-Type -AssemblyName UIAutomationClient
226
+ Add-Type -AssemblyName UIAutomationTypes
227
+
228
+ ${windowFilter}
229
+ $maxElements = ${this.options.maxElements}
230
+
231
+ $root = [System.Windows.Automation.AutomationElement]::RootElement
232
+
233
+ # If targeting specific window, find it first
234
+ if ($targetWindow -ne "") {
235
+ $nameCondition = New-Object System.Windows.Automation.PropertyCondition(
236
+ [System.Windows.Automation.AutomationElement]::NameProperty, $targetWindow
237
+ )
238
+ $targetEl = $root.FindFirst([System.Windows.Automation.TreeScope]::Children, $nameCondition)
239
+ if ($targetEl) { $root = $targetEl }
240
+ }
241
+
242
+ $condition = [System.Windows.Automation.Condition]::TrueCondition
243
+ $elements = $root.FindAll([System.Windows.Automation.TreeScope]::Descendants, $condition)
244
+
245
+ $results = @()
246
+ $count = 0
247
+
248
+ foreach ($el in $elements) {
249
+ if ($count -ge $maxElements) { break }
250
+ try {
251
+ $rect = $el.Current.BoundingRectangle
252
+ if ($rect.Width -le 0 -or $rect.Height -le 0) { continue }
253
+ if ($rect.X -lt -10000 -or $rect.Y -lt -10000) { continue }
254
+
255
+ $name = $el.Current.Name
256
+ $ctrlType = $el.Current.ControlType.ProgrammaticName -replace 'ControlType\\.', ''
257
+ $autoId = $el.Current.AutomationId
258
+ $className = $el.Current.ClassName
259
+ $isEnabled = $el.Current.IsEnabled
260
+
261
+ # Skip elements with no useful identifying info
262
+ if ([string]::IsNullOrWhiteSpace($name) -and [string]::IsNullOrWhiteSpace($autoId)) { continue }
263
+
264
+ # Generate a unique ID
265
+ $id = "$ctrlType|$name|$autoId|$([int]$rect.X)|$([int]$rect.Y)"
266
+
267
+ $results += @{
268
+ id = $id
269
+ name = $name
270
+ type = $ctrlType
271
+ automationId = $autoId
272
+ className = $className
273
+ bounds = @{
274
+ x = [int]$rect.X
275
+ y = [int]$rect.Y
276
+ width = [int]$rect.Width
277
+ height = [int]$rect.Height
278
+ }
279
+ center = @{
280
+ x = [int]($rect.X + $rect.Width / 2)
281
+ y = [int]($rect.Y + $rect.Height / 2)
282
+ }
283
+ isEnabled = $isEnabled
284
+ }
285
+ $count++
286
+ } catch {}
287
+ }
288
+
289
+ $results | ConvertTo-Json -Depth 4 -Compress
290
+ `;
291
+
292
+ return new Promise((resolve, reject) => {
293
+ exec(`powershell -NoProfile -Command "${script.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`,
294
+ { encoding: 'utf8', timeout: 5000, maxBuffer: 10 * 1024 * 1024 },
295
+ (error, stdout, stderr) => {
296
+ if (error) {
297
+ resolve([]);
298
+ return;
299
+ }
300
+ try {
301
+ let elements = JSON.parse(stdout.trim() || '[]');
302
+ if (!Array.isArray(elements)) elements = elements ? [elements] : [];
303
+ resolve(elements);
304
+ } catch (e) {
305
+ resolve([]);
306
+ }
307
+ }
308
+ );
309
+ });
310
+ }
311
+
312
+ /**
313
+ * Calculate diff between old and new element sets
314
+ */
315
+ calculateDiff(newElements) {
316
+ const oldElements = this.cache.elements || [];
317
+ const oldMap = new Map(oldElements.map(e => [e.id, e]));
318
+ const newMap = new Map(newElements.map(e => [e.id, e]));
319
+
320
+ const added = newElements.filter(e => !oldMap.has(e.id));
321
+ const removed = oldElements.filter(e => !newMap.has(e.id));
322
+ const changed = newElements.filter(e => {
323
+ const old = oldMap.get(e.id);
324
+ if (!old) return false;
325
+ // Check if bounds or enabled state changed
326
+ return old.bounds.x !== e.bounds.x ||
327
+ old.bounds.y !== e.bounds.y ||
328
+ old.isEnabled !== e.isEnabled;
329
+ });
330
+
331
+ return {
332
+ added,
333
+ removed,
334
+ changed,
335
+ hasChanges: added.length > 0 || removed.length > 0 || changed.length > 0
336
+ };
337
+ }
338
+
339
+ /**
340
+ * Get current UI state for AI context
341
+ * This is called by ai-service before every API call
342
+ */
343
+ getContextForAI() {
344
+ if (!this.cache.elements || this.cache.elements.length === 0) {
345
+ return null;
346
+ }
347
+
348
+ const { elements, activeWindow, lastUpdate } = this.cache;
349
+ const age = Date.now() - lastUpdate;
350
+
351
+ // Group elements by type for cleaner context
352
+ const byType = {};
353
+ elements.forEach(el => {
354
+ const type = el.type || 'Unknown';
355
+ if (!byType[type]) byType[type] = [];
356
+ byType[type].push(el);
357
+ });
358
+
359
+ // Build context string
360
+ let context = `\n## Live UI State (${age}ms ago)\n`;
361
+
362
+ if (activeWindow) {
363
+ context += `**Active Window**: ${activeWindow.title || 'Unknown'} (${activeWindow.processName})\n`;
364
+ context += `**Window Bounds**: (${activeWindow.bounds.x}, ${activeWindow.bounds.y}) ${activeWindow.bounds.width}x${activeWindow.bounds.height}\n\n`;
365
+ }
366
+
367
+ // List interactive elements (buttons, text fields, etc.)
368
+ const interactiveTypes = ['Button', 'Edit', 'ComboBox', 'CheckBox', 'RadioButton', 'MenuItem', 'ListItem', 'TabItem', 'Hyperlink'];
369
+
370
+ context += `**Interactive Elements** (${elements.length} total):\n`;
371
+
372
+ let listed = 0;
373
+ for (const type of interactiveTypes) {
374
+ const typeElements = byType[type] || [];
375
+ for (const el of typeElements.slice(0, 10)) { // Limit per type
376
+ if (listed >= 30) break; // Total limit
377
+ const name = el.name || el.automationId || '[unnamed]';
378
+ context += `- **${type}**: "${name}" at (${el.center.x}, ${el.center.y})${el.isEnabled ? '' : ' [disabled]'}\n`;
379
+ listed++;
380
+ }
381
+ }
382
+
383
+ if (elements.length > listed) {
384
+ context += `... and ${elements.length - listed} more elements\n`;
385
+ }
386
+
387
+ return context;
388
+ }
389
+
390
+ /**
391
+ * Find element by text (for semantic actions)
392
+ */
393
+ findElementByText(searchText, options = {}) {
394
+ const { exact = false, type = null } = options;
395
+ const { elements } = this.cache;
396
+
397
+ if (!elements) return null;
398
+
399
+ const matches = elements.filter(el => {
400
+ const name = el.name || '';
401
+ const textMatch = exact
402
+ ? name.toLowerCase() === searchText.toLowerCase()
403
+ : name.toLowerCase().includes(searchText.toLowerCase());
404
+
405
+ if (!textMatch) return false;
406
+ if (type && el.type !== type) return false;
407
+
408
+ return true;
409
+ });
410
+
411
+ return matches.length > 0 ? matches[0] : null;
412
+ }
413
+
414
+ /**
415
+ * Find all elements matching criteria
416
+ */
417
+ findElements(criteria = {}) {
418
+ const { elements } = this.cache;
419
+ if (!elements) return [];
420
+
421
+ return elements.filter(el => {
422
+ if (criteria.text && !el.name?.toLowerCase().includes(criteria.text.toLowerCase())) return false;
423
+ if (criteria.type && el.type !== criteria.type) return false;
424
+ if (criteria.automationId && el.automationId !== criteria.automationId) return false;
425
+ if (criteria.enabledOnly && !el.isEnabled) return false;
426
+ return true;
427
+ });
428
+ }
429
+
430
+ /**
431
+ * Get element at specific coordinates
432
+ */
433
+ getElementAtPoint(x, y) {
434
+ const { elements } = this.cache;
435
+ if (!elements) return null;
436
+
437
+ // Find elements that contain the point, prefer smaller (more specific) elements
438
+ const containing = elements.filter(el => {
439
+ const { bounds } = el;
440
+ return x >= bounds.x && x <= bounds.x + bounds.width &&
441
+ y >= bounds.y && y <= bounds.y + bounds.height;
442
+ });
443
+
444
+ if (containing.length === 0) return null;
445
+
446
+ // Sort by area (smallest first - most specific element)
447
+ containing.sort((a, b) => {
448
+ const areaA = a.bounds.width * a.bounds.height;
449
+ const areaB = b.bounds.width * b.bounds.height;
450
+ return areaA - areaB;
451
+ });
452
+
453
+ return containing[0];
454
+ }
455
+
456
+ /**
457
+ * Get current metrics
458
+ */
459
+ getMetrics() {
460
+ return {
461
+ ...this.metrics,
462
+ cacheSize: this.cache.elements?.length || 0,
463
+ lastUpdate: this.cache.lastUpdate,
464
+ isPolling: this.isPolling
465
+ };
466
+ }
467
+
468
+ /**
469
+ * Clean up
470
+ */
471
+ killPsProcess() {
472
+ if (this.psProcess) {
473
+ try {
474
+ this.psProcess.kill();
475
+ } catch (e) {}
476
+ this.psProcess = null;
477
+ this.psReady = false;
478
+ }
479
+ }
480
+
481
+ /**
482
+ * Destroy watcher
483
+ */
484
+ destroy() {
485
+ this.stop();
486
+ this.removeAllListeners();
487
+ }
488
+ }
489
+
490
+ // Singleton instance
491
+ let instance = null;
492
+
493
+ function getUIWatcher(options) {
494
+ if (!instance) {
495
+ instance = new UIWatcher(options);
496
+ }
497
+ return instance;
498
+ }
499
+
500
+ module.exports = {
501
+ UIWatcher,
502
+ getUIWatcher
503
+ };
@@ -24,7 +24,10 @@ let state = {
24
24
  inspectMode: false,
25
25
  inspectRegions: [],
26
26
  hoveredRegion: null,
27
- selectedRegionId: null
27
+ selectedRegionId: null,
28
+ // Live UI mirror state
29
+ uiMirrorMode: false,
30
+ uiMirrorElements: []
28
31
  };
29
32
 
30
33
  // ===== CANVAS SETUP =====
@@ -448,6 +451,23 @@ if (window.electronAPI) {
448
451
  });
449
452
  }
450
453
 
454
+ // Listen for live UI watcher updates (background UI changes)
455
+ if (window.electronAPI.onUIWatcherUpdate) {
456
+ window.electronAPI.onUIWatcherUpdate((diff) => {
457
+ if (diff && (diff.added?.length || diff.changed?.length || diff.removed?.length)) {
458
+ console.log('[Overlay] UI watcher update:', {
459
+ added: diff.added?.length || 0,
460
+ changed: diff.changed?.length || 0,
461
+ removed: diff.removed?.length || 0
462
+ });
463
+ // Update UI mirror elements for subtle visual feedback
464
+ state.uiMirrorElements = diff.currentElements || [];
465
+ // Could trigger subtle highlight of changed elements here
466
+ // For now, just log - full visual rendering can be added later
467
+ }
468
+ });
469
+ }
470
+
451
471
  // Identify
452
472
  console.log('Hooked electronAPI events');
453
473
  } else {
@@ -86,5 +86,10 @@ contextBridge.exposeInMainWorld('electronAPI', {
86
86
  requestInspectRegions: () => ipcRenderer.send('request-inspect-regions'),
87
87
 
88
88
  // Toggle inspect mode
89
- toggleInspectMode: () => ipcRenderer.send('toggle-inspect-mode')
89
+ toggleInspectMode: () => ipcRenderer.send('toggle-inspect-mode'),
90
+
91
+ // ===== LIVE UI MIRROR API =====
92
+
93
+ // Listen for live UI watcher updates (element changes in background)
94
+ onUIWatcherUpdate: (callback) => ipcRenderer.on('ui-watcher-update', (event, diff) => callback(diff))
90
95
  });