copilot-liku-cli 0.0.1 → 0.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -1
- package/package.json +3 -3
- package/src/cli/liku.js +1 -1
- package/src/main/ai-service.js +128 -33
- package/src/main/index.js +30 -1
- package/src/main/system-automation.js +116 -6
- package/src/main/ui-watcher.js +503 -0
- package/src/renderer/overlay/overlay.js +21 -1
- package/src/renderer/overlay/preload.js +6 -1
package/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# GitHub Copilot CLI: Liku Edition (Public Preview)
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/copilot-liku-cli)
|
|
4
|
+
[](https://nodejs.org/)
|
|
5
|
+
[](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
|
-
|
|
96
|
+
Install globally from npm:
|
|
93
97
|
```bash
|
|
94
98
|
npm install -g copilot-liku-cli
|
|
95
99
|
```
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "copilot-liku-cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "GitHub Copilot CLI with headless agent + ultra-thin overlay architecture",
|
|
5
5
|
"main": "src/main/index.js",
|
|
6
6
|
"bin": {
|
|
7
|
-
"liku": "
|
|
7
|
+
"liku": "src/cli/liku.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "node scripts/start.js",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"license": "MIT",
|
|
27
27
|
"repository": {
|
|
28
28
|
"type": "git",
|
|
29
|
-
"url": "https://github.com/TayDa64/copilot-Liku-cli.git"
|
|
29
|
+
"url": "git+https://github.com/TayDa64/copilot-Liku-cli.git"
|
|
30
30
|
},
|
|
31
31
|
"bugs": {
|
|
32
32
|
"url": "https://github.com/TayDa64/copilot-Liku-cli/issues"
|
package/src/cli/liku.js
CHANGED
package/src/main/ai-service.js
CHANGED
|
@@ -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. **
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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": "
|
|
140
|
-
{"type": "
|
|
141
|
-
{"type": "key", "key": "
|
|
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": "
|
|
216
|
+
"verification": "A new Windows Terminal window should open"
|
|
144
217
|
}
|
|
145
218
|
\`\`\`
|
|
146
219
|
|
|
147
220
|
### Action Types:
|
|
148
|
-
- \`{"type": "
|
|
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", "
|
|
153
|
-
- \`{"type": "scroll", "direction": "up|down", "amount": <number>}\` - Scroll
|
|
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
|
-
-
|
|
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
|
-
-
|
|
174
|
-
-
|
|
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
|
-
**
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
**
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
335
|
-
|
|
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;
|
|
@@ -2148,6 +2154,25 @@ app.whenReady().then(() => {
|
|
|
2148
2154
|
registerShortcuts();
|
|
2149
2155
|
setupIPC();
|
|
2150
2156
|
|
|
2157
|
+
// Start the UI watcher for live UI monitoring
|
|
2158
|
+
try {
|
|
2159
|
+
uiWatcher = new UIWatcher({
|
|
2160
|
+
pollInterval: 400,
|
|
2161
|
+
maxElements: 60,
|
|
2162
|
+
includeInvisible: false
|
|
2163
|
+
});
|
|
2164
|
+
uiWatcher.on('ui-changed', (diff) => {
|
|
2165
|
+
// Forward UI changes to overlay for live mirror updates
|
|
2166
|
+
if (overlayWindow && !overlayWindow.isDestroyed()) {
|
|
2167
|
+
overlayWindow.webContents.send('ui-watcher-update', diff);
|
|
2168
|
+
}
|
|
2169
|
+
});
|
|
2170
|
+
uiWatcher.start();
|
|
2171
|
+
console.log('[Main] UI Watcher started for live UI monitoring');
|
|
2172
|
+
} catch (e) {
|
|
2173
|
+
console.warn('[Main] Could not start UI watcher:', e.message);
|
|
2174
|
+
}
|
|
2175
|
+
|
|
2151
2176
|
// Set up Copilot OAuth callback to notify chat on auth completion
|
|
2152
2177
|
aiService.setOAuthCallback((result) => {
|
|
2153
2178
|
if (chatWindow && !chatWindow.isDestroyed()) {
|
|
@@ -2197,9 +2222,13 @@ app.on('window-all-closed', () => {
|
|
|
2197
2222
|
}
|
|
2198
2223
|
});
|
|
2199
2224
|
|
|
2200
|
-
// Clean up shortcuts on quit
|
|
2225
|
+
// Clean up shortcuts and UI watcher on quit
|
|
2201
2226
|
app.on('will-quit', () => {
|
|
2202
2227
|
globalShortcut.unregisterAll();
|
|
2228
|
+
if (uiWatcher) {
|
|
2229
|
+
uiWatcher.stop();
|
|
2230
|
+
console.log('[Main] UI Watcher stopped');
|
|
2231
|
+
}
|
|
2203
2232
|
});
|
|
2204
2233
|
|
|
2205
2234
|
// Prevent app from quitting when closing chat window
|
|
@@ -443,15 +443,125 @@ Add-Type -AssemblyName System.Windows.Forms
|
|
|
443
443
|
}
|
|
444
444
|
|
|
445
445
|
/**
|
|
446
|
-
* Press a key or key combination (e.g., "ctrl+c", "enter", "alt+tab")
|
|
446
|
+
* Press a key or key combination (e.g., "ctrl+c", "enter", "alt+tab", "win+r")
|
|
447
|
+
* Now supports Windows key using SendInput with virtual key codes
|
|
447
448
|
*/
|
|
448
449
|
async function pressKey(keyCombo) {
|
|
449
|
-
let sendKeysStr = '';
|
|
450
|
-
|
|
451
|
-
// Parse key combo
|
|
452
450
|
const parts = keyCombo.toLowerCase().split('+').map(k => k.trim());
|
|
453
451
|
|
|
454
|
-
//
|
|
452
|
+
// Check if Windows key is involved - requires special handling
|
|
453
|
+
const hasWinKey = parts.includes('win') || parts.includes('windows') || parts.includes('super');
|
|
454
|
+
|
|
455
|
+
if (hasWinKey) {
|
|
456
|
+
// Use SendInput for Windows key combos
|
|
457
|
+
const otherKeys = parts.filter(p => p !== 'win' && p !== 'windows' && p !== 'super');
|
|
458
|
+
const hasCtrl = otherKeys.includes('ctrl') || otherKeys.includes('control');
|
|
459
|
+
const hasAlt = otherKeys.includes('alt');
|
|
460
|
+
const hasShift = otherKeys.includes('shift');
|
|
461
|
+
const mainKey = otherKeys.find(p => !['ctrl', 'control', 'alt', 'shift'].includes(p)) || '';
|
|
462
|
+
|
|
463
|
+
// Virtual key codes for common keys
|
|
464
|
+
const vkCodes = {
|
|
465
|
+
'a': 0x41, 'b': 0x42, 'c': 0x43, 'd': 0x44, 'e': 0x45, 'f': 0x46, 'g': 0x47, 'h': 0x48,
|
|
466
|
+
'i': 0x49, 'j': 0x4A, 'k': 0x4B, 'l': 0x4C, 'm': 0x4D, 'n': 0x4E, 'o': 0x4F, 'p': 0x50,
|
|
467
|
+
'q': 0x51, 'r': 0x52, 's': 0x53, 't': 0x54, 'u': 0x55, 'v': 0x56, 'w': 0x57, 'x': 0x58,
|
|
468
|
+
'y': 0x59, 'z': 0x5A,
|
|
469
|
+
'0': 0x30, '1': 0x31, '2': 0x32, '3': 0x33, '4': 0x34, '5': 0x35, '6': 0x36, '7': 0x37, '8': 0x38, '9': 0x39,
|
|
470
|
+
'enter': 0x0D, 'return': 0x0D, 'tab': 0x09, 'escape': 0x1B, 'esc': 0x1B,
|
|
471
|
+
'space': 0x20, 'backspace': 0x08, 'delete': 0x2E, 'del': 0x2E,
|
|
472
|
+
'up': 0x26, 'down': 0x28, 'left': 0x25, 'right': 0x27,
|
|
473
|
+
'home': 0x24, 'end': 0x23, 'pageup': 0x21, 'pagedown': 0x22,
|
|
474
|
+
'f1': 0x70, 'f2': 0x71, 'f3': 0x72, 'f4': 0x73, 'f5': 0x74, 'f6': 0x75,
|
|
475
|
+
'f7': 0x76, 'f8': 0x77, 'f9': 0x78, 'f10': 0x79, 'f11': 0x7A, 'f12': 0x7B,
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
const mainKeyCode = mainKey ? (vkCodes[mainKey] || mainKey.charCodeAt(0)) : 0;
|
|
479
|
+
|
|
480
|
+
const script = `
|
|
481
|
+
Add-Type -TypeDefinition @"
|
|
482
|
+
using System;
|
|
483
|
+
using System.Runtime.InteropServices;
|
|
484
|
+
|
|
485
|
+
public class WinKeyPress {
|
|
486
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
487
|
+
public struct INPUT {
|
|
488
|
+
public uint type;
|
|
489
|
+
public InputUnion U;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
[StructLayout(LayoutKind.Explicit)]
|
|
493
|
+
public struct InputUnion {
|
|
494
|
+
[FieldOffset(0)] public MOUSEINPUT mi;
|
|
495
|
+
[FieldOffset(0)] public KEYBDINPUT ki;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
499
|
+
public struct MOUSEINPUT {
|
|
500
|
+
public int dx, dy;
|
|
501
|
+
public uint mouseData, dwFlags, time;
|
|
502
|
+
public IntPtr dwExtraInfo;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
506
|
+
public struct KEYBDINPUT {
|
|
507
|
+
public ushort wVk;
|
|
508
|
+
public ushort wScan;
|
|
509
|
+
public uint dwFlags;
|
|
510
|
+
public uint time;
|
|
511
|
+
public IntPtr dwExtraInfo;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
public const uint INPUT_KEYBOARD = 1;
|
|
515
|
+
public const uint KEYEVENTF_KEYUP = 0x0002;
|
|
516
|
+
public const ushort VK_LWIN = 0x5B;
|
|
517
|
+
public const ushort VK_CONTROL = 0x11;
|
|
518
|
+
public const ushort VK_SHIFT = 0x10;
|
|
519
|
+
public const ushort VK_MENU = 0x12; // Alt
|
|
520
|
+
|
|
521
|
+
[DllImport("user32.dll", SetLastError = true)]
|
|
522
|
+
public static extern uint SendInput(uint nInputs, INPUT[] pInputs, int cbSize);
|
|
523
|
+
|
|
524
|
+
public static void KeyDown(ushort vk) {
|
|
525
|
+
INPUT[] inputs = new INPUT[1];
|
|
526
|
+
inputs[0].type = INPUT_KEYBOARD;
|
|
527
|
+
inputs[0].U.ki.wVk = vk;
|
|
528
|
+
inputs[0].U.ki.dwFlags = 0;
|
|
529
|
+
SendInput(1, inputs, Marshal.SizeOf(typeof(INPUT)));
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
public static void KeyUp(ushort vk) {
|
|
533
|
+
INPUT[] inputs = new INPUT[1];
|
|
534
|
+
inputs[0].type = INPUT_KEYBOARD;
|
|
535
|
+
inputs[0].U.ki.wVk = vk;
|
|
536
|
+
inputs[0].U.ki.dwFlags = KEYEVENTF_KEYUP;
|
|
537
|
+
SendInput(1, inputs, Marshal.SizeOf(typeof(INPUT)));
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
"@
|
|
541
|
+
|
|
542
|
+
# Press modifiers
|
|
543
|
+
[WinKeyPress]::KeyDown([WinKeyPress]::VK_LWIN)
|
|
544
|
+
${hasCtrl ? '[WinKeyPress]::KeyDown([WinKeyPress]::VK_CONTROL)' : ''}
|
|
545
|
+
${hasAlt ? '[WinKeyPress]::KeyDown([WinKeyPress]::VK_MENU)' : ''}
|
|
546
|
+
${hasShift ? '[WinKeyPress]::KeyDown([WinKeyPress]::VK_SHIFT)' : ''}
|
|
547
|
+
|
|
548
|
+
# Press main key if any
|
|
549
|
+
${mainKeyCode ? `[WinKeyPress]::KeyDown(${mainKeyCode})
|
|
550
|
+
Start-Sleep -Milliseconds 50
|
|
551
|
+
[WinKeyPress]::KeyUp(${mainKeyCode})` : 'Start-Sleep -Milliseconds 100'}
|
|
552
|
+
|
|
553
|
+
# Release modifiers in reverse order
|
|
554
|
+
${hasShift ? '[WinKeyPress]::KeyUp([WinKeyPress]::VK_SHIFT)' : ''}
|
|
555
|
+
${hasAlt ? '[WinKeyPress]::KeyUp([WinKeyPress]::VK_MENU)' : ''}
|
|
556
|
+
${hasCtrl ? '[WinKeyPress]::KeyUp([WinKeyPress]::VK_CONTROL)' : ''}
|
|
557
|
+
[WinKeyPress]::KeyUp([WinKeyPress]::VK_LWIN)
|
|
558
|
+
`;
|
|
559
|
+
await executePowerShell(script);
|
|
560
|
+
console.log(`[AUTOMATION] Pressed Windows key combo: ${keyCombo} (using SendInput)`);
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Non-Windows key combos use SendKeys
|
|
455
565
|
let modifiers = '';
|
|
456
566
|
let mainKey = '';
|
|
457
567
|
|
|
@@ -470,7 +580,7 @@ async function pressKey(keyCombo) {
|
|
|
470
580
|
}
|
|
471
581
|
}
|
|
472
582
|
|
|
473
|
-
sendKeysStr = modifiers + (mainKey ? `(${mainKey})` : '');
|
|
583
|
+
const sendKeysStr = modifiers + (mainKey ? `(${mainKey})` : '');
|
|
474
584
|
|
|
475
585
|
if (!sendKeysStr) {
|
|
476
586
|
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
|
});
|