impel-cli 0.20.48 → 0.20.49

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,23 @@
1
1
  # Release notes
2
2
 
3
+ ## 0.20.49 — Restore managed app startup
4
+
5
+ - Restores normal ChatGPT and Claude startup by disabling the experimental
6
+ embedded Tasks UI in managed vendor apps. The fallback's Electron window and
7
+ DOM-injection paths no longer run in production.
8
+ - Retains tenant-bound Tasks through the CLI and MCP while the desktop entry
9
+ point is redesigned against an explicitly supported vendor integration seam.
10
+ - Makes every production-generated main preload an exact inert stub. The
11
+ reviewed prototype remains test-only and cannot be enabled by a managed app
12
+ environment variable.
13
+ - Advances the managed profile contract to v40 so every existing macOS app
14
+ rewrites and reloads the corrected external Tasks preload. Vendor pins and
15
+ built bundle bytes remain unchanged.
16
+ - Adds a production-default contract proving the embedded path is inert, while
17
+ retaining deterministic coverage for the prototype behind its opt-in. The
18
+ pinned managed-app smoke also requires the normal working surface, the exact
19
+ inert preload bytes, zero injected controls, and zero Tasks web requests.
20
+
3
21
  ## 0.20.48 — Confine Tasks to the primary app window
4
22
 
5
23
  - Fixes the remaining `0.20.47` regression that let an auxiliary ChatGPT or
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "impel-cli",
3
- "version": "0.20.48",
3
+ "version": "0.20.49",
4
4
  "description": "Prepare isolated Claude and Codex workspaces for every accessible Impel tenant",
5
5
  "type": "module",
6
6
  "bin": {
@@ -103,6 +103,7 @@ const NAVIGATION_SOURCE = String.raw`(() => {
103
103
  let slot = null;
104
104
  let active = false;
105
105
  let lastAnchor = null;
106
+ let readySignalled = false;
106
107
  const roots = () => {
107
108
  const result = [document];
108
109
  for (let index = 0; index < result.length; index += 1) {
@@ -233,6 +234,10 @@ const NAVIGATION_SOURCE = String.raw`(() => {
233
234
  slot = nav;
234
235
  }
235
236
  select(active);
237
+ if (nav?.isConnected && !readySignalled) {
238
+ readySignalled = true;
239
+ setTimeout(() => signal("ready"), 0);
240
+ }
236
241
  };
237
242
  const style = document.createElement("style");
238
243
  style.textContent = "#" + NAV_ID + " svg{width:16px;height:16px;flex:0 0 auto}#" + NAV_ID + "." + ACTIVE_CLASS + "{background:color-mix(in srgb,CanvasText 10%,transparent)}#" + NAV_ID + ":focus-visible{outline:2px solid #7c8cff;outline-offset:2px}";
@@ -260,13 +265,19 @@ const NAVIGATION_SOURCE = String.raw`(() => {
260
265
  style.remove();
261
266
  delete window.__impelDesktopTasksSetActive;
262
267
  };
263
- return { anchor: lastAnchor ? label(lastAnchor) : null, connected: Boolean(nav?.isConnected) };
268
+ const shellControls = controls().filter((element) => visible(element) && Boolean(label(element)));
269
+ return {
270
+ anchor: lastAnchor ? label(lastAnchor) : null,
271
+ connected: Boolean(nav?.isConnected),
272
+ shellReady: shellControls.length >= 2,
273
+ };
264
274
  })();`;
265
275
 
266
276
  const NATIVE_NAVIGATION_HTML = `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="Content-Security-Policy" content="default-src 'none';script-src 'unsafe-inline';style-src 'unsafe-inline'"><style>:root{color-scheme:light dark;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}*{box-sizing:border-box}html,body{width:100%;height:100%;margin:0;overflow:hidden}button{width:100%;height:100%;display:flex;align-items:center;gap:9px;border:0;border-radius:8px;padding:0 12px;background:color-mix(in srgb,Canvas 92%,CanvasText 8%);color:CanvasText;cursor:pointer}svg{width:16px;height:16px}</style></head><body><button id="tasks" type="button" aria-label="Tasks">${TASKS_ICON}<span>Tasks</span></button><script>const button=document.getElementById("tasks");button.addEventListener("click",()=>{button.dataset.active="true";window.impelDesktopTasks.show()});window.__impelDesktopTasksSetActive=active=>{button.dataset.active=String(Boolean(active))}</script></body></html>`;
267
277
 
268
- export function desktopTasksMainPreload(appUrl) {
278
+ export function desktopTasksMainPreload(appUrl, { enableEmbed = false } = {}) {
269
279
  const appOrigin = desktopTasksAppOrigin(appUrl);
280
+ if (!enableEmbed) return `"use strict";\n`;
270
281
  const parserSource = parseDesktopTasksCredential.toString();
271
282
  const escapeRegexSource = escapeRegex.toString();
272
283
  return `"use strict";
@@ -364,22 +375,11 @@ if (
364
375
  return;
365
376
  }
366
377
  const windows = new Map();
367
- const internalWindows = new WeakSet();
368
378
  const internalContents = new Set();
369
379
  const nativeNavOwners = new Map();
370
380
  const configuredSessions = new WeakSet();
371
- let creatingInternalWindow = false;
372
381
  let hasDomNavigation = false;
373
382
  let primaryHost = null;
374
- const createInternalWindow = (options) => {
375
- creatingInternalWindow = true;
376
- try {
377
- const window = new BrowserWindow(options);
378
- internalWindows.add(window);
379
- return window;
380
- }
381
- finally { creatingInternalWindow = false; }
382
- };
383
383
  const stateFor = (window) => {
384
384
  let state = windows.get(window.id);
385
385
  if (!state) {
@@ -474,11 +474,13 @@ if (
474
474
  }
475
475
  };
476
476
  const isHostWindow = (window) => {
477
- if (!window || window.isDestroyed() || internalWindows.has(window) || window.getParentWindow?.()) return false;
477
+ if (!window || window.isDestroyed() || window.getParentWindow?.()) return false;
478
478
  try {
479
- const url = new URL(window.webContents.getURL());
479
+ const rawUrl = window.webContents.getURL();
480
+ if (!rawUrl || rawUrl === "about:blank") return false;
481
+ const url = new URL(rawUrl);
480
482
  if (url.searchParams.has("initialRoute")) return false;
481
- } catch {}
483
+ } catch { return false; }
482
484
  return true;
483
485
  };
484
486
  const primaryHostWindow = () => {
@@ -593,9 +595,9 @@ if (
593
595
  };
594
596
  const layoutNativeNav = (window) => {
595
597
  const state = windows.get(window.id);
596
- if (!state?.nativeNav || state.nativeNav.isDestroyed()) return;
598
+ if (!state?.nativeNav || state.nativeNav.webContents.isDestroyed()) return;
597
599
  const content = window.getContentBounds();
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 });
600
+ state.nativeNav.setBounds({ x: 12, y: Math.max(48, content.height - 92), width: Math.min(120, Math.max(80, content.width - 24)), height: 38 });
599
601
  };
600
602
  const ensureNativeNav = (probeWindow) => {
601
603
  // Renderer probes finish independently. Only the canonical host may
@@ -604,21 +606,14 @@ if (
604
606
  const window = fallbackHostWindow();
605
607
  if (!window || probeWindow !== window) return;
606
608
  const state = stateFor(window);
607
- if (state.nativeNav && !state.nativeNav.isDestroyed()) return;
609
+ if (state.nativeNav && !state.nativeNav.webContents.isDestroyed()) return;
608
610
  // Auxiliary vendor windows (for example Codex's hidden avatar overlay)
609
611
  // are also top-level BrowserWindows. Never give them their own Tasks
610
- // fallback: duplicate child windows interfere with first-run navigation
611
- // and can cover the primary shell.
612
+ // fallback: duplicate views can cover the primary shell.
612
613
  if ([...windows.values()].some((candidate) => (
613
- candidate.nativeNav && !candidate.nativeNav.isDestroyed()
614
+ candidate.nativeNav && !candidate.nativeNav.webContents.isDestroyed()
614
615
  ))) return;
615
- const nativeNav = createInternalWindow({
616
- parent: window,
617
- frame: false,
618
- show: false,
619
- resizable: false,
620
- skipTaskbar: true,
621
- backgroundColor: "#292929",
616
+ const nativeNav = new WebContentsView({
622
617
  webPreferences: {
623
618
  preload: process.env.IMPEL_DESKTOP_TASKS_VIEW_PRELOAD,
624
619
  contextIsolation: true,
@@ -629,15 +624,19 @@ if (
629
624
  state.nativeNav = nativeNav;
630
625
  internalContents.add(nativeNav.webContents);
631
626
  nativeNavOwners.set(nativeNav.webContents, window);
627
+ window.contentView.addChildView(nativeNav);
628
+ nativeNav.setVisible(false);
629
+ nativeNav.setBackgroundColor("#292929");
632
630
  nativeNav.webContents.setWindowOpenHandler(() => ({ action: "deny" }));
633
631
  nativeNav.webContents.on("will-navigate", (event, url) => { if (url !== nativeNavUrl) event.preventDefault(); });
634
632
  nativeNav.webContents.on("destroyed", () => {
635
633
  internalContents.delete(nativeNav.webContents);
636
634
  nativeNavOwners.delete(nativeNav.webContents);
635
+ if (state.nativeNav === nativeNav) state.nativeNav = null;
637
636
  });
638
637
  nativeNav.webContents.loadURL(nativeNavUrl).catch(() => record("native-navigation-load-failed"));
639
638
  layoutNativeNav(window);
640
- nativeNav.showInactive();
639
+ nativeNav.setVisible(true);
641
640
  record("native-navigation-installed");
642
641
  };
643
642
  const removeNativeNav = (window) => {
@@ -647,7 +646,13 @@ if (
647
646
  state.nativeNav = null;
648
647
  internalContents.delete(nativeNav.webContents);
649
648
  nativeNavOwners.delete(nativeNav.webContents);
650
- try { nativeNav.destroy(); } catch {}
649
+ try { window.contentView.removeChildView(nativeNav); } catch {}
650
+ try { nativeNav.webContents.close(); } catch {}
651
+ };
652
+ const markDomNavigation = (window) => {
653
+ hasDomNavigation = true;
654
+ stateFor(window).hasDomNavigation = true;
655
+ for (const owner of BrowserWindow.getAllWindows()) removeNativeNav(owner);
651
656
  };
652
657
  const ownerWindow = (contents) => {
653
658
  const window = BrowserWindow.fromWebContents(contents) || contents.getOwnerBrowserWindow?.() || null;
@@ -663,12 +668,12 @@ if (
663
668
  const window = ownerWindow(contents);
664
669
  if (!window || window !== primaryHostWindow()) return;
665
670
  const state = stateFor(window);
671
+ const shellReady = results.some((result) => result?.shellReady);
666
672
  if (results.some((result) => result?.anchor)) {
667
- hasDomNavigation = true;
668
- state.hasDomNavigation = true;
669
- for (const owner of BrowserWindow.getAllWindows()) removeNativeNav(owner);
670
- } else if (attempt < 3) setTimeout(() => inject(attempt + 1), 1000);
671
- else if (!state.hasDomNavigation && !hasDomNavigation) ensureNativeNav(window);
673
+ markDomNavigation(window);
674
+ } else if (!shellReady && attempt < 30) setTimeout(() => inject(attempt + 1), 1000);
675
+ else if (shellReady && !state.hasDomNavigation && !hasDomNavigation) ensureNativeNav(window);
676
+ else if (!shellReady) record("navigation-shell-unready");
672
677
  })
673
678
  .catch(() => record("navigation-injection-failed"));
674
679
  };
@@ -686,7 +691,8 @@ if (
686
691
  queueMicrotask(() => { if (handledNavigation === url) handledNavigation = null; });
687
692
  let action;
688
693
  try { action = new URL(url).hostname; } catch { return; }
689
- if (action === "hide") hide(window);
694
+ if (action === "ready") markDomNavigation(window);
695
+ else if (action === "hide") hide(window);
690
696
  else if (action === "show") show(window, parseBounds(url, window));
691
697
  else if (action === "layout") layoutBoard(window, parseBounds(url, window));
692
698
  };
@@ -694,31 +700,30 @@ if (
694
700
  contents.on("will-frame-navigate", handleNavigation);
695
701
  };
696
702
  const attachWindow = (window) => {
697
- if (!window || window.isDestroyed() || creatingInternalWindow || window.getParentWindow?.() || window.__impelDesktopTasksWindowAttached) return;
703
+ if (!window || window.isDestroyed() || window.getParentWindow?.() || window.__impelDesktopTasksWindowAttached) return;
698
704
  window.__impelDesktopTasksWindowAttached = true;
699
705
  attachContents(window.webContents);
700
706
  window.on("resize", () => { layoutNativeNav(window); layoutBoard(window); });
701
707
  window.on("focus", () => {
702
708
  const state = windows.get(window.id);
703
- state?.nativeNav?.showInactive();
709
+ state?.nativeNav?.setVisible(true);
704
710
  if (state?.visible) state.view?.setVisible(true);
705
711
  });
706
712
  window.on("blur", () => setTimeout(() => {
707
713
  const state = windows.get(window.id);
708
- if (BrowserWindow.getFocusedWindow() === state?.nativeNav) return;
709
714
  // Keep the only path to Tasks available while the app is frontmost.
710
- // BrowserWindow blur also fires when a user clicks the child fallback;
711
- // hiding it here made the button disappear before its click arrived.
712
- if (state?.nativeNav && (!window.isVisible() || window.isMinimized())) state.nativeNav.hide();
715
+ // Moving focus into the fallback view must not hide it before the click
716
+ // arrives; only a hidden or minimized host hides the affordance.
717
+ if (state?.nativeNav && (!window.isVisible() || window.isMinimized())) state.nativeNav.setVisible(false);
713
718
  }, 150));
714
719
  window.on("minimize", () => {
715
720
  const state = windows.get(window.id);
716
- state?.nativeNav?.hide();
721
+ state?.nativeNav?.setVisible(false);
717
722
  state?.view?.setVisible(false);
718
723
  });
719
724
  window.on("restore", () => {
720
725
  const state = windows.get(window.id);
721
- state?.nativeNav?.showInactive();
726
+ state?.nativeNav?.setVisible(true);
722
727
  if (state?.visible) state.view?.setVisible(true);
723
728
  });
724
729
  window.on("closed", () => {
@@ -726,7 +731,8 @@ if (
726
731
  if (state) {
727
732
  try { if (state.view) window.contentView.removeChildView(state.view); } catch {}
728
733
  try { state.view?.webContents.close(); } catch {}
729
- try { state.nativeNav?.destroy(); } catch {}
734
+ try { if (state.nativeNav) window.contentView.removeChildView(state.nativeNav); } catch {}
735
+ try { state.nativeNav?.webContents.close(); } catch {}
730
736
  }
731
737
  windows.delete(window.id);
732
738
  if (primaryHost === window) primaryHost = null;
@@ -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
- // 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;
4
+ // v40 renders the Tasks fallback inside the vendor window and waits for the app
5
+ // shell, so no child BrowserWindow can pin ChatGPT's transient startup splash.
6
+ export const CURRENT_CONFIG_VERSION = 40;