cicy-desktop 1.0.8
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/.github/workflows/build.yml +85 -0
- package/.kiro/steering/dev-workflow.md +166 -0
- package/AGENTS.md +247 -0
- package/CLAUDE.md +162 -0
- package/DOCKER.md +85 -0
- package/Dockerfile +46 -0
- package/README.md +720 -0
- package/TODO-anti-detection.md +326 -0
- package/bin/cicy +176 -0
- package/bin/preinstall.sh +32 -0
- package/copy-to-desktop.sh +26 -0
- package/docs/AUTOMATION-API.md +342 -0
- package/docs/REQUEST_MONITORING.md +435 -0
- package/docs/REST-API-FEATURE.md +155 -0
- package/docs/REST-API.md +319 -0
- package/docs/feature-distributed-multi-agent.md +555 -0
- package/docs/yaml.md +255 -0
- package/electron-mcp-fixed.command +134 -0
- package/electron-mcp-simple.command +135 -0
- package/electron-mcp.command +92 -0
- package/generate-openapi.js +158 -0
- package/jest.config.js +10 -0
- package/jest.setup.global.js +13 -0
- package/jest.teardown.global.js +7 -0
- package/package.json +75 -0
- package/service.sh +164 -0
- package/src/config.js +8 -0
- package/src/extension/inject.js +135 -0
- package/src/main-old.js +837 -0
- package/src/main.js +403 -0
- package/src/preload-rpc.js +4 -0
- package/src/server/args-parser.js +37 -0
- package/src/server/electron-setup.js +33 -0
- package/src/server/express-app.js +166 -0
- package/src/server/logging.js +58 -0
- package/src/server/mcp-server.js +53 -0
- package/src/server/tool-registry.js +77 -0
- package/src/server/ui-routes.js +81 -0
- package/src/swagger-ui.html +41 -0
- package/src/tools/account-tools.js +194 -0
- package/src/tools/automation-tools.js +297 -0
- package/src/tools/cdp-tools.js +444 -0
- package/src/tools/clipboard-tools.js +180 -0
- package/src/tools/download-tools.js +57 -0
- package/src/tools/exec-js.js +297 -0
- package/src/tools/exec-tools.js +139 -0
- package/src/tools/file-tools.js +212 -0
- package/src/tools/hook-chatgpt.js +489 -0
- package/src/tools/hook-gemini.js +454 -0
- package/src/tools/index.js +19 -0
- package/src/tools/ipc-bridge.js +31 -0
- package/src/tools/ping.js +60 -0
- package/src/tools/r-reset.js +28 -0
- package/src/tools/screenshot-tools.js +28 -0
- package/src/tools/system-tools.js +531 -0
- package/src/tools/window-tools.js +882 -0
- package/src/ui.html +914 -0
- package/src/utils/auth.js +81 -0
- package/src/utils/cdp-utils.js +8 -0
- package/src/utils/download-manager.js +41 -0
- package/src/utils/process-utils.js +185 -0
- package/src/utils/snapshot-utils.js +56 -0
- package/src/utils/window-monitor.js +605 -0
- package/src/utils/window-state.js +137 -0
- package/src/utils/window-utils.js +336 -0
- package/update-desktop.sh +33 -0
package/src/ui.html
ADDED
|
@@ -0,0 +1,914 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Electron Window Monitor</title>
|
|
7
|
+
<style>
|
|
8
|
+
* {
|
|
9
|
+
box-sizing: border-box;
|
|
10
|
+
margin: 0;
|
|
11
|
+
padding: 0;
|
|
12
|
+
}
|
|
13
|
+
body {
|
|
14
|
+
font-family:
|
|
15
|
+
system-ui,
|
|
16
|
+
-apple-system,
|
|
17
|
+
sans-serif;
|
|
18
|
+
background: #0f1117;
|
|
19
|
+
color: #e2e8f0;
|
|
20
|
+
min-height: 100vh;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/* ── Buttons ── */
|
|
24
|
+
.btn {
|
|
25
|
+
display: inline-flex;
|
|
26
|
+
align-items: center;
|
|
27
|
+
justify-content: center;
|
|
28
|
+
padding: 8px 16px;
|
|
29
|
+
border-radius: 7px;
|
|
30
|
+
border: none;
|
|
31
|
+
cursor: pointer;
|
|
32
|
+
font-size: 0.85rem;
|
|
33
|
+
font-weight: 500;
|
|
34
|
+
transition: opacity 0.15s;
|
|
35
|
+
}
|
|
36
|
+
.btn:hover:not(:disabled) {
|
|
37
|
+
opacity: 0.8;
|
|
38
|
+
}
|
|
39
|
+
.btn:disabled {
|
|
40
|
+
opacity: 0.4;
|
|
41
|
+
cursor: default;
|
|
42
|
+
}
|
|
43
|
+
.btn-primary {
|
|
44
|
+
background: #6366f1;
|
|
45
|
+
color: #fff;
|
|
46
|
+
}
|
|
47
|
+
.btn-success {
|
|
48
|
+
background: #16a34a;
|
|
49
|
+
color: #fff;
|
|
50
|
+
}
|
|
51
|
+
.btn-danger {
|
|
52
|
+
background: #dc2626;
|
|
53
|
+
color: #fff;
|
|
54
|
+
}
|
|
55
|
+
.btn-ghost {
|
|
56
|
+
background: #1e2235;
|
|
57
|
+
color: #94a3b8;
|
|
58
|
+
border: 1px solid #2d3148;
|
|
59
|
+
}
|
|
60
|
+
.btn-sm {
|
|
61
|
+
padding: 5px 11px;
|
|
62
|
+
font-size: 0.78rem;
|
|
63
|
+
}
|
|
64
|
+
.btn-block {
|
|
65
|
+
width: 100%;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/* ── Login ── */
|
|
69
|
+
#login-view {
|
|
70
|
+
display: none;
|
|
71
|
+
align-items: center;
|
|
72
|
+
justify-content: center;
|
|
73
|
+
min-height: 100vh;
|
|
74
|
+
}
|
|
75
|
+
.login-box {
|
|
76
|
+
background: #1a1d2e;
|
|
77
|
+
border: 1px solid #2d3148;
|
|
78
|
+
border-radius: 12px;
|
|
79
|
+
padding: 40px;
|
|
80
|
+
width: 360px;
|
|
81
|
+
}
|
|
82
|
+
.login-box h1 {
|
|
83
|
+
font-size: 1.4rem;
|
|
84
|
+
margin-bottom: 8px;
|
|
85
|
+
}
|
|
86
|
+
.login-box p {
|
|
87
|
+
color: #94a3b8;
|
|
88
|
+
font-size: 0.875rem;
|
|
89
|
+
margin-bottom: 24px;
|
|
90
|
+
}
|
|
91
|
+
.login-box input {
|
|
92
|
+
width: 100%;
|
|
93
|
+
padding: 10px 14px;
|
|
94
|
+
border-radius: 8px;
|
|
95
|
+
border: 1px solid #3d4265;
|
|
96
|
+
background: #0f1117;
|
|
97
|
+
color: #e2e8f0;
|
|
98
|
+
font-size: 0.9rem;
|
|
99
|
+
margin-bottom: 12px;
|
|
100
|
+
outline: none;
|
|
101
|
+
}
|
|
102
|
+
.login-box input:focus {
|
|
103
|
+
border-color: #6366f1;
|
|
104
|
+
}
|
|
105
|
+
#login-error {
|
|
106
|
+
color: #f87171;
|
|
107
|
+
font-size: 0.8rem;
|
|
108
|
+
margin-top: 8px;
|
|
109
|
+
min-height: 18px;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/* ── Main view (split pane) ── */
|
|
113
|
+
#main-view {
|
|
114
|
+
display: none;
|
|
115
|
+
flex-direction: column;
|
|
116
|
+
height: 100vh;
|
|
117
|
+
overflow: hidden;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/* Top bar */
|
|
121
|
+
.topbar {
|
|
122
|
+
display: flex;
|
|
123
|
+
align-items: center;
|
|
124
|
+
justify-content: space-between;
|
|
125
|
+
padding: 10px 20px;
|
|
126
|
+
gap: 12px;
|
|
127
|
+
flex-wrap: wrap;
|
|
128
|
+
flex-shrink: 0;
|
|
129
|
+
background: #1a1d2e;
|
|
130
|
+
border-bottom: 1px solid #2d3148;
|
|
131
|
+
}
|
|
132
|
+
.topbar h1 {
|
|
133
|
+
font-size: 1.1rem;
|
|
134
|
+
display: flex;
|
|
135
|
+
align-items: center;
|
|
136
|
+
gap: 8px;
|
|
137
|
+
}
|
|
138
|
+
.topbar-actions {
|
|
139
|
+
display: flex;
|
|
140
|
+
gap: 8px;
|
|
141
|
+
flex-wrap: wrap;
|
|
142
|
+
}
|
|
143
|
+
.win-select {
|
|
144
|
+
padding: 6px 12px;
|
|
145
|
+
border-radius: 6px;
|
|
146
|
+
border: 1px solid #3d4265;
|
|
147
|
+
background: #0f1117;
|
|
148
|
+
color: #e2e8f0;
|
|
149
|
+
font-size: 0.85rem;
|
|
150
|
+
min-width: 200px;
|
|
151
|
+
cursor: pointer;
|
|
152
|
+
outline: none;
|
|
153
|
+
}
|
|
154
|
+
.win-select:focus {
|
|
155
|
+
border-color: #6366f1;
|
|
156
|
+
}
|
|
157
|
+
.live-dot {
|
|
158
|
+
width: 8px;
|
|
159
|
+
height: 8px;
|
|
160
|
+
border-radius: 50%;
|
|
161
|
+
background: #4ade80;
|
|
162
|
+
animation: pulse 1.4s infinite;
|
|
163
|
+
}
|
|
164
|
+
@keyframes pulse {
|
|
165
|
+
0%,
|
|
166
|
+
100% {
|
|
167
|
+
opacity: 1;
|
|
168
|
+
}
|
|
169
|
+
50% {
|
|
170
|
+
opacity: 0.25;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/* Bottom pane (live capture + controls) */
|
|
175
|
+
#bottom-pane {
|
|
176
|
+
flex: 1;
|
|
177
|
+
display: flex;
|
|
178
|
+
overflow: hidden;
|
|
179
|
+
}
|
|
180
|
+
.bp-empty {
|
|
181
|
+
flex: 1;
|
|
182
|
+
display: flex;
|
|
183
|
+
align-items: center;
|
|
184
|
+
justify-content: center;
|
|
185
|
+
color: #475569;
|
|
186
|
+
font-size: 0.9rem;
|
|
187
|
+
}
|
|
188
|
+
.bp-capture {
|
|
189
|
+
flex: 1;
|
|
190
|
+
background: #000;
|
|
191
|
+
display: flex;
|
|
192
|
+
align-items: center;
|
|
193
|
+
justify-content: center;
|
|
194
|
+
overflow: hidden;
|
|
195
|
+
}
|
|
196
|
+
#bp-img {
|
|
197
|
+
max-width: 100%;
|
|
198
|
+
max-height: 100%;
|
|
199
|
+
object-fit: contain;
|
|
200
|
+
display: block;
|
|
201
|
+
}
|
|
202
|
+
.bp-panel {
|
|
203
|
+
width: 260px;
|
|
204
|
+
flex-shrink: 0;
|
|
205
|
+
background: #1a1d2e;
|
|
206
|
+
border-left: 1px solid #2d3148;
|
|
207
|
+
padding: 14px;
|
|
208
|
+
overflow-y: auto;
|
|
209
|
+
display: flex;
|
|
210
|
+
flex-direction: column;
|
|
211
|
+
gap: 18px;
|
|
212
|
+
}
|
|
213
|
+
.panel-section h3 {
|
|
214
|
+
font-size: 0.7rem;
|
|
215
|
+
font-weight: 600;
|
|
216
|
+
text-transform: uppercase;
|
|
217
|
+
letter-spacing: 0.07em;
|
|
218
|
+
color: #64748b;
|
|
219
|
+
margin-bottom: 8px;
|
|
220
|
+
}
|
|
221
|
+
.info-row {
|
|
222
|
+
font-size: 0.78rem;
|
|
223
|
+
margin-bottom: 3px;
|
|
224
|
+
word-break: break-all;
|
|
225
|
+
color: #94a3b8;
|
|
226
|
+
}
|
|
227
|
+
.info-row strong {
|
|
228
|
+
color: #e2e8f0;
|
|
229
|
+
}
|
|
230
|
+
.bounds-grid {
|
|
231
|
+
display: grid;
|
|
232
|
+
grid-template-columns: 1fr 1fr;
|
|
233
|
+
gap: 7px;
|
|
234
|
+
margin-bottom: 9px;
|
|
235
|
+
}
|
|
236
|
+
.field {
|
|
237
|
+
display: flex;
|
|
238
|
+
flex-direction: column;
|
|
239
|
+
gap: 3px;
|
|
240
|
+
}
|
|
241
|
+
.field label {
|
|
242
|
+
font-size: 0.72rem;
|
|
243
|
+
color: #94a3b8;
|
|
244
|
+
}
|
|
245
|
+
.field input[type="number"] {
|
|
246
|
+
padding: 5px 8px;
|
|
247
|
+
border-radius: 6px;
|
|
248
|
+
border: 1px solid #3d4265;
|
|
249
|
+
background: #0f1117;
|
|
250
|
+
color: #e2e8f0;
|
|
251
|
+
font-size: 0.82rem;
|
|
252
|
+
width: 100%;
|
|
253
|
+
outline: none;
|
|
254
|
+
}
|
|
255
|
+
.field input[type="number"]:focus {
|
|
256
|
+
border-color: #6366f1;
|
|
257
|
+
}
|
|
258
|
+
.interval-row {
|
|
259
|
+
display: flex;
|
|
260
|
+
align-items: center;
|
|
261
|
+
gap: 7px;
|
|
262
|
+
}
|
|
263
|
+
.interval-row input[type="range"] {
|
|
264
|
+
flex: 1;
|
|
265
|
+
accent-color: #6366f1;
|
|
266
|
+
}
|
|
267
|
+
.interval-label {
|
|
268
|
+
font-size: 0.78rem;
|
|
269
|
+
color: #e2e8f0;
|
|
270
|
+
white-space: nowrap;
|
|
271
|
+
min-width: 36px;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/* ── Snapshot dialog overlay ── */
|
|
275
|
+
#snap-overlay {
|
|
276
|
+
display: none;
|
|
277
|
+
position: fixed;
|
|
278
|
+
inset: 0;
|
|
279
|
+
background: rgba(0, 0, 0, 0.7);
|
|
280
|
+
z-index: 1000;
|
|
281
|
+
align-items: center;
|
|
282
|
+
justify-content: center;
|
|
283
|
+
}
|
|
284
|
+
#snap-overlay.open {
|
|
285
|
+
display: flex;
|
|
286
|
+
}
|
|
287
|
+
.snap-dialog {
|
|
288
|
+
background: #1a1d2e;
|
|
289
|
+
border: 1px solid #3d4265;
|
|
290
|
+
border-radius: 12px;
|
|
291
|
+
width: min(720px, 92vw);
|
|
292
|
+
max-height: 80vh;
|
|
293
|
+
display: flex;
|
|
294
|
+
flex-direction: column;
|
|
295
|
+
overflow: hidden;
|
|
296
|
+
}
|
|
297
|
+
.snap-dialog-header {
|
|
298
|
+
display: flex;
|
|
299
|
+
align-items: center;
|
|
300
|
+
justify-content: space-between;
|
|
301
|
+
padding: 12px 16px;
|
|
302
|
+
border-bottom: 1px solid #2d3148;
|
|
303
|
+
flex-shrink: 0;
|
|
304
|
+
}
|
|
305
|
+
.snap-dialog-header h3 {
|
|
306
|
+
font-size: 0.9rem;
|
|
307
|
+
}
|
|
308
|
+
.snap-dialog-body {
|
|
309
|
+
overflow-y: auto;
|
|
310
|
+
padding: 16px;
|
|
311
|
+
font-family: monospace;
|
|
312
|
+
font-size: 0.8rem;
|
|
313
|
+
line-height: 1.6;
|
|
314
|
+
color: #cbd5e1;
|
|
315
|
+
white-space: pre-wrap;
|
|
316
|
+
word-break: break-word;
|
|
317
|
+
flex: 1;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/* ── Watch view (iframe embed) ── */
|
|
321
|
+
#watch-view {
|
|
322
|
+
display: none;
|
|
323
|
+
}
|
|
324
|
+
#watch-img {
|
|
325
|
+
width: 100vw;
|
|
326
|
+
height: 100vh;
|
|
327
|
+
object-fit: contain;
|
|
328
|
+
background: #000;
|
|
329
|
+
display: block;
|
|
330
|
+
}
|
|
331
|
+
</style>
|
|
332
|
+
</head>
|
|
333
|
+
<body>
|
|
334
|
+
<!-- LOGIN VIEW -->
|
|
335
|
+
<div id="login-view">
|
|
336
|
+
<div class="login-box">
|
|
337
|
+
<h1>Window Monitor</h1>
|
|
338
|
+
<p>Enter your authentication token to continue.</p>
|
|
339
|
+
<input type="password" id="token-input" placeholder="Token" autocomplete="off" />
|
|
340
|
+
<button class="btn btn-primary btn-block" id="login-btn">Sign in</button>
|
|
341
|
+
<div id="login-error"></div>
|
|
342
|
+
</div>
|
|
343
|
+
</div>
|
|
344
|
+
|
|
345
|
+
<!-- MAIN VIEW: top grid selector + bottom detail pane -->
|
|
346
|
+
<div id="main-view">
|
|
347
|
+
<!-- Top bar -->
|
|
348
|
+
<div class="topbar">
|
|
349
|
+
<h1><span class="live-dot"></span> Window Monitor</h1>
|
|
350
|
+
<div class="topbar-actions">
|
|
351
|
+
<button class="btn btn-ghost btn-sm" id="refresh-btn">Refresh</button>
|
|
352
|
+
<button class="btn btn-primary btn-sm" id="open-chatgpt-btn">ChatGPT</button>
|
|
353
|
+
<button class="btn btn-success btn-sm" id="loop-btn">⏸</button>
|
|
354
|
+
<button class="btn btn-ghost btn-sm" id="toggle-panel-btn">☰</button>
|
|
355
|
+
<button class="btn btn-danger btn-sm" id="close-all-btn">Close All</button>
|
|
356
|
+
<button class="btn btn-ghost btn-sm" id="logout-btn">Logout</button>
|
|
357
|
+
</div>
|
|
358
|
+
</div>
|
|
359
|
+
<!-- Select bar -->
|
|
360
|
+
<div style="padding: 8px 20px; background: #1a1d2e; border-bottom: 1px solid #2d3148">
|
|
361
|
+
<select id="win-select" class="win-select" style="width: 100%">
|
|
362
|
+
<option value="">Select a window...</option>
|
|
363
|
+
</select>
|
|
364
|
+
</div>
|
|
365
|
+
|
|
366
|
+
<!-- Bottom pane -->
|
|
367
|
+
<div id="bottom-pane"></div>
|
|
368
|
+
</div>
|
|
369
|
+
|
|
370
|
+
<!-- WATCH VIEW (iframe-only, no nav chrome) -->
|
|
371
|
+
<div id="watch-view">
|
|
372
|
+
<img id="watch-img" alt="Live capture" />
|
|
373
|
+
</div>
|
|
374
|
+
|
|
375
|
+
<!-- WEBPAGE SNAPSHOT DIALOG -->
|
|
376
|
+
<div id="snap-overlay">
|
|
377
|
+
<div class="snap-dialog">
|
|
378
|
+
<div class="snap-dialog-header">
|
|
379
|
+
<h3>Page Snapshot</h3>
|
|
380
|
+
<button class="btn btn-ghost btn-sm" id="snap-close-btn">✕ Close</button>
|
|
381
|
+
</div>
|
|
382
|
+
<div class="snap-dialog-body" id="snap-body">Loading…</div>
|
|
383
|
+
</div>
|
|
384
|
+
</div>
|
|
385
|
+
|
|
386
|
+
<script>
|
|
387
|
+
(function () {
|
|
388
|
+
const TOKEN_KEY = "ELECTRON_MCP_TOKEN";
|
|
389
|
+
const SELECTED_WIN_KEY = "ELECTRON_MCP_SELECTED_WIN";
|
|
390
|
+
const INTERVAL_KEY = "ELECTRON_MCP_INTERVAL";
|
|
391
|
+
const QUALITY_KEY = "ELECTRON_MCP_QUALITY";
|
|
392
|
+
const SCALE_KEY = "ELECTRON_MCP_SCALE";
|
|
393
|
+
const LOOP_KEY = "ELECTRON_MCP_LOOP";
|
|
394
|
+
let watchTimer = null;
|
|
395
|
+
let currentWinId = null;
|
|
396
|
+
let captureInterval = parseInt(localStorage.getItem(INTERVAL_KEY)) || 1000;
|
|
397
|
+
let jpegQuality = parseInt(localStorage.getItem(QUALITY_KEY)) || 80;
|
|
398
|
+
let captureScale = parseFloat(localStorage.getItem(SCALE_KEY)) || 0.5;
|
|
399
|
+
let loopEnabled = localStorage.getItem(LOOP_KEY) !== "false";
|
|
400
|
+
|
|
401
|
+
function $(id) {
|
|
402
|
+
return document.getElementById(id);
|
|
403
|
+
}
|
|
404
|
+
function getToken() {
|
|
405
|
+
return localStorage.getItem(TOKEN_KEY) || "";
|
|
406
|
+
}
|
|
407
|
+
function saveToken(t) {
|
|
408
|
+
localStorage.setItem(TOKEN_KEY, t);
|
|
409
|
+
}
|
|
410
|
+
function saveInterval(v) {
|
|
411
|
+
localStorage.setItem(INTERVAL_KEY, v);
|
|
412
|
+
}
|
|
413
|
+
function saveQuality(v) {
|
|
414
|
+
localStorage.setItem(QUALITY_KEY, v);
|
|
415
|
+
}
|
|
416
|
+
function saveScale(v) {
|
|
417
|
+
localStorage.setItem(SCALE_KEY, v);
|
|
418
|
+
}
|
|
419
|
+
function getSelectedWin() {
|
|
420
|
+
const v = localStorage.getItem(SELECTED_WIN_KEY);
|
|
421
|
+
return v ? parseInt(v) : null;
|
|
422
|
+
}
|
|
423
|
+
function saveSelectedWin(winId) {
|
|
424
|
+
localStorage.setItem(SELECTED_WIN_KEY, winId);
|
|
425
|
+
}
|
|
426
|
+
function clearToken() {
|
|
427
|
+
localStorage.removeItem(TOKEN_KEY);
|
|
428
|
+
}
|
|
429
|
+
function tok() {
|
|
430
|
+
return `token=${encodeURIComponent(getToken())}`;
|
|
431
|
+
}
|
|
432
|
+
function snapUrl(winId) {
|
|
433
|
+
return `/ui/snapshot?win_id=${winId}&quality=${jpegQuality}&scale=${captureScale}&${tok()}`;
|
|
434
|
+
}
|
|
435
|
+
function escHtml(s) {
|
|
436
|
+
return String(s)
|
|
437
|
+
.replace(/&/g, "&")
|
|
438
|
+
.replace(/</g, "<")
|
|
439
|
+
.replace(/>/g, ">")
|
|
440
|
+
.replace(/"/g, """);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// ── RPC ─────────────────────────────────────────────────
|
|
444
|
+
async function rpc(tool, args = {}) {
|
|
445
|
+
const res = await fetch(`/rpc/${tool}`, {
|
|
446
|
+
method: "POST",
|
|
447
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${getToken()}` },
|
|
448
|
+
body: JSON.stringify(args),
|
|
449
|
+
});
|
|
450
|
+
if (res.status === 401) {
|
|
451
|
+
clearToken();
|
|
452
|
+
showLogin();
|
|
453
|
+
throw new Error("Unauthorized");
|
|
454
|
+
}
|
|
455
|
+
return res;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
async function rpcJson(tool, args = {}) {
|
|
459
|
+
const res = await rpc(tool, args);
|
|
460
|
+
if (!res.ok) throw new Error(`rpc/${tool} → ${res.status}`);
|
|
461
|
+
const data = await res.json();
|
|
462
|
+
return JSON.parse(data.result.content[0].text);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function stopWatch() {
|
|
466
|
+
if (watchTimer) {
|
|
467
|
+
clearInterval(watchTimer);
|
|
468
|
+
watchTimer = null;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function showView(id) {
|
|
473
|
+
["login-view", "main-view", "watch-view"].forEach((v) => {
|
|
474
|
+
$(v).style.display =
|
|
475
|
+
v === id
|
|
476
|
+
? v === "main-view"
|
|
477
|
+
? "flex"
|
|
478
|
+
: v === "login-view"
|
|
479
|
+
? "flex"
|
|
480
|
+
: "block"
|
|
481
|
+
: "none";
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function showLogin() {
|
|
486
|
+
showView("login-view");
|
|
487
|
+
setupLogin();
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// ── URL param bootstrap ─────────────────────────────────
|
|
491
|
+
const params = new URLSearchParams(location.search);
|
|
492
|
+
const urlToken = params.get("token");
|
|
493
|
+
const urlWinId = params.get("win_id");
|
|
494
|
+
|
|
495
|
+
if (urlToken) {
|
|
496
|
+
saveToken(urlToken);
|
|
497
|
+
history.replaceState(
|
|
498
|
+
null,
|
|
499
|
+
"",
|
|
500
|
+
location.pathname + (urlWinId ? `?win_id=${urlWinId}` : "")
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
// iframe watch-only mode
|
|
505
|
+
if (urlWinId) {
|
|
506
|
+
showView("watch-view");
|
|
507
|
+
iframeWatch(parseInt(urlWinId));
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// ── Entry ───────────────────────────────────────────────
|
|
512
|
+
if (!getToken()) {
|
|
513
|
+
showView("login-view");
|
|
514
|
+
setupLogin();
|
|
515
|
+
} else {
|
|
516
|
+
showView("main-view");
|
|
517
|
+
loadWindows();
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// ── Login ───────────────────────────────────────────────
|
|
521
|
+
function setupLogin() {
|
|
522
|
+
const input = $("token-input");
|
|
523
|
+
const btn = $("login-btn");
|
|
524
|
+
const err = $("login-error");
|
|
525
|
+
|
|
526
|
+
async function tryLogin() {
|
|
527
|
+
const t = input.value.trim();
|
|
528
|
+
if (!t) {
|
|
529
|
+
err.textContent = "Token required.";
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
err.textContent = "";
|
|
533
|
+
btn.disabled = true;
|
|
534
|
+
btn.textContent = "Verifying…";
|
|
535
|
+
try {
|
|
536
|
+
const res = await fetch("/rpc/get_windows", {
|
|
537
|
+
method: "POST",
|
|
538
|
+
headers: { "Content-Type": "application/json", Authorization: `Bearer ${t}` },
|
|
539
|
+
body: "{}",
|
|
540
|
+
});
|
|
541
|
+
if (res.ok) {
|
|
542
|
+
saveToken(t);
|
|
543
|
+
showView("main-view");
|
|
544
|
+
loadWindows();
|
|
545
|
+
} else {
|
|
546
|
+
err.textContent = "Invalid token.";
|
|
547
|
+
}
|
|
548
|
+
} catch {
|
|
549
|
+
err.textContent = "Connection error.";
|
|
550
|
+
} finally {
|
|
551
|
+
btn.disabled = false;
|
|
552
|
+
btn.textContent = "Sign in";
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
btn.onclick = tryLogin;
|
|
557
|
+
input.onkeydown = (e) => {
|
|
558
|
+
if (e.key === "Enter") tryLogin();
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// ── Grid (top selector) ─────────────────────────────────
|
|
563
|
+
async function loadWindows() {
|
|
564
|
+
const btn = $("refresh-btn");
|
|
565
|
+
if (btn) {
|
|
566
|
+
btn.disabled = true;
|
|
567
|
+
btn.textContent = "Refreshing…";
|
|
568
|
+
}
|
|
569
|
+
try {
|
|
570
|
+
const wins = await rpcJson("get_windows");
|
|
571
|
+
renderWindows(Array.isArray(wins) ? wins : []);
|
|
572
|
+
} catch (e) {
|
|
573
|
+
if (e.message !== "Unauthorized") console.error(e);
|
|
574
|
+
} finally {
|
|
575
|
+
if (btn) {
|
|
576
|
+
btn.disabled = false;
|
|
577
|
+
btn.textContent = "Refresh";
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
function renderWindows(wins) {
|
|
583
|
+
const select = $("win-select");
|
|
584
|
+
const selWinId = currentWinId || getSelectedWin();
|
|
585
|
+
|
|
586
|
+
if (!wins.length) {
|
|
587
|
+
select.innerHTML = '<option value="">No windows open</option>';
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Build select options with title, url, dimensions, id
|
|
592
|
+
select.innerHTML =
|
|
593
|
+
'<option value="">Select a window...</option>' +
|
|
594
|
+
wins
|
|
595
|
+
.map((w) => {
|
|
596
|
+
const { x, y, width, height } = w.bounds;
|
|
597
|
+
const label = `#${w.id} ${w.title || "(no title)"} - ${width}×${height} @(${x},${y})`;
|
|
598
|
+
const selected = w.id === selWinId ? " selected" : "";
|
|
599
|
+
return `<option value="${w.id}"${selected}>${escHtml(label)}</option>`;
|
|
600
|
+
})
|
|
601
|
+
.join("");
|
|
602
|
+
|
|
603
|
+
// Handle select change
|
|
604
|
+
select.onchange = () => {
|
|
605
|
+
const winId = parseInt(select.value);
|
|
606
|
+
if (winId) selectWindow(winId);
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
// Auto-select saved window
|
|
610
|
+
if (!currentWinId && selWinId) {
|
|
611
|
+
const exists = wins.find((w) => w.id === selWinId);
|
|
612
|
+
if (exists) {
|
|
613
|
+
selectWindow(selWinId);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
$("refresh-btn").onclick = () => {
|
|
619
|
+
loadWindows();
|
|
620
|
+
if (currentWinId && window._detailTick) {
|
|
621
|
+
window._detailTick();
|
|
622
|
+
}
|
|
623
|
+
};
|
|
624
|
+
|
|
625
|
+
// Toggle loop
|
|
626
|
+
const loopBtn = $("loop-btn");
|
|
627
|
+
function updateLoopBtn() {
|
|
628
|
+
loopBtn.textContent = loopEnabled ? "⏸" : "▶";
|
|
629
|
+
loopBtn.className = loopEnabled ? "btn btn-success btn-sm" : "btn btn-ghost btn-sm";
|
|
630
|
+
}
|
|
631
|
+
updateLoopBtn();
|
|
632
|
+
loopBtn.onclick = () => {
|
|
633
|
+
loopEnabled = !loopEnabled;
|
|
634
|
+
localStorage.setItem(LOOP_KEY, loopEnabled);
|
|
635
|
+
updateLoopBtn();
|
|
636
|
+
if (loopEnabled && currentWinId && window._detailTick) {
|
|
637
|
+
watchTimer = setInterval(window._detailTick, captureInterval);
|
|
638
|
+
} else if (!loopEnabled && watchTimer) {
|
|
639
|
+
clearInterval(watchTimer);
|
|
640
|
+
watchTimer = null;
|
|
641
|
+
}
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
// Toggle right panel
|
|
645
|
+
let panelVisible = localStorage.getItem("PANEL_VISIBLE") !== "false";
|
|
646
|
+
$("toggle-panel-btn").onclick = () => {
|
|
647
|
+
panelVisible = !panelVisible;
|
|
648
|
+
localStorage.setItem("PANEL_VISIBLE", panelVisible);
|
|
649
|
+
const bp = $("bottom-pane");
|
|
650
|
+
const panel = bp ? bp.querySelector(".bp-panel") : null;
|
|
651
|
+
if (panel) {
|
|
652
|
+
panel.style.display = panelVisible ? "flex" : "none";
|
|
653
|
+
}
|
|
654
|
+
};
|
|
655
|
+
|
|
656
|
+
$("open-chatgpt-btn").onclick = async () => {
|
|
657
|
+
try {
|
|
658
|
+
const wins = await rpcJson("get_windows");
|
|
659
|
+
const chatgptWin = (Array.isArray(wins) ? wins : []).find(
|
|
660
|
+
(w) => w.url && w.url.includes("chatgpt.com")
|
|
661
|
+
);
|
|
662
|
+
if (chatgptWin) {
|
|
663
|
+
selectWindow(chatgptWin.id);
|
|
664
|
+
} else {
|
|
665
|
+
await rpc("open_window", {
|
|
666
|
+
url: "https://chatgpt.com",
|
|
667
|
+
width: 1200,
|
|
668
|
+
height: 800,
|
|
669
|
+
});
|
|
670
|
+
loadWindows();
|
|
671
|
+
}
|
|
672
|
+
} catch (e) {
|
|
673
|
+
console.error(e);
|
|
674
|
+
}
|
|
675
|
+
};
|
|
676
|
+
|
|
677
|
+
$("close-all-btn").onclick = async () => {
|
|
678
|
+
if (!confirm("Close all windows?")) return;
|
|
679
|
+
const wins = await rpcJson("get_windows").catch(() => []);
|
|
680
|
+
await Promise.all(
|
|
681
|
+
(Array.isArray(wins) ? wins : []).map((w) => rpc("close_window", { win_id: w.id }))
|
|
682
|
+
);
|
|
683
|
+
stopWatch();
|
|
684
|
+
currentWinId = null;
|
|
685
|
+
showEmptyPane();
|
|
686
|
+
loadWindows();
|
|
687
|
+
};
|
|
688
|
+
|
|
689
|
+
$("logout-btn").onclick = () => {
|
|
690
|
+
clearToken();
|
|
691
|
+
stopWatch();
|
|
692
|
+
currentWinId = null;
|
|
693
|
+
showLogin();
|
|
694
|
+
};
|
|
695
|
+
|
|
696
|
+
window.quickClose = async function (winId) {
|
|
697
|
+
if (!confirm(`Close window #${winId}?`)) return;
|
|
698
|
+
await rpc("close_window", { win_id: winId });
|
|
699
|
+
if (currentWinId === winId) {
|
|
700
|
+
stopWatch();
|
|
701
|
+
currentWinId = null;
|
|
702
|
+
showEmptyPane();
|
|
703
|
+
}
|
|
704
|
+
loadWindows();
|
|
705
|
+
};
|
|
706
|
+
|
|
707
|
+
// ── Bottom pane ─────────────────────────────────────────
|
|
708
|
+
function showEmptyPane() {
|
|
709
|
+
$("bottom-pane").innerHTML =
|
|
710
|
+
'<div class="bp-empty" id="bp-empty">← Select a window above to inspect it</div>';
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function buildBottomPane() {
|
|
714
|
+
$("bottom-pane").innerHTML = `
|
|
715
|
+
<div class="bp-capture">
|
|
716
|
+
<img id="bp-img" alt="Live capture">
|
|
717
|
+
</div>
|
|
718
|
+
<div class="bp-panel">
|
|
719
|
+
<div class="panel-section">
|
|
720
|
+
<h3>Info</h3>
|
|
721
|
+
<div class="info-row"><strong id="bp-win-title"></strong></div>
|
|
722
|
+
<div class="info-row" id="bp-win-url" style="font-size:0.7rem"></div>
|
|
723
|
+
<div class="info-row" style="margin-top:5px;font-size:0.7rem;color:#64748b">
|
|
724
|
+
Display: <span id="bp-screen-size"></span>
|
|
725
|
+
</div>
|
|
726
|
+
</div>
|
|
727
|
+
<div class="panel-section">
|
|
728
|
+
<h3>Capture interval</h3>
|
|
729
|
+
<div class="interval-row">
|
|
730
|
+
<input type="range" id="bp-interval" min="200" max="5000" step="100" value="${captureInterval}">
|
|
731
|
+
<span class="interval-label" id="bp-interval-label">${(captureInterval / 1000).toFixed(1)} s</span>
|
|
732
|
+
</div>
|
|
733
|
+
</div>
|
|
734
|
+
<div class="panel-section">
|
|
735
|
+
<h3>JPEG quality</h3>
|
|
736
|
+
<div class="interval-row">
|
|
737
|
+
<input type="range" id="bp-quality" min="10" max="100" step="5" value="${jpegQuality}">
|
|
738
|
+
<span class="interval-label" id="bp-quality-label">${jpegQuality}</span>
|
|
739
|
+
</div>
|
|
740
|
+
</div>
|
|
741
|
+
<div class="panel-section">
|
|
742
|
+
<h3>Capture scale</h3>
|
|
743
|
+
<div class="interval-row">
|
|
744
|
+
<input type="range" id="bp-scale" min="0.1" max="1" step="0.1" value="${captureScale}">
|
|
745
|
+
<span class="interval-label" id="bp-scale-label">${Math.round(captureScale * 100)}%</span>
|
|
746
|
+
</div>
|
|
747
|
+
</div>
|
|
748
|
+
<div class="panel-section">
|
|
749
|
+
<h3>Bounds</h3>
|
|
750
|
+
<div class="bounds-grid">
|
|
751
|
+
<div class="field"><label>X</label><input type="number" id="bounds-x"></div>
|
|
752
|
+
<div class="field"><label>Y</label><input type="number" id="bounds-y"></div>
|
|
753
|
+
<div class="field"><label>Width</label><input type="number" id="bounds-w" min="100"></div>
|
|
754
|
+
<div class="field"><label>Height</label><input type="number" id="bounds-h" min="100"></div>
|
|
755
|
+
</div>
|
|
756
|
+
<button class="btn btn-success btn-block" id="apply-bounds-btn">Apply Bounds</button>
|
|
757
|
+
<div id="bounds-feedback" style="font-size:0.72rem;margin-top:6px;min-height:16px"></div>
|
|
758
|
+
</div>
|
|
759
|
+
<div class="panel-section">
|
|
760
|
+
<button class="btn btn-danger btn-block" id="bp-close-win-btn">Close Window</button>
|
|
761
|
+
</div>
|
|
762
|
+
</div>`;
|
|
763
|
+
|
|
764
|
+
// Wire controls
|
|
765
|
+
$("bp-interval").oninput = function () {
|
|
766
|
+
captureInterval = parseInt(this.value);
|
|
767
|
+
$("bp-interval-label").textContent = (captureInterval / 1000).toFixed(1) + " s";
|
|
768
|
+
saveInterval(captureInterval);
|
|
769
|
+
if (watchTimer && window._detailTick) {
|
|
770
|
+
clearInterval(watchTimer);
|
|
771
|
+
watchTimer = setInterval(window._detailTick, captureInterval);
|
|
772
|
+
}
|
|
773
|
+
};
|
|
774
|
+
|
|
775
|
+
$("bp-quality").oninput = function () {
|
|
776
|
+
jpegQuality = parseInt(this.value);
|
|
777
|
+
$("bp-quality-label").textContent = jpegQuality;
|
|
778
|
+
saveQuality(jpegQuality);
|
|
779
|
+
};
|
|
780
|
+
|
|
781
|
+
$("bp-scale").oninput = function () {
|
|
782
|
+
captureScale = parseFloat(this.value);
|
|
783
|
+
$("bp-scale-label").textContent = Math.round(captureScale * 100) + "%";
|
|
784
|
+
saveScale(captureScale);
|
|
785
|
+
};
|
|
786
|
+
|
|
787
|
+
$("apply-bounds-btn").onclick = async () => {
|
|
788
|
+
if (!currentWinId) return;
|
|
789
|
+
const btn = $("apply-bounds-btn");
|
|
790
|
+
const fb = $("bounds-feedback");
|
|
791
|
+
btn.disabled = true;
|
|
792
|
+
btn.textContent = "Applying…";
|
|
793
|
+
fb.style.color = "#94a3b8";
|
|
794
|
+
fb.textContent = "";
|
|
795
|
+
try {
|
|
796
|
+
const res = await rpc("set_window_bounds", {
|
|
797
|
+
win_id: currentWinId,
|
|
798
|
+
x: parseInt($("bounds-x").value),
|
|
799
|
+
y: parseInt($("bounds-y").value),
|
|
800
|
+
width: parseInt($("bounds-w").value),
|
|
801
|
+
height: parseInt($("bounds-h").value),
|
|
802
|
+
});
|
|
803
|
+
const data = await res.json();
|
|
804
|
+
const isErr = data.result?.isError;
|
|
805
|
+
fb.style.color = isErr ? "#f87171" : "#4ade80";
|
|
806
|
+
fb.textContent = isErr ? data.result?.content?.[0]?.text || "Error" : "✓ Applied";
|
|
807
|
+
await loadDetailInfo(currentWinId);
|
|
808
|
+
if (window._detailTick) window._detailTick();
|
|
809
|
+
} catch (e) {
|
|
810
|
+
fb.style.color = "#f87171";
|
|
811
|
+
fb.textContent = e.message;
|
|
812
|
+
} finally {
|
|
813
|
+
btn.disabled = false;
|
|
814
|
+
btn.textContent = "Apply Bounds";
|
|
815
|
+
}
|
|
816
|
+
};
|
|
817
|
+
|
|
818
|
+
$("bp-close-win-btn").onclick = async () => {
|
|
819
|
+
if (!currentWinId) return;
|
|
820
|
+
if (!confirm(`Close window #${currentWinId}?`)) return;
|
|
821
|
+
await rpc("close_window", { win_id: currentWinId });
|
|
822
|
+
stopWatch();
|
|
823
|
+
currentWinId = null;
|
|
824
|
+
showEmptyPane();
|
|
825
|
+
loadWindows();
|
|
826
|
+
};
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// ── Select a window (click on card) ────────────────────
|
|
830
|
+
window.selectWindow = async function (winId) {
|
|
831
|
+
if (currentWinId === winId) return; // already selected
|
|
832
|
+
|
|
833
|
+
stopWatch();
|
|
834
|
+
|
|
835
|
+
// Update select dropdown
|
|
836
|
+
const select = $("win-select");
|
|
837
|
+
if (select) select.value = winId;
|
|
838
|
+
|
|
839
|
+
currentWinId = winId;
|
|
840
|
+
saveSelectedWin(winId);
|
|
841
|
+
|
|
842
|
+
// Focus the Electron window so it becomes active
|
|
843
|
+
rpc("control_electron_BrowserWindow", { win_id: winId, code: "win.focus()" }).catch(
|
|
844
|
+
() => {}
|
|
845
|
+
);
|
|
846
|
+
|
|
847
|
+
// Build pane if first time
|
|
848
|
+
if (!$("bp-img")) buildBottomPane();
|
|
849
|
+
|
|
850
|
+
await loadDetailInfo(winId);
|
|
851
|
+
|
|
852
|
+
// Start capture loop
|
|
853
|
+
let prevUrl = null;
|
|
854
|
+
const img = $("bp-img");
|
|
855
|
+
|
|
856
|
+
async function tick() {
|
|
857
|
+
if (!img) return;
|
|
858
|
+
try {
|
|
859
|
+
const res = await fetch(snapUrl(winId));
|
|
860
|
+
if (!res.ok) return;
|
|
861
|
+
const blob = await res.blob();
|
|
862
|
+
const url = URL.createObjectURL(blob);
|
|
863
|
+
img.src = url;
|
|
864
|
+
if (prevUrl) URL.revokeObjectURL(prevUrl);
|
|
865
|
+
prevUrl = url;
|
|
866
|
+
} catch (_) {}
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
window._detailTick = tick;
|
|
870
|
+
tick();
|
|
871
|
+
if (loopEnabled) {
|
|
872
|
+
watchTimer = setInterval(tick, captureInterval);
|
|
873
|
+
}
|
|
874
|
+
};
|
|
875
|
+
|
|
876
|
+
async function loadDetailInfo(winId) {
|
|
877
|
+
const wins = await rpcJson("get_windows").catch(() => []);
|
|
878
|
+
const w = (Array.isArray(wins) ? wins : []).find((w) => w.id === winId);
|
|
879
|
+
if (!w) return;
|
|
880
|
+
if ($("bp-win-title")) $("bp-win-title").textContent = w.title || "(no title)";
|
|
881
|
+
if ($("bp-win-url")) $("bp-win-url").textContent = w.url || "";
|
|
882
|
+
if ($("bp-screen-size"))
|
|
883
|
+
$("bp-screen-size").textContent =
|
|
884
|
+
`${screen.width}×${screen.height} (×${window.devicePixelRatio})`;
|
|
885
|
+
if ($("bounds-x")) {
|
|
886
|
+
$("bounds-x").value = w.bounds.x;
|
|
887
|
+
$("bounds-y").value = w.bounds.y;
|
|
888
|
+
$("bounds-w").value = w.bounds.width;
|
|
889
|
+
$("bounds-h").value = w.bounds.height;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
// ── iframe watch mode ───────────────────────────────────
|
|
894
|
+
function iframeWatch(winId) {
|
|
895
|
+
const img = $("watch-img");
|
|
896
|
+
let prevUrl = null;
|
|
897
|
+
async function tick() {
|
|
898
|
+
try {
|
|
899
|
+
const res = await fetch(snapUrl(winId));
|
|
900
|
+
if (!res.ok) return;
|
|
901
|
+
const blob = await res.blob();
|
|
902
|
+
const url = URL.createObjectURL(blob);
|
|
903
|
+
img.src = url;
|
|
904
|
+
if (prevUrl) URL.revokeObjectURL(prevUrl);
|
|
905
|
+
prevUrl = url;
|
|
906
|
+
} catch (_) {}
|
|
907
|
+
}
|
|
908
|
+
tick();
|
|
909
|
+
setInterval(tick, 1000);
|
|
910
|
+
}
|
|
911
|
+
})();
|
|
912
|
+
</script>
|
|
913
|
+
</body>
|
|
914
|
+
</html>
|