sliccy 1.4.0 → 1.4.1

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.
@@ -21,6 +21,7 @@ export declare class ElectronOverlayInjector {
21
21
  private readonly cdpPort;
22
22
  private readonly bootstrapScript;
23
23
  private readonly connections;
24
+ private readonly cspBypassedTargets;
24
25
  private syncTimer;
25
26
  private syncing;
26
27
  private constructor();
@@ -33,6 +34,11 @@ export declare class ElectronOverlayInjector {
33
34
  start(): Promise<void>;
34
35
  stop(): void;
35
36
  private syncTargets;
37
+ /**
38
+ * Check if the overlay iframe loaded successfully by evaluating a probe script.
39
+ * Returns true if the iframe element exists and has started loading content.
40
+ */
41
+ private probeOverlayIframeLoaded;
36
42
  private connectToTarget;
37
43
  }
38
44
  export {};
@@ -1,9 +1,11 @@
1
1
  import { execFile as nodeExecFile, spawn } from 'child_process';
2
2
  import { existsSync } from 'fs';
3
3
  import { readFile } from 'fs/promises';
4
+ import * as http from 'http';
5
+ import * as https from 'https';
4
6
  import { promisify } from 'util';
5
7
  import { WebSocket } from 'ws';
6
- import { buildElectronAppLaunchSpec, buildElectronOverlayAppUrl, buildElectronOverlayBootstrapScript, buildElectronOverlayEntryUrl, getElectronOverlayEntryDistPath, getElectronServeOrigin, shouldInjectElectronOverlayTarget, } from './electron-runtime.js';
8
+ import { buildElectronAppLaunchSpec, buildElectronOverlayAppUrl, buildElectronOverlayBootstrapScript, buildElectronOverlayEntryUrl, getElectronOverlayEntryDistPath, getElectronServeOrigin, selectBestOverlayTargets, } from './electron-runtime.js';
7
9
  const execFile = promisify(nodeExecFile);
8
10
  const ELECTRON_OVERLAY_SYNC_INTERVAL_MS = 1500;
9
11
  function commandLineExecutableMatchesPattern(commandLine, pattern) {
@@ -183,6 +185,7 @@ export class ElectronOverlayInjector {
183
185
  cdpPort;
184
186
  bootstrapScript;
185
187
  connections = new Map();
188
+ cspBypassedTargets = new Set();
186
189
  syncTimer = null;
187
190
  syncing = false;
188
191
  constructor(cdpPort, bootstrapScript) {
@@ -228,7 +231,14 @@ export class ElectronOverlayInjector {
228
231
  throw new Error(`CDP target listing failed with ${response.status} ${response.statusText}`);
229
232
  }
230
233
  const targets = (await response.json());
231
- const injectableTargets = targets.filter(shouldInjectElectronOverlayTarget);
234
+ const pageCount = targets.filter(t => t.type === 'page').length;
235
+ const injectableTargets = selectBestOverlayTargets(targets);
236
+ if (injectableTargets.length < pageCount) {
237
+ console.log(`[electron-float] Selected ${injectableTargets.length}/${pageCount} page targets for overlay injection`);
238
+ for (const t of injectableTargets) {
239
+ console.log(`[electron-float] → ${t.title || '(untitled)'} @ ${t.url.substring(0, 80)}`);
240
+ }
241
+ }
232
242
  const liveConnectionIds = new Set(injectableTargets.map((target) => target.webSocketDebuggerUrl));
233
243
  for (const [targetId, connection] of this.connections.entries()) {
234
244
  if (liveConnectionIds.has(targetId))
@@ -256,20 +266,232 @@ export class ElectronOverlayInjector {
256
266
  this.syncing = false;
257
267
  }
258
268
  }
269
+ /**
270
+ * Check if the overlay iframe loaded successfully by evaluating a probe script.
271
+ * Returns true if the iframe element exists and has started loading content.
272
+ */
273
+ async probeOverlayIframeLoaded(ws, send) {
274
+ return new Promise((resolve) => {
275
+ const probeId = send('Runtime.evaluate', {
276
+ expression: `(function() {
277
+ var host = document.getElementById('slicc-electron-overlay-root');
278
+ if (!host || !host.shadowRoot) return 'no-host';
279
+ var sidebar = host.shadowRoot.querySelector('slicc-electron-sidebar');
280
+ if (!sidebar || !sidebar.shadowRoot) return 'no-sidebar';
281
+ var iframe = sidebar.shadowRoot.querySelector('iframe');
282
+ if (!iframe) return 'no-iframe';
283
+ if (!iframe.src) return 'no-src';
284
+ return 'ok';
285
+ })()`,
286
+ awaitPromise: false,
287
+ returnByValue: true,
288
+ });
289
+ const timeout = setTimeout(() => {
290
+ cleanup();
291
+ resolve(false);
292
+ }, 3000);
293
+ const onMessage = (data) => {
294
+ try {
295
+ const msg = JSON.parse(data.toString());
296
+ if (msg.id === probeId) {
297
+ cleanup();
298
+ const value = msg.result?.result?.value;
299
+ resolve(value === 'ok');
300
+ }
301
+ }
302
+ catch { /* ignore */ }
303
+ };
304
+ const cleanup = () => {
305
+ clearTimeout(timeout);
306
+ ws.off('message', onMessage);
307
+ };
308
+ ws.on('message', onMessage);
309
+ });
310
+ }
259
311
  connectToTarget(target) {
260
312
  const targetId = target.webSocketDebuggerUrl;
261
313
  const ws = new WebSocket(targetId);
262
314
  this.connections.set(targetId, ws);
263
315
  let messageId = 1;
264
316
  const send = (method, params) => {
265
- ws.send(JSON.stringify({ id: messageId++, method, params }));
317
+ const id = messageId++;
318
+ ws.send(JSON.stringify({ id, method, params }));
319
+ return id;
266
320
  };
321
+ const cspBypassedTargets = this.cspBypassedTargets;
322
+ const bootstrapScript = this.bootstrapScript;
323
+ let pendingReload = false;
324
+ let pendingCspEscalation = false;
325
+ let fetchProxyActive = false;
267
326
  ws.on('open', () => {
268
- send('Page.enable');
327
+ const isWebContent = target.url.startsWith('https://');
328
+ const alreadyBypassed = cspBypassedTargets.has(target.url);
329
+ console.log(`[electron-float] Connected to target, web=${isWebContent}, bypassed=${alreadyBypassed}, url=${target.url}`);
269
330
  send('Runtime.enable');
270
- send('Page.addScriptToEvaluateOnNewDocument', { source: this.bootstrapScript });
271
- send('Runtime.evaluate', { expression: this.bootstrapScript, awaitPromise: false });
272
- console.log(`[electron-float] Overlay injector attached to ${target.url}`);
331
+ send('Page.enable');
332
+ // Set CSP bypass affects future resource loads on the current page
333
+ send('Page.setBypassCSP', { enabled: true });
334
+ if (alreadyBypassed) {
335
+ // Already reloaded with CSP bypass previously — just inject
336
+ console.log(`[electron-float] Injecting overlay (CSP already bypassed)...`);
337
+ send('Runtime.evaluate', { expression: bootstrapScript, awaitPromise: false });
338
+ return;
339
+ }
340
+ // First connection to this target URL: inject overlay immediately, then
341
+ // check if the iframe loaded. If CSP blocked it, fall back to reload+proxy.
342
+ console.log(`[electron-float] Injecting overlay (first attempt)...`);
343
+ send('Runtime.evaluate', { expression: bootstrapScript, awaitPromise: false });
344
+ if (!isWebContent) {
345
+ // Local content (file://, app protocol) — CSP is not an issue
346
+ return;
347
+ }
348
+ // After a short delay, probe whether the overlay iframe loaded.
349
+ // If CSP blocked it, reload the page so Page.setBypassCSP takes effect.
350
+ // If that still doesn't work, escalate to the Fetch proxy.
351
+ setTimeout(async () => {
352
+ if (ws.readyState !== WebSocket.OPEN)
353
+ return;
354
+ const loaded = await this.probeOverlayIframeLoaded(ws, send);
355
+ if (loaded) {
356
+ console.log(`[electron-float] Overlay iframe loaded successfully — no CSP reload needed`);
357
+ cspBypassedTargets.add(target.url);
358
+ return;
359
+ }
360
+ // Phase 2: Page.setBypassCSP was already set — a simple reload should
361
+ // make the browser ignore CSP headers on the fresh navigation.
362
+ console.log(`[electron-float] Overlay iframe blocked by CSP, reloading with bypass: ${target.url}`);
363
+ cspBypassedTargets.add(target.url);
364
+ pendingReload = true;
365
+ pendingCspEscalation = true;
366
+ send('Page.reload', { ignoreCache: true });
367
+ }, 1500);
368
+ });
369
+ // Handle CDP events: lifecycle events and Fetch interception
370
+ ws.on('message', (data) => {
371
+ try {
372
+ const msg = JSON.parse(data.toString());
373
+ // Inject overlay after page load completes (after CSP-bypass reload)
374
+ if (msg.method === 'Page.loadEventFired' && pendingReload) {
375
+ pendingReload = false;
376
+ console.log(`[electron-float] Page loaded after CSP reload, injecting overlay...`);
377
+ if (ws.readyState !== WebSocket.OPEN)
378
+ return;
379
+ send('Runtime.evaluate', { expression: bootstrapScript, awaitPromise: false });
380
+ // If this was a simple reload (no proxy), check if the iframe loads now.
381
+ // If it still doesn't, escalate to the Fetch proxy as a last resort.
382
+ if (pendingCspEscalation) {
383
+ pendingCspEscalation = false;
384
+ setTimeout(async () => {
385
+ if (ws.readyState !== WebSocket.OPEN)
386
+ return;
387
+ const loaded = await this.probeOverlayIframeLoaded(ws, send);
388
+ if (loaded) {
389
+ console.log(`[electron-float] Overlay iframe loaded after CSP reload — no proxy needed`);
390
+ return;
391
+ }
392
+ console.log(`[electron-float] CSP reload insufficient, escalating to Fetch proxy: ${target.url}`);
393
+ fetchProxyActive = true;
394
+ const urlOrigin = new URL(target.url).origin;
395
+ send('Fetch.enable', {
396
+ patterns: [{ urlPattern: `${urlOrigin}/*`, requestStage: 'Request' }],
397
+ });
398
+ pendingReload = true;
399
+ send('Page.reload', { ignoreCache: true });
400
+ }, 1500);
401
+ }
402
+ }
403
+ if (msg.method === 'Fetch.requestPaused' && fetchProxyActive) {
404
+ const requestId = msg.params?.requestId;
405
+ if (!requestId) {
406
+ console.warn('[electron-float] Fetch.requestPaused without requestId, skipping');
407
+ return;
408
+ }
409
+ const url = msg.params?.request?.url || '';
410
+ const method = msg.params?.request?.method || 'GET';
411
+ const requestHeaders = msg.params?.request?.headers || {};
412
+ const postData = msg.params?.request?.postData;
413
+ // Only proxy HTML document requests (Accept header contains text/html)
414
+ const acceptHeader = requestHeaders['Accept'] || requestHeaders['accept'] || '';
415
+ if (!acceptHeader.includes('text/html')) {
416
+ send('Fetch.continueRequest', { requestId });
417
+ return;
418
+ }
419
+ console.log(`[electron-float] Proxying request to strip CSP: ${url.substring(0, 60)}`);
420
+ // Make the request ourselves using Node.js http/https
421
+ const parsedUrl = new URL(url);
422
+ const transport = parsedUrl.protocol === 'https:' ? https : http;
423
+ const options = {
424
+ hostname: parsedUrl.hostname,
425
+ port: parsedUrl.port || (parsedUrl.protocol === 'https:' ? 443 : 80),
426
+ path: parsedUrl.pathname + parsedUrl.search,
427
+ method: method,
428
+ headers: requestHeaders,
429
+ };
430
+ const proxyReq = transport.request(options, (proxyRes) => {
431
+ const bodyChunks = [];
432
+ proxyRes.on('data', (chunk) => bodyChunks.push(chunk));
433
+ proxyRes.on('end', () => {
434
+ if (ws.readyState !== WebSocket.OPEN)
435
+ return;
436
+ const fullBody = Buffer.concat(bodyChunks);
437
+ // Build response headers, stripping CSP and hop-by-hop headers
438
+ // that are invalid in Fetch.fulfillRequest responses
439
+ const HOP_BY_HOP = new Set([
440
+ 'content-security-policy',
441
+ 'content-security-policy-report-only',
442
+ 'transfer-encoding',
443
+ 'connection',
444
+ 'keep-alive',
445
+ ]);
446
+ const responseHeaders = [];
447
+ let strippedCSP = false;
448
+ for (const [name, value] of Object.entries(proxyRes.headers)) {
449
+ const lower = name.toLowerCase();
450
+ if (lower.includes('content-security-policy')) {
451
+ strippedCSP = true;
452
+ continue;
453
+ }
454
+ if (HOP_BY_HOP.has(lower))
455
+ continue;
456
+ // Update content-length to match actual body size
457
+ if (lower === 'content-length') {
458
+ responseHeaders.push({ name, value: String(fullBody.length) });
459
+ continue;
460
+ }
461
+ if (Array.isArray(value)) {
462
+ value.forEach(v => responseHeaders.push({ name, value: v }));
463
+ }
464
+ else if (value) {
465
+ responseHeaders.push({ name, value });
466
+ }
467
+ }
468
+ if (strippedCSP) {
469
+ console.log(`[electron-float] Stripped CSP from: ${url.substring(0, 60)}`);
470
+ }
471
+ send('Fetch.fulfillRequest', {
472
+ requestId,
473
+ responseCode: proxyRes.statusCode || 200,
474
+ responseHeaders,
475
+ body: fullBody.toString('base64'),
476
+ });
477
+ });
478
+ });
479
+ proxyReq.on('error', (err) => {
480
+ console.error(`[electron-float] Proxy request failed for ${url.substring(0, 60)}:`, err.message);
481
+ if (ws.readyState === WebSocket.OPEN) {
482
+ send('Fetch.failRequest', { requestId, errorReason: 'Failed' });
483
+ }
484
+ });
485
+ // Forward request body if present (for POST/PUT requests)
486
+ if (postData) {
487
+ proxyReq.write(postData);
488
+ }
489
+ proxyReq.end();
490
+ }
491
+ }
492
+ catch {
493
+ // Ignore parse errors for non-JSON messages
494
+ }
273
495
  });
274
496
  ws.on('close', () => {
275
497
  if (this.connections.get(targetId) === ws) {
@@ -17,6 +17,7 @@ export interface ElectronAppLaunchSpec {
17
17
  }
18
18
  export interface ElectronInspectableTarget {
19
19
  type: string;
20
+ title?: string;
20
21
  url: string;
21
22
  webSocketDebuggerUrl?: string;
22
23
  }
@@ -26,6 +27,25 @@ export declare const DEFAULT_ELECTRON_CDP_PORT = 9223;
26
27
  export declare const DEFAULT_ELECTRON_TARGET_URL = "about:blank";
27
28
  export declare const DEFAULT_ELECTRON_OVERLAY_TAB = "chat";
28
29
  export declare const ELECTRON_OVERLAY_APP_PATH = "/electron";
30
+ export declare const PORT_HASH_RANGE = 40;
31
+ /**
32
+ * Simple string hash function that returns a value between 0 and max-1.
33
+ * Exported for testing.
34
+ */
35
+ export declare function hashString(str: string, max: number): number;
36
+ /**
37
+ * Get a port for an Electron app based on its path.
38
+ * Uses hash-based offset from base port, with fallback to next available port
39
+ * starting from the preferred port (to stay in the app's "slot" range).
40
+ */
41
+ export declare function getElectronAppPort(appPath: string, basePort: number): Promise<number>;
42
+ /**
43
+ * Get both CDP and serve ports for an Electron app.
44
+ */
45
+ export declare function getElectronAppPorts(appPath: string): Promise<{
46
+ cdpPort: number;
47
+ servePort: number;
48
+ }>;
29
49
  export declare function getElectronAppDisplayName(appPath: string): string;
30
50
  export declare function resolveElectronAppExecutablePath(appPath: string, platform?: NodeJS.Platform): string;
31
51
  export declare function buildElectronAppProcessMatchPatterns(appPath: string, platform?: NodeJS.Platform): string[];
@@ -56,3 +76,9 @@ export declare function buildElectronOverlayBootstrapScript(options: {
56
76
  activeTab?: string;
57
77
  }): string;
58
78
  export declare function shouldInjectElectronOverlayTarget(target: ElectronInspectableTarget): boolean;
79
+ /**
80
+ * From a list of injectable targets, select the best target per origin.
81
+ * Multi-window apps (like Teams) expose several page targets for the same origin;
82
+ * we only want to inject the overlay into the primary content window.
83
+ */
84
+ export declare function selectBestOverlayTargets(targets: ElectronInspectableTarget[]): ElectronInspectableTarget[];
@@ -1,10 +1,97 @@
1
1
  import { basename, join, resolve } from 'path';
2
+ import { accessSync, constants, readdirSync, statSync } from 'fs';
2
3
  export const DEFAULT_ELECTRON_SERVE_PORT = 5710;
3
4
  export const DEFAULT_ELECTRON_SERVE_HOST = 'localhost';
4
5
  export const DEFAULT_ELECTRON_CDP_PORT = 9223;
5
6
  export const DEFAULT_ELECTRON_TARGET_URL = 'about:blank';
6
7
  export const DEFAULT_ELECTRON_OVERLAY_TAB = 'chat';
7
8
  export const ELECTRON_OVERLAY_APP_PATH = '/electron';
9
+ // Port allocation constants
10
+ export const PORT_HASH_RANGE = 40;
11
+ /**
12
+ * Simple string hash function that returns a value between 0 and max-1.
13
+ * Exported for testing.
14
+ */
15
+ export function hashString(str, max) {
16
+ let hash = 0;
17
+ for (let i = 0; i < str.length; i++) {
18
+ const char = str.charCodeAt(i);
19
+ hash = ((hash << 5) - hash) + char;
20
+ hash = hash & hash; // Convert to 32-bit integer
21
+ }
22
+ return Math.abs(hash) % max;
23
+ }
24
+ /**
25
+ * Try to listen on a specific port and host, returning the assigned port.
26
+ */
27
+ async function tryListenOnPort(port, host) {
28
+ const { createServer } = await import('net');
29
+ return new Promise((resolve, reject) => {
30
+ const server = createServer();
31
+ server.on('error', reject);
32
+ server.listen(port, host, () => {
33
+ const addr = server.address();
34
+ const assignedPort = addr && typeof addr === 'object' ? addr.port : port;
35
+ server.close(() => resolve(assignedPort));
36
+ });
37
+ });
38
+ }
39
+ /**
40
+ * Check if a port is available on both IPv4 (127.0.0.1) and IPv6 (::1).
41
+ * On macOS, `localhost` resolves to `::1`, so we need to check both.
42
+ */
43
+ async function isPortAvailable(port) {
44
+ try {
45
+ await tryListenOnPort(port, '127.0.0.1');
46
+ try {
47
+ await tryListenOnPort(port, '::1');
48
+ }
49
+ catch (err) {
50
+ // ::1 may not be available on some systems — only fail on EADDRINUSE
51
+ if (err.code === 'EADDRINUSE') {
52
+ return false;
53
+ }
54
+ }
55
+ return true;
56
+ }
57
+ catch {
58
+ return false;
59
+ }
60
+ }
61
+ /**
62
+ * Find an available port starting from the given port.
63
+ */
64
+ async function findAvailablePort(startPort, maxAttempts = 100) {
65
+ for (let i = 0; i < maxAttempts; i++) {
66
+ const port = startPort + i;
67
+ if (await isPortAvailable(port)) {
68
+ return port;
69
+ }
70
+ }
71
+ throw new Error(`Could not find available port starting from ${startPort}`);
72
+ }
73
+ /**
74
+ * Get a port for an Electron app based on its path.
75
+ * Uses hash-based offset from base port, with fallback to next available port
76
+ * starting from the preferred port (to stay in the app's "slot" range).
77
+ */
78
+ export async function getElectronAppPort(appPath, basePort) {
79
+ const offset = hashString(appPath, PORT_HASH_RANGE);
80
+ const preferredPort = basePort + offset;
81
+ if (await isPortAvailable(preferredPort)) {
82
+ return preferredPort;
83
+ }
84
+ // Fallback: find next available port starting from preferred (stay in slot range)
85
+ return findAvailablePort(preferredPort + 1);
86
+ }
87
+ /**
88
+ * Get both CDP and serve ports for an Electron app.
89
+ */
90
+ export async function getElectronAppPorts(appPath) {
91
+ const cdpPort = await getElectronAppPort(appPath, DEFAULT_ELECTRON_CDP_PORT);
92
+ const servePort = await getElectronAppPort(appPath, DEFAULT_ELECTRON_SERVE_PORT);
93
+ return { cdpPort, servePort };
94
+ }
8
95
  export function getElectronAppDisplayName(appPath) {
9
96
  const trimmedPath = appPath.replace(/[\\/]+$/, '');
10
97
  const fileName = basename(trimmedPath);
@@ -16,7 +103,80 @@ export function getElectronAppDisplayName(appPath) {
16
103
  export function resolveElectronAppExecutablePath(appPath, platform = process.platform) {
17
104
  const resolvedAppPath = resolve(appPath);
18
105
  if (platform === 'darwin' && resolvedAppPath.toLowerCase().endsWith('.app')) {
19
- return join(resolvedAppPath, 'Contents', 'MacOS', getElectronAppDisplayName(resolvedAppPath));
106
+ const macOSDir = join(resolvedAppPath, 'Contents', 'MacOS');
107
+ // First try the expected name (app name without .app)
108
+ const expectedName = getElectronAppDisplayName(resolvedAppPath);
109
+ const expectedPath = join(macOSDir, expectedName);
110
+ try {
111
+ const stat = statSync(expectedPath);
112
+ if (stat.isFile()) {
113
+ return expectedPath;
114
+ }
115
+ }
116
+ catch {
117
+ // Expected path doesn't exist, scan the directory
118
+ }
119
+ // Scan MacOS directory for the main executable
120
+ // Many Electron apps use "Electron" as the executable name
121
+ // Prefer known main executable names, filter out helper processes
122
+ const helperPatterns = [
123
+ /helper/i,
124
+ /crash/i,
125
+ /gpu/i,
126
+ /renderer/i,
127
+ /plugin/i,
128
+ /utility/i,
129
+ ];
130
+ try {
131
+ const entries = readdirSync(macOSDir);
132
+ // Helper to check if a file is executable
133
+ const isExecutable = (path) => {
134
+ try {
135
+ accessSync(path, constants.X_OK);
136
+ return true;
137
+ }
138
+ catch {
139
+ return false;
140
+ }
141
+ };
142
+ // First pass: look for "Electron" executable (common in Electron apps)
143
+ if (entries.includes('Electron')) {
144
+ const electronPath = join(macOSDir, 'Electron');
145
+ try {
146
+ const stat = statSync(electronPath);
147
+ if (stat.isFile() && isExecutable(electronPath)) {
148
+ return electronPath;
149
+ }
150
+ }
151
+ catch {
152
+ // Continue to next fallback
153
+ }
154
+ }
155
+ // Second pass: find first non-helper executable with execute permission
156
+ for (const entry of entries) {
157
+ // Skip hidden files, scripts, and helper executables
158
+ if (entry.startsWith('.') || entry.endsWith('.sh'))
159
+ continue;
160
+ if (helperPatterns.some((p) => p.test(entry)))
161
+ continue;
162
+ const entryPath = join(macOSDir, entry);
163
+ try {
164
+ const stat = statSync(entryPath);
165
+ if (stat.isFile() && isExecutable(entryPath)) {
166
+ return entryPath;
167
+ }
168
+ }
169
+ catch {
170
+ continue;
171
+ }
172
+ }
173
+ }
174
+ catch {
175
+ // Can't read directory, fall back to expected path
176
+ }
177
+ // Fall back to expected path even if it doesn't exist
178
+ // (error will be caught later)
179
+ return expectedPath;
20
180
  }
21
181
  return resolvedAppPath;
22
182
  }
@@ -135,3 +295,67 @@ export function shouldInjectElectronOverlayTarget(target) {
135
295
  return false;
136
296
  return true;
137
297
  }
298
+ /**
299
+ * Extract the origin from a URL, or return the URL as-is for non-standard schemes.
300
+ */
301
+ function safeOrigin(url) {
302
+ try {
303
+ return new URL(url).origin;
304
+ }
305
+ catch {
306
+ return url;
307
+ }
308
+ }
309
+ /**
310
+ * Score a target for "primary window" ranking within a group of same-origin pages.
311
+ * Higher score = more likely the main content window.
312
+ */
313
+ function scoreOverlayTarget(target) {
314
+ let score = 0;
315
+ const title = target.title ?? '';
316
+ const url = target.url;
317
+ // Longer, more descriptive titles indicate content windows (e.g.
318
+ // "Calendar | Adobe | Microsoft Teams" vs generic "Microsoft Teams")
319
+ score += Math.min(title.length, 120);
320
+ // Penalize URLs with hash fragments that suggest hidden/auxiliary windows
321
+ // (e.g. Teams uses #deepLink=default&isMinimized=false for background windows)
322
+ if (url.includes('isMinimized=') || url.includes('deepLink=')) {
323
+ score -= 200;
324
+ }
325
+ // Prefer clean URLs without large hash fragments (shell pages often have them)
326
+ const hashLength = url.includes('#') ? url.length - url.indexOf('#') : 0;
327
+ score -= Math.min(hashLength, 100);
328
+ return score;
329
+ }
330
+ /**
331
+ * From a list of injectable targets, select the best target per origin.
332
+ * Multi-window apps (like Teams) expose several page targets for the same origin;
333
+ * we only want to inject the overlay into the primary content window.
334
+ */
335
+ export function selectBestOverlayTargets(targets) {
336
+ const injectable = targets.filter(shouldInjectElectronOverlayTarget);
337
+ // Group by origin
338
+ const byOrigin = new Map();
339
+ for (const target of injectable) {
340
+ const origin = safeOrigin(target.url);
341
+ const group = byOrigin.get(origin);
342
+ if (group) {
343
+ group.push(target);
344
+ }
345
+ else {
346
+ byOrigin.set(origin, [target]);
347
+ }
348
+ }
349
+ // Pick the best target from each origin group
350
+ const result = [];
351
+ for (const group of byOrigin.values()) {
352
+ if (group.length === 1) {
353
+ result.push(group[0]);
354
+ continue;
355
+ }
356
+ // Sort by score descending and pick the winner
357
+ group.sort((a, b) => scoreOverlayTarget(b) - scoreOverlayTarget(a));
358
+ result.push(group[0]);
359
+ }
360
+ return result;
361
+ }
package/dist/cli/index.js CHANGED
@@ -8,6 +8,7 @@ import { fileURLToPath } from 'url';
8
8
  import express from 'express';
9
9
  import { WebSocketServer, WebSocket } from 'ws';
10
10
  import { ElectronAppAlreadyRunningError, ElectronOverlayInjector, launchElectronApp, } from './electron-controller.js';
11
+ import { getElectronAppPorts } from './electron-runtime.js';
11
12
  import { buildChromeLaunchArgs, ensureQaProfileScaffold, findChromeExecutable, resolveChromeLaunchProfile, waitForCdpPortFromStderr, } from './chrome-launch.js';
12
13
  import { resolveCliBrowserLaunchUrl } from './launch-url.js';
13
14
  import { parseCliRuntimeFlags } from './runtime-flags.js';
@@ -249,16 +250,33 @@ const PREFERRED_HMR_PORT = 24679;
249
250
  async function main() {
250
251
  // Resolve available ports before anything else — serve port must be known
251
252
  // before Chrome launches (the launch URL contains it).
252
- const SERVE_PORT = await findAvailablePort(PREFERRED_SERVE_PORT);
253
- // For Chrome CDP, we pass port 0 to let Chrome pick any available port,
254
- // then parse the actual port from its stderr. This avoids race conditions
255
- // where Node's port probe succeeds but Chrome still can't bind the port.
256
- // Electron mode keeps the preferred port (external CDP, not launched by us).
257
- const REQUESTED_CDP_PORT = ELECTRON_MODE ? PREFERRED_CDP_PORT : 0;
258
- let CDP_PORT = ELECTRON_MODE ? PREFERRED_CDP_PORT : 0;
253
+ let SERVE_PORT;
254
+ let CDP_PORT;
255
+ let REQUESTED_CDP_PORT;
256
+ let usingDynamicElectronPorts = false;
257
+ if (ELECTRON_MODE && ELECTRON_APP && !RUNTIME_FLAGS.explicitCdpPort) {
258
+ // Dynamic port allocation for Electron apps (hash-based with fallback)
259
+ const ports = await getElectronAppPorts(ELECTRON_APP);
260
+ CDP_PORT = ports.cdpPort;
261
+ SERVE_PORT = ports.servePort;
262
+ REQUESTED_CDP_PORT = CDP_PORT;
263
+ usingDynamicElectronPorts = true;
264
+ }
265
+ else {
266
+ SERVE_PORT = await findAvailablePort(PREFERRED_SERVE_PORT);
267
+ // For Chrome CDP, we pass port 0 to let Chrome pick any available port,
268
+ // then parse the actual port from its stderr. This avoids race conditions
269
+ // where Node's port probe succeeds but Chrome still can't bind the port.
270
+ // Electron mode keeps the preferred port (external CDP, not launched by us).
271
+ REQUESTED_CDP_PORT = ELECTRON_MODE ? PREFERRED_CDP_PORT : 0;
272
+ CDP_PORT = ELECTRON_MODE ? PREFERRED_CDP_PORT : 0;
273
+ }
259
274
  const HMR_PORT = DEV_MODE ? await findAvailablePort(PREFERRED_HMR_PORT) : PREFERRED_HMR_PORT;
260
275
  const SERVE_ORIGIN = `http://localhost:${SERVE_PORT}`;
261
- if (SERVE_PORT !== PREFERRED_SERVE_PORT) {
276
+ if (usingDynamicElectronPorts) {
277
+ console.log(`Dynamic port allocation for Electron app: CDP=${CDP_PORT}, serve=${SERVE_PORT}`);
278
+ }
279
+ else if (SERVE_PORT !== PREFERRED_SERVE_PORT) {
262
280
  console.log(`Port ${PREFERRED_SERVE_PORT} in use, serving on port ${SERVE_PORT}`);
263
281
  }
264
282
  if (DEV_MODE && HMR_PORT !== PREFERRED_HMR_PORT) {
@@ -295,14 +313,49 @@ async function main() {
295
313
  launchedBrowserProcess = child;
296
314
  launchedBrowserLabel = displayName;
297
315
  pipeChildOutput(child, 'electron-app');
316
+ // Track when app exits - quick exits before CDP connects indicate a problem
317
+ let cdpConnected = false;
318
+ let exitCode = null;
319
+ let exitResolve = null;
320
+ const exitPromise = new Promise((resolve) => {
321
+ exitResolve = resolve;
322
+ });
298
323
  child.on('exit', (code) => {
324
+ exitCode = code;
325
+ exitResolve?.();
299
326
  if (shuttingDown)
300
327
  return;
301
- console.log(`${displayName} exited with code ${code}`);
302
- process.exit(0);
328
+ if (cdpConnected) {
329
+ // Normal exit after we connected
330
+ console.log(`${displayName} exited with code ${code}`);
331
+ process.exit(0);
332
+ }
333
+ // If CDP not yet connected, don't exit - let waitForCDP handle it
303
334
  });
304
335
  console.log(`Waiting for ${displayName} CDP on port ${CDP_PORT}...`);
305
- await waitForCDP(CDP_PORT, 40, 500);
336
+ try {
337
+ // Race between CDP connection and app exit
338
+ await Promise.race([
339
+ waitForCDP(CDP_PORT, 40, 500).then(() => {
340
+ cdpConnected = true;
341
+ }),
342
+ exitPromise.then(() => {
343
+ if (!cdpConnected) {
344
+ throw new Error('app-exited');
345
+ }
346
+ }),
347
+ ]);
348
+ }
349
+ catch (err) {
350
+ // Check if app exited quickly (likely due to disabled remote debugging fuse)
351
+ if (exitCode !== null) {
352
+ console.error(`\n${displayName} exited with code ${exitCode} before remote debugging was available.`);
353
+ console.error('This usually means the app has disabled remote debugging (EnableNodeCliInspectArguments fuse).');
354
+ console.error('Some Electron apps disable this for security. Check if there is a developer/debug build available.\n');
355
+ process.exit(1);
356
+ }
357
+ throw new Error(`Could not connect to ${displayName} CDP on port ${CDP_PORT}`);
358
+ }
306
359
  console.log(`Connected to ${displayName} on CDP port ${CDP_PORT}`);
307
360
  // Auto-discover leader's tray join URL when another instance runs on the preferred port.
308
361
  // The leader may still be creating its tray session, so retry a few times.
@@ -3,6 +3,8 @@ export interface CliRuntimeFlags {
3
3
  dev: boolean;
4
4
  serveOnly: boolean;
5
5
  cdpPort: number;
6
+ /** Whether --cdp-port was explicitly specified */
7
+ explicitCdpPort: boolean;
6
8
  electron: boolean;
7
9
  electronApp: string | null;
8
10
  kill: boolean;
@@ -139,6 +139,7 @@ export function parseCliRuntimeFlags(argv) {
139
139
  dev,
140
140
  serveOnly,
141
141
  cdpPort,
142
+ explicitCdpPort,
142
143
  electron,
143
144
  electronApp,
144
145
  kill,
@@ -1,4 +1,4 @@
1
- "use strict";(()=>{var k=[{id:"chat",label:"Chat"},{id:"terminal",label:"Terminal"},{id:"files",label:"Files"},{id:"memory",label:"Memory"}],j="slicc-hidden-tabs",R=["terminal","memory"];function F(){try{let r=localStorage.getItem(j);return r?new Set(JSON.parse(r)):new Set(R)}catch{return new Set(R)}}var N=k.filter(r=>!F().has(r.id)),v="chat",et=new Set(k.map(r=>r.id));function i(r,t=v){return r&&r.length>0?r:t}var q=["top-left","top-right","bottom-left","bottom-right"],W="top-right",K=6,G=12,J=.6;function l(r,t=W){return q.includes(r)?r:t}function g(r={}){return{open:r.open??!1,activeTab:i(r.activeTab,v),corner:l(r.corner)}}function P(r){return{...r,open:!r.open}}function y(r,t){return r.open===t?r:{...r,open:t}}function m(r,t){let e=i(t,r.activeTab);return r.activeTab===e?r:{...r,activeTab:e}}function x(r,t){let e=l(t,r.corner);return r.corner===e?r:{...r,corner:e}}function S(r,t){return r>=K||r>=G&&t>=J}function Y({clientX:r,clientY:t,viewportWidth:e,viewportHeight:n,velocityXPxPerMs:a=0,velocityYPxPerMs:s=0,flickProjectionMs:u=180}){let b=U(r+a*u,0,e),h=U(t+s*u,0,n),f=b<e/2?"left":"right";return`${h<n/2?"top":"bottom"}-${f}`}function U(r,t,e){return Math.min(Math.max(r,t),e)}var M="slicc-electron-overlay:set-tab";var _="slicc-electron-overlay-root",L="slicc-electron-overlay",c="slicc-electron-launcher",d="slicc-electron-sidebar",p="",o=18,V="slicc-electron-overlay-launcher-corner",w=`
1
+ "use strict";(()=>{var B=[{id:"chat",label:"Chat"},{id:"terminal",label:"Terminal"},{id:"files",label:"Files"},{id:"memory",label:"Memory"}],Z="slicc-hidden-tabs",V=["terminal","memory"];function tt(){try{let r=localStorage.getItem(Z);return r?new Set(JSON.parse(r)):new Set(V)}catch{return new Set(V)}}var H=B.filter(r=>!tt().has(r.id)),C="chat",lt=new Set(B.map(r=>r.id));function l(r,t=C){return r&&r.length>0?r:t}var et=["top-left","top-right","bottom-left","bottom-right"],rt="top-right",nt=6,at=12,ot=.6;function b(r,t=rt){return et.includes(r)?r:t}function T(r={}){return{open:r.open??!1,activeTab:l(r.activeTab,C),corner:b(r.corner)}}function z(r){return{...r,open:!r.open}}function _(r,t){return r.open===t?r:{...r,open:t}}function w(r,t){let e=l(t,r.activeTab);return r.activeTab===e?r:{...r,activeTab:e}}function I(r,t){let e=b(t,r.corner);return r.corner===e?r:{...r,corner:e}}function R(r,t){return r>=nt||r>=at&&t>=ot}function j({clientX:r,clientY:t,viewportWidth:e,viewportHeight:n,velocityXPxPerMs:a=0,velocityYPxPerMs:o=0,flickProjectionMs:i=180}){let s=$(r+a*i,0,e),d=$(t+o*i,0,n),u=s<e/2?"left":"right";return`${d<n/2?"top":"bottom"}-${u}`}function $(r,t,e){return Math.min(Math.max(r,t),e)}var F="slicc-electron-overlay:set-tab";var k="slicc-electron-overlay-root",U="slicc-electron-overlay",L="slicc-electron-launcher",O="slicc-electron-sidebar",v="",h=18,G="slicc-electron-overlay-launcher-corner";function X(r,t){let e=r.createElement("style");return e.textContent=t,e}function K(r){let t=r.createElementNS("http://www.w3.org/2000/svg","svg");t.setAttribute("viewBox","0 0 24 24"),t.setAttribute("aria-hidden","true");let e=[{cx:"9",cy:"7.2",r:"4.1"},{cx:"15.2",cy:"8.1",r:"4"}];for(let{cx:a,cy:o,r:i}of e){let s=r.createElementNS("http://www.w3.org/2000/svg","circle");s.setAttribute("cx",a),s.setAttribute("cy",o),s.setAttribute("r",i),t.appendChild(s)}let n=r.createElementNS("http://www.w3.org/2000/svg","path");return n.setAttribute("d","M9.8 12.4h5.1L12.4 21z"),t.appendChild(n),t}var D=`
2
2
  :host {
3
3
  color-scheme: dark light;
4
4
  --s2-gray-25: #1a1a1a;
@@ -47,314 +47,93 @@
47
47
  --s2-shadow-elevated: 0 10px 32px rgba(0, 0, 0, 0.14), 0 2px 10px rgba(0, 0, 0, 0.08);
48
48
  }
49
49
  }
50
- `;function D(){return`
51
- <svg viewBox="0 0 24 24" aria-hidden="true">
52
- <circle cx="9" cy="7.2" r="4.1"></circle>
53
- <circle cx="15.2" cy="8.1" r="4"></circle>
54
- <path d="M9.8 12.4h5.1L12.4 21z"></path>
55
- </svg>
56
- `}function Q(r){return N.map(({id:t,label:e})=>{let n=t===r;return`
57
- <button
58
- type="button"
59
- class="tab-bar__tab${n?" tab-bar__tab--active":""}"
60
- role="tab"
61
- aria-selected="${String(n)}"
62
- data-tab="${t}"
63
- >${e}</button>
64
- `}).join("")}function Z(r,t){try{let e=new URL(r,window.location.href);return e.searchParams.set("tab",t),e.toString()}catch{return r}}var O=class extends HTMLElement{static observedAttributes=["open","corner"];button=null;pointerState=null;suppressClick=!1;connectedCallback(){this.shadowRoot||this.render(),this.sync()}attributeChangedCallback(){this.sync()}render(){let t=this.attachShadow({mode:"open"});t.innerHTML=`
65
- <style>
66
- ${w}
67
- :host {
68
- all: initial;
69
- position: fixed;
70
- top: ${o}px;
71
- right: ${o}px;
72
- z-index: 1;
73
- pointer-events: auto;
74
- display: block;
75
- font-family: var(--s2-font-family);
76
- transition: top var(--s2-transition-default), right var(--s2-transition-default), bottom var(--s2-transition-default), left var(--s2-transition-default);
77
- }
78
- :host([corner="top-left"]) {
79
- top: ${o}px;
80
- right: auto;
81
- bottom: auto;
82
- left: ${o}px;
83
- }
84
- :host([corner="top-right"]) {
85
- top: ${o}px;
86
- right: ${o}px;
87
- bottom: auto;
88
- left: auto;
89
- }
90
- :host([corner="bottom-left"]) {
91
- top: auto;
92
- right: auto;
93
- bottom: ${o}px;
94
- left: ${o}px;
95
- }
96
- :host([corner="bottom-right"]) {
97
- top: auto;
98
- right: ${o}px;
99
- bottom: ${o}px;
100
- left: auto;
101
- }
102
- :host([dragging]) {
103
- transition: none;
104
- }
105
- *, *::before, *::after { box-sizing: border-box; }
106
- button {
107
- width: 44px;
108
- height: 44px;
109
- position: relative;
110
- border: 1px solid rgba(255, 255, 255, 0.62);
111
- border-radius: var(--s2-radius-pill);
112
- background:
113
- radial-gradient(circle at 30% 28%, rgba(255, 255, 255, 0.34), transparent 34%),
114
- linear-gradient(160deg, #ffb15c 0%, color-mix(in srgb, var(--slicc-cone) 92%, #ffb15c) 34%, #a34b00 72%, #38220f 100%);
115
- color: #fff;
116
- box-shadow:
117
- 0 0 0 1px rgba(255, 255, 255, 0.18) inset,
118
- 0 0 0 4px rgba(0, 0, 0, 0.28),
119
- 0 12px 30px rgba(0, 0, 0, 0.4),
120
- 0 2px 10px rgba(0, 0, 0, 0.24);
121
- cursor: grab;
122
- display: inline-flex;
123
- align-items: center;
124
- justify-content: center;
125
- transition:
126
- transform var(--s2-transition-default),
127
- background var(--s2-transition-default),
128
- border-color var(--s2-transition-default),
129
- box-shadow var(--s2-transition-default);
130
- backdrop-filter: blur(12px) saturate(1.05);
131
- touch-action: none;
132
- user-select: none;
133
- -webkit-user-select: none;
134
- }
135
- button::before {
136
- content: '';
137
- position: absolute;
138
- inset: -5px;
139
- border-radius: inherit;
140
- border: 1px solid rgba(255, 255, 255, 0.32);
141
- box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.18);
142
- pointer-events: none;
143
- }
144
- :host([dragging]) button { cursor: grabbing; }
145
- button:hover {
146
- transform: translateY(-1px);
147
- border-color: rgba(255, 255, 255, 0.78);
148
- box-shadow:
149
- 0 0 0 1px rgba(255, 255, 255, 0.24) inset,
150
- 0 0 0 4px rgba(0, 0, 0, 0.34),
151
- 0 16px 34px rgba(0, 0, 0, 0.44),
152
- 0 0 18px color-mix(in srgb, var(--slicc-cone) 40%, transparent);
153
- }
154
- button:active { transform: scale(0.96); }
155
- button[aria-pressed="true"] {
156
- border-color: rgba(255, 255, 255, 0.84);
157
- box-shadow:
158
- 0 0 0 1px rgba(255, 255, 255, 0.24) inset,
159
- 0 0 0 4px rgba(0, 0, 0, 0.36),
160
- 0 16px 38px rgba(0, 0, 0, 0.46),
161
- 0 0 22px color-mix(in srgb, var(--slicc-cone) 52%, transparent);
162
- }
163
- svg {
164
- width: 20px;
165
- height: 20px;
166
- fill: currentColor;
167
- filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.55));
168
- }
169
- </style>
170
- <button type="button" aria-label="Toggle SLICC overlay">
171
- ${D()}
172
- </button>
173
- `,this.button=t.querySelector("button"),this.button?.addEventListener("click",e=>{if(this.suppressClick){this.suppressClick=!1,e.preventDefault(),e.stopImmediatePropagation();return}this.dispatchEvent(new CustomEvent("slicc-overlay-toggle",{bubbles:!0,composed:!0}))}),this.button?.addEventListener("pointerdown",this.onPointerDown),this.button?.addEventListener("pointermove",this.onPointerMove),this.button?.addEventListener("pointerup",this.onPointerUp),this.button?.addEventListener("pointercancel",this.onPointerCancel)}sync(){this.button?.setAttribute("aria-pressed",String(this.hasAttribute("open")))}onPointerDown=t=>{if(!t.isPrimary||t.button!==0||!this.button)return;let e=this.getBoundingClientRect();this.pointerState={pointerId:t.pointerId,startX:t.clientX,startY:t.clientY,startLeft:e.left,startTop:e.top,width:e.width,height:e.height,lastX:t.clientX,lastY:t.clientY,lastTimestamp:t.timeStamp,velocityX:0,velocityY:0,dragging:!1},this.suppressClick=!1,this.button.setPointerCapture(t.pointerId),t.preventDefault()};onPointerMove=t=>{let e=this.pointerState;if(!e||t.pointerId!==e.pointerId)return;let n=t.clientX-e.startX,a=t.clientY-e.startY,s=Math.hypot(n,a);!e.dragging&&S(s,0)&&(e.dragging=!0,this.setAttribute("dragging",""));let u=Math.max(t.timeStamp-e.lastTimestamp,1),b=(t.clientX-e.lastX)/u,h=(t.clientY-e.lastY)/u;if(e.velocityX=e.velocityX===0?b:e.velocityX*.35+b*.65,e.velocityY=e.velocityY===0?h:e.velocityY*.35+h*.65,e.lastX=t.clientX,e.lastY=t.clientY,e.lastTimestamp=t.timeStamp,!e.dragging)return;let f=Math.max(o,window.innerWidth-e.width-o),I=Math.max(o,window.innerHeight-e.height-o),B=X(e.startLeft+n,o,f),z=X(e.startTop+a,o,I);this.style.left=`${B}px`,this.style.top=`${z}px`,this.style.right="auto",this.style.bottom="auto",t.preventDefault()};onPointerUp=t=>{this.finishPointerInteraction(t)};onPointerCancel=t=>{this.finishPointerInteraction(t,!1)};finishPointerInteraction(t,e=!0){let n=this.pointerState;if(!n||t.pointerId!==n.pointerId||!this.button)return;let a=Math.hypot(t.clientX-n.startX,t.clientY-n.startY),s=Math.hypot(n.velocityX,n.velocityY);if(e&&(n.dragging||S(a,s))){let b=Y({clientX:t.clientX,clientY:t.clientY,viewportWidth:window.innerWidth,viewportHeight:window.innerHeight,velocityXPxPerMs:n.velocityX,velocityYPxPerMs:n.velocityY});this.dispatchEvent(new CustomEvent("slicc-overlay-move",{bubbles:!0,composed:!0,detail:{corner:b}})),this.suppressClick=!0,t.preventDefault()}this.button.hasPointerCapture(t.pointerId)&&this.button.releasePointerCapture(t.pointerId),this.pointerState=null,this.removeAttribute("dragging"),this.resetDragStyles()}resetDragStyles(){this.style.left="",this.style.top="",this.style.right="",this.style.bottom=""}},C=class extends HTMLElement{static observedAttributes=["open","active-tab","app-url"];tabButtons=new Map;iframe=null;emptyState=null;currentAppUrl=p;frameLoaded=!1;lastPostedTab=null;connectedCallback(){this.shadowRoot||this.render(),this.sync()}attributeChangedCallback(){this.sync()}render(){let t=this.attachShadow({mode:"open"}),e=i(this.getAttribute("active-tab"));t.innerHTML=`
174
- <style>
175
- ${w}
176
- :host {
177
- all: initial;
178
- position: fixed;
179
- inset: 0;
180
- display: block;
181
- pointer-events: none;
182
- font-family: var(--s2-font-family);
183
- }
184
- :host([open]) {
185
- pointer-events: auto;
186
- }
187
- *, *::before, *::after { box-sizing: border-box; }
188
- .backdrop {
189
- position: absolute;
190
- inset: 0;
191
- background: rgba(0, 0, 0, 0.18);
192
- opacity: 0;
193
- transition: opacity var(--s2-transition-default);
194
- }
195
- :host([open]) .backdrop {
196
- opacity: 1;
197
- }
198
- .sidebar {
199
- position: absolute;
200
- top: 12px;
201
- right: 12px;
202
- bottom: 12px;
203
- width: min(440px, calc(100vw - 24px));
204
- display: flex;
205
- flex-direction: column;
206
- overflow: hidden;
207
- background: color-mix(in srgb, var(--s2-bg-base) 96%, transparent);
208
- color: var(--s2-content-default);
209
- border: 1px solid var(--s2-border-subtle);
210
- border-radius: var(--s2-radius-xl);
211
- box-shadow: var(--s2-shadow-elevated);
212
- transform: translateX(calc(100% + 28px));
213
- transition: transform var(--s2-transition-default);
214
- backdrop-filter: blur(16px);
215
- }
216
- :host([open]) .sidebar {
217
- transform: translateX(0);
218
- }
219
- .header {
220
- display: flex;
221
- align-items: center;
222
- justify-content: space-between;
223
- gap: 12px;
224
- padding: 14px 16px 12px;
225
- border-bottom: 1px solid var(--s2-border-subtle);
226
- background: color-mix(in srgb, var(--s2-bg-layer-1) 92%, transparent);
227
- }
228
- .header__brand {
229
- display: flex;
230
- align-items: center;
231
- gap: 12px;
232
- min-width: 0;
233
- }
234
- .header__logo {
235
- width: 34px;
236
- height: 34px;
237
- border-radius: 50%;
238
- display: inline-flex;
239
- align-items: center;
240
- justify-content: center;
241
- background: linear-gradient(180deg, rgba(239, 112, 0, 0.9), rgba(239, 112, 0, 0.7));
242
- color: #fff;
243
- box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.18);
244
- }
245
- .header__logo svg {
246
- width: 18px;
247
- height: 18px;
248
- fill: currentColor;
249
- }
250
- .header__title {
251
- font-size: 15px;
252
- font-weight: 700;
253
- letter-spacing: 0.01em;
254
- }
255
- .header__subtitle {
256
- font-size: 11px;
257
- color: var(--s2-content-secondary);
258
- }
259
- .header__close {
260
- appearance: none;
261
- border: 1px solid var(--s2-border-subtle);
262
- background: var(--s2-bg-layer-2);
263
- color: var(--s2-content-default);
264
- width: 32px;
265
- height: 32px;
266
- border-radius: 50%;
267
- cursor: pointer;
268
- font-size: 18px;
269
- line-height: 1;
270
- }
271
- .tab-bar {
272
- display: grid;
273
- grid-template-columns: repeat(4, minmax(0, 1fr));
274
- gap: 8px;
275
- padding: 12px 16px;
276
- border-bottom: 1px solid var(--s2-border-subtle);
277
- background: color-mix(in srgb, var(--s2-bg-layer-1) 96%, transparent);
278
- }
279
- .tab-bar__tab {
280
- appearance: none;
281
- border: 1px solid var(--s2-border-subtle);
282
- border-radius: var(--s2-radius-default);
283
- background: var(--s2-bg-layer-2);
284
- color: var(--s2-content-secondary);
285
- padding: 9px 8px;
286
- font-size: 12px;
287
- font-weight: 600;
288
- cursor: pointer;
289
- transition: background var(--s2-transition-default), color var(--s2-transition-default), border-color var(--s2-transition-default);
290
- }
291
- .tab-bar__tab--active {
292
- background: color-mix(in srgb, var(--s2-accent) 18%, var(--s2-bg-layer-2));
293
- color: var(--s2-content-default);
294
- border-color: color-mix(in srgb, var(--s2-accent) 45%, var(--s2-border-default));
295
- }
296
- .viewport {
297
- position: relative;
298
- flex: 1;
299
- min-height: 0;
300
- background: var(--s2-bg-sunken);
301
- }
302
- iframe {
303
- border: 0;
304
- width: 100%;
305
- height: 100%;
306
- display: block;
307
- background: var(--s2-bg-base);
308
- }
309
- .empty-state {
310
- position: absolute;
311
- inset: 0;
312
- display: flex;
313
- align-items: center;
314
- justify-content: center;
315
- padding: 24px;
316
- text-align: center;
317
- color: var(--s2-content-secondary);
318
- font-size: 13px;
319
- line-height: 1.5;
320
- }
321
- .empty-state[hidden] {
322
- display: none;
323
- }
324
- </style>
325
- <div class="backdrop" part="backdrop"></div>
326
- <aside class="sidebar" part="sidebar" aria-label="SLICC overlay sidebar">
327
- <header class="header">
328
- <div class="header__brand">
329
- <span class="header__logo">${D()}</span>
330
- <div>
331
- <div class="header__title">slicc</div>
332
- <div class="header__subtitle">electron float</div>
333
- </div>
334
- </div>
335
- <button type="button" class="header__close" aria-label="Close SLICC overlay">\xD7</button>
336
- </header>
337
- <div class="tab-bar" role="tablist" aria-label="SLICC overlay tabs">
338
- ${Q(e)}
339
- </div>
340
- <div class="viewport">
341
- <iframe title="SLICC electron float"></iframe>
342
- <div class="empty-state">Starting the local SLICC runtime\u2026</div>
343
- </div>
344
- </aside>
345
- `,t.querySelector(".backdrop")?.addEventListener("click",()=>{this.dispatchEvent(new CustomEvent("slicc-overlay-close",{bubbles:!0,composed:!0}))}),t.querySelector(".header__close")?.addEventListener("click",()=>{this.dispatchEvent(new CustomEvent("slicc-overlay-close",{bubbles:!0,composed:!0}))}),t.querySelectorAll("[data-tab]").forEach(n=>{let a=i(n.dataset.tab);this.tabButtons.set(a,n),n.addEventListener("click",()=>{this.dispatchEvent(new CustomEvent("slicc-overlay-select-tab",{bubbles:!0,composed:!0,detail:{tab:a}}))})}),this.iframe=t.querySelector("iframe"),this.emptyState=t.querySelector(".empty-state"),this.iframe?.addEventListener("load",()=>{this.frameLoaded=!0,this.postActiveTab()})}sync(){let t=i(this.getAttribute("active-tab")),e=this.getAttribute("app-url")?.trim()??p;for(let[n,a]of this.tabButtons){let s=n===t;a.classList.toggle("tab-bar__tab--active",s),a.setAttribute("aria-selected",String(s))}this.emptyState?.toggleAttribute("hidden",!!e),this.syncFrameUrl(e,t),this.postActiveTab()}syncFrameUrl(t,e){if(this.iframe){if(!t){this.currentAppUrl&&(this.currentAppUrl=p,this.frameLoaded=!1,this.lastPostedTab=null,this.iframe.removeAttribute("src"));return}t!==this.currentAppUrl&&(this.currentAppUrl=t,this.frameLoaded=!1,this.lastPostedTab=null,this.iframe.src=Z(t,e))}}postActiveTab(){if(!this.frameLoaded||!this.iframe?.contentWindow)return;let t=i(this.getAttribute("active-tab"));this.lastPostedTab!==t&&(this.iframe.contentWindow.postMessage({type:M,tab:t},"*"),this.lastPostedTab=t)}},E=class extends HTMLElement{static observedAttributes=["open","active-tab","app-url","corner"];state=g();appUrlValue=p;syncingAttributes=!1;connectedCallback(){this.state=g({open:this.hasAttribute("open"),activeTab:i(this.getAttribute("active-tab")),corner:l(this.getAttribute("corner")??A(this.ownerDocument.defaultView))}),this.appUrlValue=this.getAttribute("app-url")?.trim()??p,this.shadowRoot||this.render(),this.syncChildren(),T(this.ownerDocument.defaultView,this.state.corner),this.ownerDocument.addEventListener("keydown",this.onKeyDown,!0)}disconnectedCallback(){this.ownerDocument.removeEventListener("keydown",this.onKeyDown,!0)}attributeChangedCallback(){this.syncingAttributes||(this.state=g({open:this.hasAttribute("open"),activeTab:i(this.getAttribute("active-tab")),corner:l(this.getAttribute("corner")??A(this.ownerDocument.defaultView))}),this.appUrlValue=this.getAttribute("app-url")?.trim()??p,this.syncChildren(),T(this.ownerDocument.defaultView,this.state.corner))}get open(){return this.state.open}set open(t){this.applyState(y(this.state,t))}get activeTab(){return this.state.activeTab}set activeTab(t){this.applyState(m(this.state,t))}get corner(){return this.state.corner}set corner(t){this.applyState(x(this.state,t))}get appUrl(){return this.appUrlValue}set appUrl(t){let e=t.trim();e!==this.appUrlValue&&(this.appUrlValue=e,this.syncingAttributes=!0,e?this.setAttribute("app-url",e):this.removeAttribute("app-url"),this.syncingAttributes=!1,this.syncChildren())}toggle(){this.applyState(P(this.state))}showSidebar(){this.applyState(y(this.state,!0))}hideSidebar(){this.applyState(y(this.state,!1))}applyState(t){t.open===this.state.open&&t.activeTab===this.state.activeTab&&t.corner===this.state.corner||(this.state=t,this.syncingAttributes=!0,this.toggleAttribute("open",t.open),this.setAttribute("active-tab",t.activeTab),this.setAttribute("corner",t.corner),this.syncingAttributes=!1,this.syncChildren(),T(this.ownerDocument.defaultView,this.state.corner))}onKeyDown=t=>{t.key!=="Escape"||!this.state.open||this.hideSidebar()};render(){let t=this.attachShadow({mode:"open"});t.innerHTML=`
346
- <style>
347
- ${w}
348
- :host {
349
- all: initial;
350
- position: fixed;
351
- inset: 0;
352
- display: block;
353
- pointer-events: none;
354
- z-index: 2147483647;
355
- contain: layout style paint;
356
- }
357
- </style>
358
- <${c}></${c}>
359
- <${d}></${d}>
360
- `,t.querySelector(c)?.addEventListener("slicc-overlay-toggle",()=>this.toggle()),t.querySelector(c)?.addEventListener("slicc-overlay-move",e=>{let n=e.detail?.corner;this.applyState(x(this.state,n))}),t.querySelector(d)?.addEventListener("slicc-overlay-close",()=>this.hideSidebar()),t.querySelector(d)?.addEventListener("slicc-overlay-select-tab",e=>{let n=e.detail?.tab;this.applyState(m(this.state,n))})}syncChildren(){let t=this.shadowRoot;if(!t)return;let e=t.querySelector(c),n=t.querySelector(d);!e||!n||(e.toggleAttribute("open",this.state.open),e.setAttribute("corner",this.state.corner),n.toggleAttribute("open",this.state.open),n.setAttribute("active-tab",this.state.activeTab),this.appUrlValue?n.setAttribute("app-url",this.appUrlValue):n.removeAttribute("app-url"))}};function tt(r=customElements){r.get(c)||r.define(c,O),r.get(d)||r.define(d,C),r.get(L)||r.define(L,E)}function $(r=document,t={}){tt(r.defaultView?.customElements??customElements);let e=r.getElementById(_),n;return e instanceof E?n=e:(e?.remove(),n=r.createElement(L)),n.id=_,n.isConnected||(r.body??r.documentElement).appendChild(n),typeof t.open=="boolean"&&(n.open=t.open),t.activeTab!==void 0&&(n.activeTab=i(t.activeTab)),t.appUrl!==void 0&&(n.appUrl=t.appUrl??p),t.corner!==void 0&&(n.corner=t.corner??A(r.defaultView)),n}function H(r=document){r.getElementById(_)?.remove()}function A(r){try{return l(r?.sessionStorage.getItem(V))}catch{return l(null)}}function T(r,t){try{r?.sessionStorage.setItem(V,t)}catch{}}function X(r,t,e){return Math.min(Math.max(r,t),e)}window.__SLICC_ELECTRON_OVERLAY__={inject(r={}){$(document,r)},remove(){H(document)}};})();
50
+ `;function it(r,t){try{let e=new URL(r,window.location.href);return e.searchParams.set("tab",t),e.toString()}catch{return r}}var P=class extends HTMLElement{static observedAttributes=["open","corner"];button=null;pointerState=null;suppressClick=!1;connectedCallback(){this.shadowRoot||this.render(),this.sync()}attributeChangedCallback(){this.sync()}render(){let t=this.attachShadow({mode:"open"}),e=this.ownerDocument,n=h;t.appendChild(X(e,`
51
+ ${D}
52
+ :host {
53
+ all: initial;
54
+ position: fixed;
55
+ top: ${n}px;
56
+ right: ${n}px;
57
+ z-index: 1;
58
+ pointer-events: auto;
59
+ display: block;
60
+ font-family: var(--s2-font-family);
61
+ transition: top var(--s2-transition-default), right var(--s2-transition-default), bottom var(--s2-transition-default), left var(--s2-transition-default);
62
+ }
63
+ :host([corner="top-left"]) { top: ${n}px; right: auto; bottom: auto; left: ${n}px; }
64
+ :host([corner="top-right"]) { top: ${n}px; right: ${n}px; bottom: auto; left: auto; }
65
+ :host([corner="bottom-left"]) { top: auto; right: auto; bottom: ${n}px; left: ${n}px; }
66
+ :host([corner="bottom-right"]) { top: auto; right: ${n}px; bottom: ${n}px; left: auto; }
67
+ :host([dragging]) { transition: none; }
68
+ *, *::before, *::after { box-sizing: border-box; }
69
+ button {
70
+ width: 44px; height: 44px; position: relative;
71
+ border: 1px solid rgba(255, 255, 255, 0.62);
72
+ border-radius: var(--s2-radius-pill);
73
+ background: radial-gradient(circle at 30% 28%, rgba(255, 255, 255, 0.34), transparent 34%),
74
+ linear-gradient(160deg, #ffb15c 0%, color-mix(in srgb, var(--slicc-cone) 92%, #ffb15c) 34%, #a34b00 72%, #38220f 100%);
75
+ color: #fff;
76
+ box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.18) inset, 0 0 0 4px rgba(0, 0, 0, 0.28), 0 12px 30px rgba(0, 0, 0, 0.4), 0 2px 10px rgba(0, 0, 0, 0.24);
77
+ cursor: grab;
78
+ display: inline-flex; align-items: center; justify-content: center;
79
+ transition: transform var(--s2-transition-default), background var(--s2-transition-default), border-color var(--s2-transition-default), box-shadow var(--s2-transition-default);
80
+ backdrop-filter: blur(12px) saturate(1.05);
81
+ touch-action: none; user-select: none; -webkit-user-select: none;
82
+ }
83
+ button::before {
84
+ content: ''; position: absolute; inset: -5px; border-radius: inherit;
85
+ border: 1px solid rgba(255, 255, 255, 0.32); box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.18); pointer-events: none;
86
+ }
87
+ :host([dragging]) button { cursor: grabbing; }
88
+ button:hover {
89
+ transform: translateY(-1px); border-color: rgba(255, 255, 255, 0.78);
90
+ box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.24) inset, 0 0 0 4px rgba(0, 0, 0, 0.34), 0 16px 34px rgba(0, 0, 0, 0.44), 0 0 18px color-mix(in srgb, var(--slicc-cone) 40%, transparent);
91
+ }
92
+ button:active { transform: scale(0.96); }
93
+ button[aria-pressed="true"] {
94
+ border-color: rgba(255, 255, 255, 0.84);
95
+ box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.24) inset, 0 0 0 4px rgba(0, 0, 0, 0.36), 0 16px 38px rgba(0, 0, 0, 0.46), 0 0 22px color-mix(in srgb, var(--slicc-cone) 52%, transparent);
96
+ }
97
+ svg { width: 20px; height: 20px; fill: currentColor; filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.55)); }
98
+ `));let a=e.createElement("button");a.type="button",a.setAttribute("aria-label","Toggle SLICC overlay"),a.appendChild(K(e)),t.appendChild(a),this.button=a,this.button?.addEventListener("click",o=>{if(this.suppressClick){this.suppressClick=!1,o.preventDefault(),o.stopImmediatePropagation();return}this.dispatchEvent(new CustomEvent("slicc-overlay-toggle",{bubbles:!0,composed:!0}))}),this.button?.addEventListener("pointerdown",this.onPointerDown),this.button?.addEventListener("pointermove",this.onPointerMove),this.button?.addEventListener("pointerup",this.onPointerUp),this.button?.addEventListener("pointercancel",this.onPointerCancel)}sync(){this.button?.setAttribute("aria-pressed",String(this.hasAttribute("open")))}onPointerDown=t=>{if(!t.isPrimary||t.button!==0||!this.button)return;let e=this.getBoundingClientRect();this.pointerState={pointerId:t.pointerId,startX:t.clientX,startY:t.clientY,startLeft:e.left,startTop:e.top,width:e.width,height:e.height,lastX:t.clientX,lastY:t.clientY,lastTimestamp:t.timeStamp,velocityX:0,velocityY:0,dragging:!1},this.suppressClick=!1,this.button.setPointerCapture(t.pointerId),t.preventDefault()};onPointerMove=t=>{let e=this.pointerState;if(!e||t.pointerId!==e.pointerId)return;let n=t.clientX-e.startX,a=t.clientY-e.startY,o=Math.hypot(n,a);!e.dragging&&R(o,0)&&(e.dragging=!0,this.setAttribute("dragging",""));let i=Math.max(t.timeStamp-e.lastTimestamp,1),s=(t.clientX-e.lastX)/i,d=(t.clientY-e.lastY)/i;if(e.velocityX=e.velocityX===0?s:e.velocityX*.35+s*.65,e.velocityY=e.velocityY===0?d:e.velocityY*.35+d*.65,e.lastX=t.clientX,e.lastY=t.clientY,e.lastTimestamp=t.timeStamp,!e.dragging)return;let u=Math.max(h,window.innerWidth-e.width-h),g=Math.max(h,window.innerHeight-e.height-h),E=W(e.startLeft+n,h,u),p=W(e.startTop+a,h,g);this.style.left=`${E}px`,this.style.top=`${p}px`,this.style.right="auto",this.style.bottom="auto",t.preventDefault()};onPointerUp=t=>{this.finishPointerInteraction(t)};onPointerCancel=t=>{this.finishPointerInteraction(t,!1)};finishPointerInteraction(t,e=!0){let n=this.pointerState;if(!n||t.pointerId!==n.pointerId||!this.button)return;let a=Math.hypot(t.clientX-n.startX,t.clientY-n.startY),o=Math.hypot(n.velocityX,n.velocityY);if(e&&(n.dragging||R(a,o))){let s=j({clientX:t.clientX,clientY:t.clientY,viewportWidth:window.innerWidth,viewportHeight:window.innerHeight,velocityXPxPerMs:n.velocityX,velocityYPxPerMs:n.velocityY});this.dispatchEvent(new CustomEvent("slicc-overlay-move",{bubbles:!0,composed:!0,detail:{corner:s}})),this.suppressClick=!0,t.preventDefault()}this.button.hasPointerCapture(t.pointerId)&&this.button.releasePointerCapture(t.pointerId),this.pointerState=null,this.removeAttribute("dragging"),this.resetDragStyles()}resetDragStyles(){this.style.left="",this.style.top="",this.style.right="",this.style.bottom=""}},Y=class extends HTMLElement{static observedAttributes=["open","active-tab","app-url"];tabButtons=new Map;iframe=null;emptyState=null;currentAppUrl=v;frameLoaded=!1;lastPostedTab=null;connectedCallback(){this.shadowRoot||this.render(),this.sync()}attributeChangedCallback(){this.sync()}render(){let t=this.attachShadow({mode:"open"}),e=this.ownerDocument,n=l(this.getAttribute("active-tab"));t.appendChild(X(e,`
99
+ ${D}
100
+ :host { all: initial; position: fixed; inset: 0; display: block; pointer-events: none; font-family: var(--s2-font-family); }
101
+ :host([open]) { pointer-events: auto; }
102
+ *, *::before, *::after { box-sizing: border-box; }
103
+ .backdrop { position: absolute; inset: 0; background: rgba(0, 0, 0, 0.18); opacity: 0; transition: opacity var(--s2-transition-default); }
104
+ :host([open]) .backdrop { opacity: 1; }
105
+ .sidebar {
106
+ position: absolute; top: 12px; right: 12px; bottom: 12px; width: min(440px, calc(100vw - 24px));
107
+ display: flex; flex-direction: column; overflow: hidden;
108
+ background: color-mix(in srgb, var(--s2-bg-base) 96%, transparent); color: var(--s2-content-default);
109
+ border: 1px solid var(--s2-border-subtle); border-radius: var(--s2-radius-xl);
110
+ box-shadow: var(--s2-shadow-elevated); transform: translateX(calc(100% + 28px));
111
+ transition: transform var(--s2-transition-default); backdrop-filter: blur(16px);
112
+ }
113
+ :host([open]) .sidebar { transform: translateX(0); }
114
+ .header { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 14px 16px 12px; border-bottom: 1px solid var(--s2-border-subtle); background: color-mix(in srgb, var(--s2-bg-layer-1) 92%, transparent); }
115
+ .header__brand { display: flex; align-items: center; gap: 12px; min-width: 0; }
116
+ .header__logo { width: 34px; height: 34px; border-radius: 50%; display: inline-flex; align-items: center; justify-content: center; background: linear-gradient(180deg, rgba(239, 112, 0, 0.9), rgba(239, 112, 0, 0.7)); color: #fff; box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.18); }
117
+ .header__logo svg { width: 18px; height: 18px; fill: currentColor; }
118
+ .header__title { font-size: 15px; font-weight: 700; letter-spacing: 0.01em; }
119
+ .header__subtitle { font-size: 11px; color: var(--s2-content-secondary); }
120
+ .header__close { appearance: none; border: 1px solid var(--s2-border-subtle); background: var(--s2-bg-layer-2); color: var(--s2-content-default); width: 32px; height: 32px; border-radius: 50%; cursor: pointer; font-size: 18px; line-height: 1; }
121
+ .tab-bar { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px; padding: 12px 16px; border-bottom: 1px solid var(--s2-border-subtle); background: color-mix(in srgb, var(--s2-bg-layer-1) 96%, transparent); }
122
+ .tab-bar__tab { appearance: none; border: 1px solid var(--s2-border-subtle); border-radius: var(--s2-radius-default); background: var(--s2-bg-layer-2); color: var(--s2-content-secondary); padding: 9px 8px; font-size: 12px; font-weight: 600; cursor: pointer; transition: background var(--s2-transition-default), color var(--s2-transition-default), border-color var(--s2-transition-default); }
123
+ .tab-bar__tab--active { background: color-mix(in srgb, var(--s2-accent) 18%, var(--s2-bg-layer-2)); color: var(--s2-content-default); border-color: color-mix(in srgb, var(--s2-accent) 45%, var(--s2-border-default)); }
124
+ .viewport { position: relative; flex: 1; min-height: 0; background: var(--s2-bg-sunken); }
125
+ iframe { border: 0; width: 100%; height: 100%; display: block; background: var(--s2-bg-base); }
126
+ .empty-state { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; padding: 24px; text-align: center; color: var(--s2-content-secondary); font-size: 13px; line-height: 1.5; }
127
+ .empty-state[hidden] { display: none; }
128
+ `));let a=e.createElement("div");a.className="backdrop",a.setAttribute("part","backdrop"),a.addEventListener("click",()=>{this.dispatchEvent(new CustomEvent("slicc-overlay-close",{bubbles:!0,composed:!0}))}),t.appendChild(a);let o=e.createElement("aside");o.className="sidebar",o.setAttribute("part","sidebar"),o.setAttribute("aria-label","SLICC overlay sidebar");let i=e.createElement("header");i.className="header";let s=e.createElement("div");s.className="header__brand";let d=e.createElement("span");d.className="header__logo",d.appendChild(K(e)),s.appendChild(d);let u=e.createElement("div"),g=e.createElement("div");g.className="header__title",g.textContent="slicc";let E=e.createElement("div");E.className="header__subtitle",E.textContent="electron float",u.appendChild(g),u.appendChild(E),s.appendChild(u),i.appendChild(s);let p=e.createElement("button");p.type="button",p.className="header__close",p.setAttribute("aria-label","Close SLICC overlay"),p.textContent="\xD7",p.addEventListener("click",()=>{this.dispatchEvent(new CustomEvent("slicc-overlay-close",{bubbles:!0,composed:!0}))}),i.appendChild(p),o.appendChild(i);let m=e.createElement("div");m.className="tab-bar",m.setAttribute("role","tablist"),m.setAttribute("aria-label","SLICC overlay tabs");for(let{id:f,label:Q}of H){let c=e.createElement("button");c.type="button",c.className="tab-bar__tab"+(f===n?" tab-bar__tab--active":""),c.setAttribute("role","tab"),c.setAttribute("aria-selected",String(f===n)),c.dataset.tab=f,c.textContent=Q,this.tabButtons.set(f,c),c.addEventListener("click",()=>{this.dispatchEvent(new CustomEvent("slicc-overlay-select-tab",{bubbles:!0,composed:!0,detail:{tab:f}}))}),m.appendChild(c)}o.appendChild(m);let y=e.createElement("div");y.className="viewport";let x=e.createElement("iframe");x.title="SLICC electron float",this.iframe=x,x.addEventListener("load",()=>{this.frameLoaded=!0,this.postActiveTab()}),y.appendChild(x);let S=e.createElement("div");S.className="empty-state",S.textContent="Starting the local SLICC runtime\u2026",this.emptyState=S,y.appendChild(S),o.appendChild(y),t.appendChild(o),this.iframe?.addEventListener("load",()=>{this.frameLoaded=!0,this.postActiveTab()})}sync(){let t=l(this.getAttribute("active-tab")),e=this.getAttribute("app-url")?.trim()??v;for(let[n,a]of this.tabButtons){let o=n===t;a.classList.toggle("tab-bar__tab--active",o),a.setAttribute("aria-selected",String(o))}this.emptyState?.toggleAttribute("hidden",!!e),this.syncFrameUrl(e,t),this.postActiveTab()}syncFrameUrl(t,e){if(this.iframe){if(!t){this.currentAppUrl&&(this.currentAppUrl=v,this.frameLoaded=!1,this.lastPostedTab=null,this.iframe.removeAttribute("src"));return}t!==this.currentAppUrl&&(this.currentAppUrl=t,this.frameLoaded=!1,this.lastPostedTab=null,this.iframe.src=it(t,e))}}postActiveTab(){if(!this.frameLoaded||!this.iframe?.contentWindow)return;let t=l(this.getAttribute("active-tab"));this.lastPostedTab!==t&&(this.iframe.contentWindow.postMessage({type:F,tab:t},"*"),this.lastPostedTab=t)}},A=class extends HTMLElement{static observedAttributes=["open","active-tab","app-url","corner"];state=T();appUrlValue=v;syncingAttributes=!1;connectedCallback(){this.state=T({open:this.hasAttribute("open"),activeTab:l(this.getAttribute("active-tab")),corner:b(this.getAttribute("corner")??M(this.ownerDocument.defaultView))}),this.appUrlValue=this.getAttribute("app-url")?.trim()??v,this.shadowRoot||this.render(),this.syncChildren(),N(this.ownerDocument.defaultView,this.state.corner),this.ownerDocument.addEventListener("keydown",this.onKeyDown,!0)}disconnectedCallback(){this.ownerDocument.removeEventListener("keydown",this.onKeyDown,!0)}attributeChangedCallback(){this.syncingAttributes||(this.state=T({open:this.hasAttribute("open"),activeTab:l(this.getAttribute("active-tab")),corner:b(this.getAttribute("corner")??M(this.ownerDocument.defaultView))}),this.appUrlValue=this.getAttribute("app-url")?.trim()??v,this.syncChildren(),N(this.ownerDocument.defaultView,this.state.corner))}get open(){return this.state.open}set open(t){this.applyState(_(this.state,t))}get activeTab(){return this.state.activeTab}set activeTab(t){this.applyState(w(this.state,t))}get corner(){return this.state.corner}set corner(t){this.applyState(I(this.state,t))}get appUrl(){return this.appUrlValue}set appUrl(t){let e=t.trim();e!==this.appUrlValue&&(this.appUrlValue=e,this.syncingAttributes=!0,e?this.setAttribute("app-url",e):this.removeAttribute("app-url"),this.syncingAttributes=!1,this.syncChildren())}toggle(){this.applyState(z(this.state))}showSidebar(){this.applyState(_(this.state,!0))}hideSidebar(){this.applyState(_(this.state,!1))}applyState(t){t.open===this.state.open&&t.activeTab===this.state.activeTab&&t.corner===this.state.corner||(this.state=t,this.syncingAttributes=!0,this.toggleAttribute("open",t.open),this.setAttribute("active-tab",t.activeTab),this.setAttribute("corner",t.corner),this.syncingAttributes=!1,this.syncChildren(),N(this.ownerDocument.defaultView,this.state.corner))}onKeyDown=t=>{t.key!=="Escape"||!this.state.open||this.hideSidebar()};render(){let t=this.attachShadow({mode:"open"}),e=this.ownerDocument;t.appendChild(X(e,`
129
+ ${D}
130
+ :host {
131
+ all: initial;
132
+ position: fixed;
133
+ inset: 0;
134
+ display: block;
135
+ pointer-events: none;
136
+ z-index: 2147483647;
137
+ contain: layout style paint;
138
+ }
139
+ `));let n=e.createElement(L);n.addEventListener("slicc-overlay-toggle",()=>this.toggle()),n.addEventListener("slicc-overlay-move",o=>{let i=o.detail?.corner;this.applyState(I(this.state,i))}),t.appendChild(n);let a=e.createElement(O);a.addEventListener("slicc-overlay-close",()=>this.hideSidebar()),a.addEventListener("slicc-overlay-select-tab",o=>{let i=o.detail?.tab;this.applyState(w(this.state,i))}),t.appendChild(a)}syncChildren(){let t=this.shadowRoot;if(!t)return;let e=t.querySelector(L),n=t.querySelector(O);!e||!n||(e.toggleAttribute("open",this.state.open),e.setAttribute("corner",this.state.corner),n.toggleAttribute("open",this.state.open),n.setAttribute("active-tab",this.state.activeTab),this.appUrlValue?n.setAttribute("app-url",this.appUrlValue):n.removeAttribute("app-url"))}};function st(r=customElements){r.get(L)||r.define(L,P),r.get(O)||r.define(O,Y),r.get(U)||r.define(U,A)}function q(r=document,t={}){st(r.defaultView?.customElements??customElements);let e=r.getElementById(k),n;return e instanceof A?n=e:(e?.remove(),n=r.createElement(U)),n.id=k,n.isConnected||(r.body??r.documentElement).appendChild(n),typeof t.open=="boolean"&&(n.open=t.open),t.activeTab!==void 0&&(n.activeTab=l(t.activeTab)),t.appUrl!==void 0&&(n.appUrl=t.appUrl??v),t.corner!==void 0&&(n.corner=t.corner??M(r.defaultView)),n}function J(r=document){r.getElementById(k)?.remove()}function M(r){try{return b(r?.sessionStorage.getItem(G))}catch{return b(null)}}function N(r,t){try{r?.sessionStorage.setItem(G,t)}catch{}}function W(r,t,e){return Math.min(Math.max(r,t),e)}window.__SLICC_ELECTRON_OVERLAY__={inject(r={}){try{q(document,r)}catch(t){console.error("[slicc-overlay] Injection failed:",t)}},remove(){try{J(document)}catch(r){console.error("[slicc-overlay] Removal failed:",r)}}};})();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sliccy",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
4
4
  "description": "Browser-based coding agent with thin CLI server",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",