impel-cli 0.20.46 → 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,38 @@
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
+
22
+ ## 0.20.47 — Keep Tasks on the primary app window
23
+
24
+ - Makes the native Tasks fallback choose the earliest surviving primary host
25
+ window deterministically, so a later ChatGPT auxiliary renderer cannot win a
26
+ probe race and cover the real app with an empty grey window.
27
+ - Adds a behavioral regression that returns the auxiliary window first, lets it
28
+ finish navigation probing first, and proves the fallback and authenticated
29
+ board still attach only to the primary window.
30
+ - Skips the prerelease soak because this is an immediate stable regression
31
+ hotfix for `0.20.46`. The change updates the external Tasks preload
32
+ orchestration without changing vendor pins, generated profile contracts, or
33
+ built bundle bytes; the full release CI, pinned managed-app launch smoke, and
34
+ Windows vendor contracts remain mandatory before tagging.
35
+
3
36
  ## 0.20.46-beta.2 — Reload reconciled apps safely
4
37
 
5
38
  - Closes every running managed macOS app before rewriting its profile and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "impel-cli",
3
- "version": "0.20.46",
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,14 +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
507
  const createView = (window) => {
508
+ if (!window || window !== primaryHostWindow() || !isHostWindow(window)) return null;
487
509
  const state = stateFor(window);
488
510
  if (state.view && !state.view.webContents.isDestroyed()) return state;
489
511
  const view = new WebContentsView({
@@ -529,6 +551,7 @@ if (
529
551
  };
530
552
  const show = (window, bounds) => {
531
553
  const state = createView(window);
554
+ if (!state) return;
532
555
  state.bounds = bounds;
533
556
  state.view.setBounds(bounds);
534
557
  state.view.setVisible(true);
@@ -539,6 +562,7 @@ if (
539
562
  record("board-shown");
540
563
  };
541
564
  const layoutBoard = (window, bounds = null) => {
565
+ if (window !== primaryHostWindow()) return;
542
566
  const state = windows.get(window.id);
543
567
  if (!state?.visible || !state.view || state.view.webContents.isDestroyed()) return;
544
568
  if (bounds) state.bounds = bounds;
@@ -573,8 +597,12 @@ if (
573
597
  const content = window.getContentBounds();
574
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 });
575
599
  };
576
- const ensureNativeNav = (window) => {
577
- if (!isPrimaryWindow(window)) return;
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.
604
+ const window = fallbackHostWindow();
605
+ if (!window || probeWindow !== window) return;
578
606
  const state = stateFor(window);
579
607
  if (state.nativeNav && !state.nativeNav.isDestroyed()) return;
580
608
  // Auxiliary vendor windows (for example Codex's hidden avatar overlay)
@@ -621,11 +649,10 @@ if (
621
649
  nativeNavOwners.delete(nativeNav.webContents);
622
650
  try { nativeNav.destroy(); } catch {}
623
651
  };
624
- const ownerWindow = (contents) => BrowserWindow.fromWebContents(contents)
625
- || contents.getOwnerBrowserWindow?.()
626
- || BrowserWindow.getFocusedWindow()
627
- || BrowserWindow.getAllWindows().find((window) => !window.isDestroyed())
628
- || null;
652
+ const ownerWindow = (contents) => {
653
+ const window = BrowserWindow.fromWebContents(contents) || contents.getOwnerBrowserWindow?.() || null;
654
+ return isHostWindow(window) ? window : null;
655
+ };
629
656
  const attachContents = (contents) => {
630
657
  if (!contents || contents.isDestroyed() || contents.__impelDesktopTasksAttached || internalContents.has(contents)) return;
631
658
  contents.__impelDesktopTasksAttached = true;
@@ -634,7 +661,7 @@ if (
634
661
  Promise.all((contents.mainFrame?.framesInSubtree || []).map((frame) => frame.executeJavaScript(navSource, true)))
635
662
  .then((results) => {
636
663
  const window = ownerWindow(contents);
637
- if (!window) return;
664
+ if (!window || window !== primaryHostWindow()) return;
638
665
  const state = stateFor(window);
639
666
  if (results.some((result) => result?.anchor)) {
640
667
  hasDomNavigation = true;
@@ -648,11 +675,15 @@ if (
648
675
  contents.on("did-finish-load", () => inject());
649
676
  contents.on("did-frame-finish-load", () => inject());
650
677
  if (!contents.isLoadingMainFrame()) setTimeout(() => inject(), 0);
678
+ let handledNavigation = null;
651
679
  const handleNavigation = (event, url) => {
652
680
  if (!String(url).startsWith("impel-tasks://")) return;
653
681
  event.preventDefault();
654
682
  const window = ownerWindow(contents);
655
- if (!window) return;
683
+ if (!window || window !== primaryHostWindow()) return;
684
+ if (handledNavigation === url) return;
685
+ handledNavigation = url;
686
+ queueMicrotask(() => { if (handledNavigation === url) handledNavigation = null; });
656
687
  let action;
657
688
  try { action = new URL(url).hostname; } catch { return; }
658
689
  if (action === "hide") hide(window);
@@ -692,11 +723,13 @@ if (
692
723
  });
693
724
  window.on("closed", () => {
694
725
  const state = windows.get(window.id);
695
- if (!state) return;
696
- try { if (state.view) window.contentView.removeChildView(state.view); } catch {}
697
- try { state.view?.webContents.close(); } catch {}
698
- 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
+ }
699
731
  windows.delete(window.id);
732
+ if (primaryHost === window) primaryHost = null;
700
733
  });
701
734
  };
702
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;