ai-remote 0.4.13 → 0.4.15

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.
@@ -0,0 +1,483 @@
1
+ import {
2
+ ROOT,
3
+ isTitlebarDragPoint,
4
+ logicalDisplaySize
5
+ } from "./cli-chunk-K2DYJXC5.mjs";
6
+
7
+ // src/cli/window.ts
8
+ import { spawn } from "node:child_process";
9
+ import { createRequire } from "node:module";
10
+
11
+ // src/cli/bundle.ts
12
+ import { existsSync, mkdirSync, readdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
13
+ import { join } from "node:path";
14
+
15
+ // src/cli/png.ts
16
+ import { deflateSync } from "node:zlib";
17
+ var CRC_TABLE = (() => {
18
+ const table = new Uint32Array(256);
19
+ for (let n = 0; n < 256; n++) {
20
+ let c = n;
21
+ for (let k = 0; k < 8; k++) c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
22
+ table[n] = c >>> 0;
23
+ }
24
+ return table;
25
+ })();
26
+ function crc32(bytes) {
27
+ let c = 4294967295;
28
+ for (let i = 0; i < bytes.length; i++) c = CRC_TABLE[(c ^ bytes[i]) & 255] ^ c >>> 8;
29
+ return (c ^ 4294967295) >>> 0;
30
+ }
31
+ function chunk(type, body) {
32
+ const out = new Uint8Array(12 + body.length);
33
+ const view = new DataView(out.buffer);
34
+ view.setUint32(0, body.length);
35
+ for (let i = 0; i < 4; i++) out[4 + i] = type.charCodeAt(i);
36
+ out.set(body, 8);
37
+ view.setUint32(8 + body.length, crc32(out.subarray(4, 8 + body.length)));
38
+ return out;
39
+ }
40
+ function encodePng(rgba, width, height) {
41
+ const stride = width * 4;
42
+ const raw = new Uint8Array((stride + 1) * height);
43
+ for (let y = 0; y < height; y++) {
44
+ raw[y * (stride + 1)] = 0;
45
+ raw.set(rgba.subarray(y * stride, y * stride + stride), y * (stride + 1) + 1);
46
+ }
47
+ const ihdr = new Uint8Array(13);
48
+ const view = new DataView(ihdr.buffer);
49
+ view.setUint32(0, width);
50
+ view.setUint32(4, height);
51
+ ihdr[8] = 8;
52
+ ihdr[9] = 6;
53
+ ihdr[10] = 0;
54
+ ihdr[11] = 0;
55
+ ihdr[12] = 0;
56
+ const parts = [
57
+ new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]),
58
+ chunk("IHDR", ihdr),
59
+ chunk("IDAT", new Uint8Array(deflateSync(raw, { level: 6 }))),
60
+ chunk("IEND", new Uint8Array(0))
61
+ ];
62
+ const total = parts.reduce((sum, part) => sum + part.length, 0);
63
+ const png = new Uint8Array(total);
64
+ let offset = 0;
65
+ for (const part of parts) {
66
+ png.set(part, offset);
67
+ offset += part.length;
68
+ }
69
+ return png;
70
+ }
71
+ function downscale(rgba, width, height, maxEdge) {
72
+ const scale = Math.min(1, maxEdge / Math.max(width, height));
73
+ if (scale >= 1) return { rgba, width, height };
74
+ const outWidth = Math.max(1, Math.round(width * scale));
75
+ const outHeight = Math.max(1, Math.round(height * scale));
76
+ const out = new Uint8Array(outWidth * outHeight * 4);
77
+ for (let y = 0; y < outHeight; y++) {
78
+ const sourceY = Math.min(height - 1, Math.floor(y / scale));
79
+ for (let x = 0; x < outWidth; x++) {
80
+ const sourceX = Math.min(width - 1, Math.floor(x / scale));
81
+ const from = (sourceY * width + sourceX) * 4;
82
+ const to = (y * outWidth + x) * 4;
83
+ out[to] = rgba[from];
84
+ out[to + 1] = rgba[from + 1];
85
+ out[to + 2] = rgba[from + 2];
86
+ out[to + 3] = rgba[from + 3];
87
+ }
88
+ }
89
+ return { rgba: out, width: outWidth, height: outHeight };
90
+ }
91
+
92
+ // src/cli/icon.ts
93
+ var ICON_VERSION = 2;
94
+ var SCREEN_TOP = [91, 149, 243];
95
+ var SCREEN_BOTTOM = [47, 103, 214];
96
+ var CURSOR = [255, 255, 255];
97
+ var ARROW = [
98
+ [0, 0],
99
+ [0, 12.7],
100
+ [3.1, 9.9],
101
+ [5.1, 14.4],
102
+ [7.3, 13.4],
103
+ [5.3, 9],
104
+ [9.2, 8.6]
105
+ ];
106
+ var ARROW_H = 14.4;
107
+ function inSquircle(x, y, cx, cy, radius) {
108
+ const dx = Math.abs(x - cx) / radius;
109
+ const dy = Math.abs(y - cy) / radius;
110
+ return dx ** 5 + dy ** 5 <= 1;
111
+ }
112
+ function centroid(points) {
113
+ let twiceArea = 0;
114
+ let x = 0;
115
+ let y = 0;
116
+ for (let i = 0, j = points.length - 1; i < points.length; j = i++) {
117
+ const [xi, yi] = points[i];
118
+ const [xj, yj] = points[j];
119
+ const cross = xj * yi - xi * yj;
120
+ twiceArea += cross;
121
+ x += (xi + xj) * cross;
122
+ y += (yi + yj) * cross;
123
+ }
124
+ return [x / (3 * twiceArea), y / (3 * twiceArea)];
125
+ }
126
+ function inPolygon(x, y, points) {
127
+ let inside = false;
128
+ for (let i = 0, j = points.length - 1; i < points.length; j = i++) {
129
+ const [xi, yi] = points[i];
130
+ const [xj, yj] = points[j];
131
+ if (yi > y !== yj > y && x < (xj - xi) * (y - yi) / (yj - yi) + xi) inside = !inside;
132
+ }
133
+ return inside;
134
+ }
135
+ function iconRgba(size, { margin = 0.08 } = {}) {
136
+ const rgba = new Uint8Array(size * size * 4);
137
+ const side = size * (1 - margin * 2);
138
+ const centre = size / 2;
139
+ const radius = side / 2;
140
+ const arrowScale = side * 0.56 / ARROW_H;
141
+ const scaled = ARROW.map(([x, y]) => [x * arrowScale, y * arrowScale]);
142
+ const [gx, gy] = centroid(scaled);
143
+ const arrow = scaled.map(([x, y]) => [x + centre - gx, y + centre - gy]);
144
+ const SAMPLES = 4;
145
+ const step = 1 / SAMPLES;
146
+ const per = 1 / (SAMPLES * SAMPLES);
147
+ for (let py = 0; py < size; py++) {
148
+ for (let px = 0; px < size; px++) {
149
+ let screen = 0;
150
+ let cursor = 0;
151
+ for (let sy = 0; sy < SAMPLES; sy++) {
152
+ for (let sx = 0; sx < SAMPLES; sx++) {
153
+ const x = px + (sx + 0.5) * step;
154
+ const y = py + (sy + 0.5) * step;
155
+ if (!inSquircle(x, y, centre, centre, radius)) continue;
156
+ screen += per;
157
+ if (inPolygon(x, y, arrow)) cursor += per;
158
+ }
159
+ }
160
+ if (screen === 0) continue;
161
+ const t = (py / size - margin) / (1 - margin * 2);
162
+ const lit = Math.min(1, Math.max(0, t));
163
+ const cursorShare = cursor / screen;
164
+ const at = (py * size + px) * 4;
165
+ for (let channel = 0; channel < 3; channel++) {
166
+ const screenTone = SCREEN_TOP[channel] + (SCREEN_BOTTOM[channel] - SCREEN_TOP[channel]) * lit;
167
+ rgba[at + channel] = Math.round(screenTone + (CURSOR[channel] - screenTone) * cursorShare);
168
+ }
169
+ rgba[at + 3] = Math.round(screen * 255);
170
+ }
171
+ }
172
+ return rgba;
173
+ }
174
+ var iconPng = (size, options) => encodePng(iconRgba(size, options), size, size);
175
+ function iconIcns() {
176
+ const entries = [
177
+ ["ic11", 32],
178
+ ["ic12", 64],
179
+ ["ic07", 128],
180
+ ["ic13", 256],
181
+ ["ic08", 256],
182
+ ["ic14", 512],
183
+ ["ic09", 512]
184
+ ];
185
+ const rendered = /* @__PURE__ */ new Map();
186
+ const chunks = entries.map(([type, edge]) => {
187
+ if (!rendered.has(edge)) rendered.set(edge, iconPng(edge));
188
+ const png = rendered.get(edge);
189
+ const chunk2 = new Uint8Array(8 + png.length);
190
+ for (let i = 0; i < 4; i++) chunk2[i] = type.charCodeAt(i);
191
+ new DataView(chunk2.buffer).setUint32(4, chunk2.length);
192
+ chunk2.set(png, 8);
193
+ return chunk2;
194
+ });
195
+ const total = 8 + chunks.reduce((sum, chunk2) => sum + chunk2.length, 0);
196
+ const out = new Uint8Array(total);
197
+ out.set([105, 99, 110, 115]);
198
+ new DataView(out.buffer).setUint32(4, total);
199
+ let at = 8;
200
+ for (const chunk2 of chunks) {
201
+ out.set(chunk2, at);
202
+ at += chunk2.length;
203
+ }
204
+ return out;
205
+ }
206
+ var ICON_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><defs><linearGradient id="s" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#5b95f3"/><stop offset="1" stop-color="#336cdb"/></linearGradient></defs><rect x="1.5" y="1.5" width="29" height="29" rx="6.5" fill="url(#s)"/><path d="M12.28 7.46 12.28 21.78 15.77 18.63 18.03 23.70 20.51 22.57 18.26 17.61 22.65 17.16 Z" fill="#ffffff"/></svg>`;
207
+
208
+ // src/cli/bundle.ts
209
+ var APP_NAME = "ai-remote";
210
+ var windowsDir = () => join(ROOT, "window");
211
+ var bundleDir = () => join(windowsDir(), `v${ICON_VERSION}`, `${APP_NAME}.app`);
212
+ var PLIST = `<?xml version="1.0" encoding="UTF-8"?>
213
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
214
+ <plist version="1.0">
215
+ <dict>
216
+ <key>CFBundleName</key><string>${APP_NAME}</string>
217
+ <key>CFBundleDisplayName</key><string>${APP_NAME}</string>
218
+ <key>CFBundleIdentifier</key><string>dev.ai-remote.window</string>
219
+ <key>CFBundleExecutable</key><string>${APP_NAME}</string>
220
+ <key>CFBundleIconFile</key><string>${APP_NAME}</string>
221
+ <key>CFBundlePackageType</key><string>APPL</string>
222
+ <key>CFBundleInfoDictionaryVersion</key><string>6.0</string>
223
+ <key>CFBundleShortVersionString</key><string>1.0</string>
224
+ <key>CFBundleVersion</key><string>${ICON_VERSION}</string>
225
+ <key>NSHighResolutionCapable</key><true/>
226
+ <key>LSApplicationCategoryType</key><string>public.app-category.utilities</string>
227
+ </dict>
228
+ </plist>
229
+ `;
230
+ function windowExecutable() {
231
+ if (process.platform !== "darwin") return null;
232
+ const app = bundleDir();
233
+ const executable = join(app, "Contents", "MacOS", APP_NAME);
234
+ try {
235
+ if (!existsSync(executable)) build(app, executable);
236
+ return executable;
237
+ } catch {
238
+ return null;
239
+ }
240
+ }
241
+ function build(app, executable) {
242
+ const contents = join(app, "Contents");
243
+ rmSync(app, { recursive: true, force: true });
244
+ mkdirSync(join(contents, "MacOS"), { recursive: true });
245
+ mkdirSync(join(contents, "Resources"), { recursive: true });
246
+ writeFileSync(join(contents, "Info.plist"), PLIST);
247
+ writeFileSync(join(contents, "PkgInfo"), "APPL????");
248
+ writeFileSync(join(contents, "Resources", `${APP_NAME}.icns`), iconIcns());
249
+ symlinkSync(process.execPath, executable);
250
+ for (const old of readdirSync(windowsDir())) {
251
+ if (old !== `v${ICON_VERSION}`) rmSync(join(windowsDir(), old), { recursive: true, force: true });
252
+ }
253
+ }
254
+
255
+ // src/cli/window.ts
256
+ function nativeWindowAvailable() {
257
+ try {
258
+ createRequire(import.meta.url).resolve("@webviewjs/webview");
259
+ return true;
260
+ } catch {
261
+ return false;
262
+ }
263
+ }
264
+ function detectCurrentDisplaySize(timeoutMs = 3e3) {
265
+ if (!nativeWindowAvailable() || !process.argv[1]) return Promise.resolve(null);
266
+ return new Promise((resolve) => {
267
+ const child = spawn(process.execPath, [process.argv[1], "__display-size"], {
268
+ stdio: ["ignore", "pipe", "ignore"]
269
+ });
270
+ let output = "";
271
+ let settled = false;
272
+ const finish = (size) => {
273
+ if (settled) return;
274
+ settled = true;
275
+ clearTimeout(timer);
276
+ resolve(size);
277
+ };
278
+ const timer = setTimeout(() => {
279
+ try {
280
+ child.kill();
281
+ } catch {
282
+ }
283
+ finish(null);
284
+ }, timeoutMs);
285
+ child.stdout?.setEncoding("utf8");
286
+ child.stdout?.on("data", (chunk2) => {
287
+ if (output.length < 4096) output += chunk2;
288
+ });
289
+ child.once("error", () => finish(null));
290
+ child.once("close", (code) => {
291
+ if (code !== 0) {
292
+ finish(null);
293
+ return;
294
+ }
295
+ const match = /^\s*\{"width":(\d+),"height":(\d+)\}\s*$/.exec(output);
296
+ const width = Number(match?.[1]);
297
+ const height = Number(match?.[2]);
298
+ finish(width > 0 && height > 0 ? { width, height } : null);
299
+ });
300
+ });
301
+ }
302
+ async function readCurrentDisplaySize() {
303
+ if (!nativeWindowAvailable()) return null;
304
+ const { Application } = await import("@webviewjs/webview");
305
+ const application = new Application();
306
+ const probe = application.createBrowserWindow({
307
+ title: "ai-remote display probe",
308
+ width: 1,
309
+ height: 1,
310
+ logical: true,
311
+ visible: false,
312
+ focused: false
313
+ });
314
+ try {
315
+ application.pumpEvents();
316
+ probe.center();
317
+ application.pumpEvents();
318
+ const monitor = probe.getCurrentMonitor() ?? probe.getPrimaryMonitor();
319
+ return monitor ? logicalDisplaySize(monitor.size, monitor.scaleFactor) : null;
320
+ } finally {
321
+ try {
322
+ probe.dispose();
323
+ } catch {
324
+ }
325
+ try {
326
+ application.exit();
327
+ } catch {
328
+ }
329
+ }
330
+ }
331
+ function openWindow(options) {
332
+ if (!options.tab && nativeWindowAvailable()) {
333
+ const args2 = [
334
+ process.argv[1],
335
+ "__window",
336
+ options.url,
337
+ "--title",
338
+ options.title,
339
+ "--width",
340
+ String(options.width),
341
+ "--height",
342
+ String(options.height)
343
+ ];
344
+ if (options.fullscreen) args2.push("--fullscreen");
345
+ const executable = windowExecutable() ?? process.execPath;
346
+ try {
347
+ const child = spawn(executable, args2, { stdio: "ignore", detached: true });
348
+ child.unref();
349
+ return { kind: "window", pid: child.pid, detail: "native window" };
350
+ } catch (error) {
351
+ return { kind: "none", detail: error instanceof Error ? error.message : String(error) };
352
+ }
353
+ }
354
+ const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
355
+ const args = process.platform === "win32" ? ["/c", "start", "", options.url] : [options.url];
356
+ try {
357
+ spawn(command, args, { stdio: "ignore", detached: true }).unref();
358
+ return {
359
+ kind: "tab",
360
+ detail: options.tab ? "asked for a browser tab" : "the native window layer is not installed"
361
+ };
362
+ } catch (error) {
363
+ return { kind: "none", detail: error instanceof Error ? error.message : String(error) };
364
+ }
365
+ }
366
+ async function runWindow(argv) {
367
+ const positional = [];
368
+ const flags = {};
369
+ for (let i = 0; i < argv.length; i++) {
370
+ const token = argv[i];
371
+ if (!token.startsWith("--")) {
372
+ positional.push(token);
373
+ continue;
374
+ }
375
+ const name = token.slice(2);
376
+ if (name === "fullscreen") flags[name] = true;
377
+ else flags[name] = argv[++i] ?? "";
378
+ }
379
+ const url = positional[0];
380
+ if (!url) throw new Error("__window needs a URL");
381
+ const { Application, FullscreenType } = await import("@webviewjs/webview");
382
+ const application = new Application();
383
+ const window = application.createBrowserWindow({
384
+ title: String(flags.title || "ai-remote"),
385
+ width: Number(flags.width) || 1280,
386
+ height: Number(flags.height) || 800,
387
+ logical: true,
388
+ resizable: true,
389
+ maximizable: true,
390
+ minimizable: true,
391
+ focused: true,
392
+ // One bar, not two. The page draws the title and the controls, and the
393
+ // system titlebar becomes a transparent strip that the content runs
394
+ // underneath -- so the traffic lights sit on the page's own toolbar.
395
+ //
396
+ // Transparent rather than hidden on purpose: the drag below moves the
397
+ // window from the page, but a window with no titlebar at all also loses the
398
+ // traffic lights the page's toolbar is laid out around.
399
+ macosTitlebarTransparent: true,
400
+ macosFullsizeContentView: true,
401
+ macosTitleHidden: true,
402
+ // Keep AppKit background dragging disabled. The page-to-native bridge below
403
+ // moves the window only after a gesture begins in the HTML title bar;
404
+ // enabling this would also steal canvas and terminal pointer drags.
405
+ macosMovableByWindowBackground: false
406
+ });
407
+ try {
408
+ const edge = 64;
409
+ window.setWindowIcon(Buffer.from(iconRgba(edge, { margin: 0 })), edge, edge);
410
+ } catch {
411
+ }
412
+ const webview = window.createWebview({ url, enableDevtools: false });
413
+ const debugDrag = process.env.AI_REMOTE_DEBUG_DRAG === "1";
414
+ let pageDrag = null;
415
+ let dragPointer = null;
416
+ webview.onIpcMessage((message) => {
417
+ let note;
418
+ try {
419
+ note = JSON.parse(message.body.toString("utf8"));
420
+ } catch {
421
+ return;
422
+ }
423
+ if (debugDrag) console.error("[page-ipc]", note);
424
+ if (note.t === "drag-end") {
425
+ pageDrag = null;
426
+ return;
427
+ }
428
+ if (typeof note.x !== "number" || typeof note.y !== "number") return;
429
+ if (note.t === "drag-start") {
430
+ if (window.fullscreen !== null) return;
431
+ const at = window.getPosition(true);
432
+ pageDrag = { pointer: { x: note.x, y: note.y }, origin: { x: at.x, y: at.y } };
433
+ dragPointer = null;
434
+ return;
435
+ }
436
+ if (note.t !== "drag-move" || !pageDrag || window.fullscreen !== null) return;
437
+ window.setPosition(
438
+ pageDrag.origin.x + (note.x - pageDrag.pointer.x),
439
+ pageDrag.origin.y + (note.y - pageDrag.pointer.y),
440
+ true
441
+ );
442
+ });
443
+ window.on("mouse-down", (event) => {
444
+ if (debugDrag) console.error("[native-mouse-down]", event);
445
+ if (process.platform !== "darwin" || event.button !== 0 || window.fullscreen !== null) return;
446
+ if (pageDrag) return;
447
+ const point = { x: event.x, y: event.y };
448
+ if (!isTitlebarDragPoint(point, window.getInnerSize(false).width, window.scaleFactor())) return;
449
+ dragPointer = point;
450
+ });
451
+ window.on("mouse-move", (event) => {
452
+ if (!dragPointer || pageDrag || window.fullscreen !== null) return;
453
+ if (debugDrag) console.error("[native-mouse-move]", event);
454
+ const dx = event.x - dragPointer.x;
455
+ const dy = event.y - dragPointer.y;
456
+ if (dx === 0 && dy === 0) return;
457
+ const position = window.getPosition(false);
458
+ window.setPosition(position.x + dx, position.y + dy, false);
459
+ });
460
+ window.on("mouse-up", (event) => {
461
+ if (event.button === 0) dragPointer = null;
462
+ });
463
+ if (flags.fullscreen) {
464
+ setTimeout(() => {
465
+ try {
466
+ window.setFullscreen(FullscreenType.Borderless);
467
+ } catch {
468
+ }
469
+ }, 400);
470
+ }
471
+ await application.whenReady({ interval: 33 });
472
+ }
473
+
474
+ export {
475
+ encodePng,
476
+ downscale,
477
+ ICON_SVG,
478
+ nativeWindowAvailable,
479
+ detectCurrentDisplaySize,
480
+ readCurrentDisplaySize,
481
+ openWindow,
482
+ runWindow
483
+ };