impel-cli 0.20.47 → 0.20.48

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/RELEASE_NOTES.md CHANGED
@@ -1,5 +1,24 @@
1
1
  # Release notes
2
2
 
3
+ ## 0.20.48 — Confine Tasks to the primary app window
4
+
5
+ - Fixes the remaining `0.20.47` regression that let an auxiliary ChatGPT or
6
+ Claude renderer attach a second authenticated Tasks board in a large framed
7
+ window, obscuring the normal app.
8
+ - Binds Tasks once to the canonical primary vendor window, rejects child,
9
+ internal, auxiliary, and unowned renderers fail-closed, and removes focused-
10
+ window guessing from navigation ownership.
11
+ - Couples fallback installation to the canonical renderer and deduplicates
12
+ paired Electron navigation events so one user action creates at most one
13
+ board view.
14
+ - Advances the managed profile contract to v39 so every existing install
15
+ rewrites and reloads the corrected external Tasks preload. Vendor pins and
16
+ built bundle bytes remain unchanged.
17
+ - Adds a behavioral reproduction of the production screenshot: an auxiliary
18
+ renderer fires both navigation events before the fallback is clicked, while
19
+ the test proves exactly one board attaches to the primary window and the
20
+ fallback remains a frameless 120×38 control.
21
+
3
22
  ## 0.20.47 — Keep Tasks on the primary app window
4
23
 
5
24
  - Makes the native Tasks fallback choose the earliest surviving primary host
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "impel-cli",
3
- "version": "0.20.47",
3
+ "version": "0.20.48",
4
4
  "description": "Prepare isolated Claude and Codex workspaces for every accessible Impel tenant",
5
5
  "type": "module",
6
6
  "bin": {
@@ -364,14 +364,20 @@ if (
364
364
  return;
365
365
  }
366
366
  const windows = new Map();
367
+ const internalWindows = new WeakSet();
367
368
  const internalContents = new Set();
368
369
  const nativeNavOwners = new Map();
369
370
  const configuredSessions = new WeakSet();
370
371
  let creatingInternalWindow = false;
371
372
  let hasDomNavigation = false;
373
+ let primaryHost = null;
372
374
  const createInternalWindow = (options) => {
373
375
  creatingInternalWindow = true;
374
- try { return new BrowserWindow(options); }
376
+ try {
377
+ const window = new BrowserWindow(options);
378
+ internalWindows.add(window);
379
+ return window;
380
+ }
375
381
  finally { creatingInternalWindow = false; }
376
382
  };
377
383
  const stateFor = (window) => {
@@ -467,7 +473,23 @@ if (
467
473
  }
468
474
  }
469
475
  };
476
+ const isHostWindow = (window) => {
477
+ if (!window || window.isDestroyed() || internalWindows.has(window) || window.getParentWindow?.()) return false;
478
+ try {
479
+ const url = new URL(window.webContents.getURL());
480
+ if (url.searchParams.has("initialRoute")) return false;
481
+ } catch {}
482
+ return true;
483
+ };
484
+ const primaryHostWindow = () => {
485
+ if (isHostWindow(primaryHost)) return primaryHost;
486
+ primaryHost = BrowserWindow.getAllWindows()
487
+ .filter(isHostWindow)
488
+ .sort((left, right) => left.id - right.id)[0] || null;
489
+ return primaryHost;
490
+ };
470
491
  const hide = (window) => {
492
+ if (window !== primaryHostWindow()) return;
471
493
  const state = windows.get(window.id);
472
494
  if (!state) return;
473
495
  state.visible = false;
@@ -476,17 +498,14 @@ if (
476
498
  deactivateNavigation();
477
499
  };
478
500
  const isPrimaryWindow = (window) => {
479
- if (!window || window.isDestroyed() || window.getParentWindow?.()) return false;
480
- try {
481
- const url = new URL(window.webContents.getURL());
482
- if (url.searchParams.has("initialRoute")) return false;
483
- } catch {}
484
- return window.isVisible?.() !== false;
501
+ return isHostWindow(window) && window === primaryHostWindow() && window.isVisible?.() !== false;
502
+ };
503
+ const fallbackHostWindow = () => {
504
+ const window = primaryHostWindow();
505
+ return isPrimaryWindow(window) ? window : null;
485
506
  };
486
- const fallbackHostWindow = () => BrowserWindow.getAllWindows()
487
- .filter(isPrimaryWindow)
488
- .sort((left, right) => left.id - right.id)[0] || null;
489
507
  const createView = (window) => {
508
+ if (!window || window !== primaryHostWindow() || !isHostWindow(window)) return null;
490
509
  const state = stateFor(window);
491
510
  if (state.view && !state.view.webContents.isDestroyed()) return state;
492
511
  const view = new WebContentsView({
@@ -532,6 +551,7 @@ if (
532
551
  };
533
552
  const show = (window, bounds) => {
534
553
  const state = createView(window);
554
+ if (!state) return;
535
555
  state.bounds = bounds;
536
556
  state.view.setBounds(bounds);
537
557
  state.view.setVisible(true);
@@ -542,6 +562,7 @@ if (
542
562
  record("board-shown");
543
563
  };
544
564
  const layoutBoard = (window, bounds = null) => {
565
+ if (window !== primaryHostWindow()) return;
545
566
  const state = windows.get(window.id);
546
567
  if (!state?.visible || !state.view || state.view.webContents.isDestroyed()) return;
547
568
  if (bounds) state.bounds = bounds;
@@ -576,12 +597,12 @@ if (
576
597
  const content = window.getContentBounds();
577
598
  state.nativeNav.setBounds({ x: content.x + 12, y: content.y + Math.max(48, content.height - 92), width: Math.min(120, Math.max(80, content.width - 24)), height: 38 });
578
599
  };
579
- const ensureNativeNav = () => {
580
- // Renderer probes finish independently. Always bind the singleton
581
- // fallback to the earliest surviving host window, not whichever visible
582
- // auxiliary renderer happens to exhaust its probe retries first.
600
+ const ensureNativeNav = (probeWindow) => {
601
+ // Renderer probes finish independently. Only the canonical host may
602
+ // install the singleton fallback; an auxiliary renderer cannot redirect
603
+ // it or acquire its own authenticated board view.
583
604
  const window = fallbackHostWindow();
584
- if (!window) return;
605
+ if (!window || probeWindow !== window) return;
585
606
  const state = stateFor(window);
586
607
  if (state.nativeNav && !state.nativeNav.isDestroyed()) return;
587
608
  // Auxiliary vendor windows (for example Codex's hidden avatar overlay)
@@ -628,11 +649,10 @@ if (
628
649
  nativeNavOwners.delete(nativeNav.webContents);
629
650
  try { nativeNav.destroy(); } catch {}
630
651
  };
631
- const ownerWindow = (contents) => BrowserWindow.fromWebContents(contents)
632
- || contents.getOwnerBrowserWindow?.()
633
- || BrowserWindow.getFocusedWindow()
634
- || BrowserWindow.getAllWindows().find((window) => !window.isDestroyed())
635
- || null;
652
+ const ownerWindow = (contents) => {
653
+ const window = BrowserWindow.fromWebContents(contents) || contents.getOwnerBrowserWindow?.() || null;
654
+ return isHostWindow(window) ? window : null;
655
+ };
636
656
  const attachContents = (contents) => {
637
657
  if (!contents || contents.isDestroyed() || contents.__impelDesktopTasksAttached || internalContents.has(contents)) return;
638
658
  contents.__impelDesktopTasksAttached = true;
@@ -641,25 +661,29 @@ if (
641
661
  Promise.all((contents.mainFrame?.framesInSubtree || []).map((frame) => frame.executeJavaScript(navSource, true)))
642
662
  .then((results) => {
643
663
  const window = ownerWindow(contents);
644
- if (!window) return;
664
+ if (!window || window !== primaryHostWindow()) return;
645
665
  const state = stateFor(window);
646
666
  if (results.some((result) => result?.anchor)) {
647
667
  hasDomNavigation = true;
648
668
  state.hasDomNavigation = true;
649
669
  for (const owner of BrowserWindow.getAllWindows()) removeNativeNav(owner);
650
670
  } else if (attempt < 3) setTimeout(() => inject(attempt + 1), 1000);
651
- else if (!state.hasDomNavigation && !hasDomNavigation) ensureNativeNav();
671
+ else if (!state.hasDomNavigation && !hasDomNavigation) ensureNativeNav(window);
652
672
  })
653
673
  .catch(() => record("navigation-injection-failed"));
654
674
  };
655
675
  contents.on("did-finish-load", () => inject());
656
676
  contents.on("did-frame-finish-load", () => inject());
657
677
  if (!contents.isLoadingMainFrame()) setTimeout(() => inject(), 0);
678
+ let handledNavigation = null;
658
679
  const handleNavigation = (event, url) => {
659
680
  if (!String(url).startsWith("impel-tasks://")) return;
660
681
  event.preventDefault();
661
682
  const window = ownerWindow(contents);
662
- if (!window) return;
683
+ if (!window || window !== primaryHostWindow()) return;
684
+ if (handledNavigation === url) return;
685
+ handledNavigation = url;
686
+ queueMicrotask(() => { if (handledNavigation === url) handledNavigation = null; });
663
687
  let action;
664
688
  try { action = new URL(url).hostname; } catch { return; }
665
689
  if (action === "hide") hide(window);
@@ -699,11 +723,13 @@ if (
699
723
  });
700
724
  window.on("closed", () => {
701
725
  const state = windows.get(window.id);
702
- if (!state) return;
703
- try { if (state.view) window.contentView.removeChildView(state.view); } catch {}
704
- try { state.view?.webContents.close(); } catch {}
705
- try { state.nativeNav?.destroy(); } catch {}
726
+ if (state) {
727
+ try { if (state.view) window.contentView.removeChildView(state.view); } catch {}
728
+ try { state.view?.webContents.close(); } catch {}
729
+ try { state.nativeNav?.destroy(); } catch {}
730
+ }
706
731
  windows.delete(window.id);
732
+ if (primaryHost === window) primaryHost = null;
707
733
  });
708
734
  };
709
735
  ipcMain.on(nativeShowChannel, (event) => {
@@ -1,6 +1,6 @@
1
1
  // Fleet generation shared by managed-profile writers and upstream clients.
2
2
  // Keep this isolated from apps.js so latency-sensitive transports do not load
3
3
  // desktop bundle machinery just to identify their managed config contract.
4
- // v38 replaces the generated desktop MCP Apps host with the authenticated web
5
- // board and removes the desktop-only Codex renderer flag.
6
- export const CURRENT_CONFIG_VERSION = 38;
4
+ // v39 confines the authenticated desktop Tasks view to one canonical primary
5
+ // vendor window and forces every existing managed profile to receive the fix.
6
+ export const CURRENT_CONFIG_VERSION = 39;