impel-cli 0.20.47 → 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 +37 -0
- package/package.json +1 -1
- package/src/desktopTasks.js +94 -62
- package/src/managedProfileVersion.js +3 -3
package/RELEASE_NOTES.md
CHANGED
|
@@ -1,5 +1,42 @@
|
|
|
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
|
+
|
|
21
|
+
## 0.20.48 — Confine Tasks to the primary app window
|
|
22
|
+
|
|
23
|
+
- Fixes the remaining `0.20.47` regression that let an auxiliary ChatGPT or
|
|
24
|
+
Claude renderer attach a second authenticated Tasks board in a large framed
|
|
25
|
+
window, obscuring the normal app.
|
|
26
|
+
- Binds Tasks once to the canonical primary vendor window, rejects child,
|
|
27
|
+
internal, auxiliary, and unowned renderers fail-closed, and removes focused-
|
|
28
|
+
window guessing from navigation ownership.
|
|
29
|
+
- Couples fallback installation to the canonical renderer and deduplicates
|
|
30
|
+
paired Electron navigation events so one user action creates at most one
|
|
31
|
+
board view.
|
|
32
|
+
- Advances the managed profile contract to v39 so every existing install
|
|
33
|
+
rewrites and reloads the corrected external Tasks preload. Vendor pins and
|
|
34
|
+
built bundle bytes remain unchanged.
|
|
35
|
+
- Adds a behavioral reproduction of the production screenshot: an auxiliary
|
|
36
|
+
renderer fires both navigation events before the fallback is clicked, while
|
|
37
|
+
the test proves exactly one board attaches to the primary window and the
|
|
38
|
+
fallback remains a frameless 120×38 control.
|
|
39
|
+
|
|
3
40
|
## 0.20.47 — Keep Tasks on the primary app window
|
|
4
41
|
|
|
5
42
|
- Makes the native Tasks fallback choose the earliest surviving primary host
|
package/package.json
CHANGED
package/src/desktopTasks.js
CHANGED
|
@@ -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
|
-
|
|
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";
|
|
@@ -367,13 +378,8 @@ if (
|
|
|
367
378
|
const internalContents = new Set();
|
|
368
379
|
const nativeNavOwners = new Map();
|
|
369
380
|
const configuredSessions = new WeakSet();
|
|
370
|
-
let creatingInternalWindow = false;
|
|
371
381
|
let hasDomNavigation = false;
|
|
372
|
-
|
|
373
|
-
creatingInternalWindow = true;
|
|
374
|
-
try { return new BrowserWindow(options); }
|
|
375
|
-
finally { creatingInternalWindow = false; }
|
|
376
|
-
};
|
|
382
|
+
let primaryHost = null;
|
|
377
383
|
const stateFor = (window) => {
|
|
378
384
|
let state = windows.get(window.id);
|
|
379
385
|
if (!state) {
|
|
@@ -467,7 +473,25 @@ if (
|
|
|
467
473
|
}
|
|
468
474
|
}
|
|
469
475
|
};
|
|
476
|
+
const isHostWindow = (window) => {
|
|
477
|
+
if (!window || window.isDestroyed() || window.getParentWindow?.()) return false;
|
|
478
|
+
try {
|
|
479
|
+
const rawUrl = window.webContents.getURL();
|
|
480
|
+
if (!rawUrl || rawUrl === "about:blank") return false;
|
|
481
|
+
const url = new URL(rawUrl);
|
|
482
|
+
if (url.searchParams.has("initialRoute")) return false;
|
|
483
|
+
} catch { return false; }
|
|
484
|
+
return true;
|
|
485
|
+
};
|
|
486
|
+
const primaryHostWindow = () => {
|
|
487
|
+
if (isHostWindow(primaryHost)) return primaryHost;
|
|
488
|
+
primaryHost = BrowserWindow.getAllWindows()
|
|
489
|
+
.filter(isHostWindow)
|
|
490
|
+
.sort((left, right) => left.id - right.id)[0] || null;
|
|
491
|
+
return primaryHost;
|
|
492
|
+
};
|
|
470
493
|
const hide = (window) => {
|
|
494
|
+
if (window !== primaryHostWindow()) return;
|
|
471
495
|
const state = windows.get(window.id);
|
|
472
496
|
if (!state) return;
|
|
473
497
|
state.visible = false;
|
|
@@ -476,17 +500,14 @@ if (
|
|
|
476
500
|
deactivateNavigation();
|
|
477
501
|
};
|
|
478
502
|
const isPrimaryWindow = (window) => {
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
return window.isVisible?.() !== false;
|
|
503
|
+
return isHostWindow(window) && window === primaryHostWindow() && window.isVisible?.() !== false;
|
|
504
|
+
};
|
|
505
|
+
const fallbackHostWindow = () => {
|
|
506
|
+
const window = primaryHostWindow();
|
|
507
|
+
return isPrimaryWindow(window) ? window : null;
|
|
485
508
|
};
|
|
486
|
-
const fallbackHostWindow = () => BrowserWindow.getAllWindows()
|
|
487
|
-
.filter(isPrimaryWindow)
|
|
488
|
-
.sort((left, right) => left.id - right.id)[0] || null;
|
|
489
509
|
const createView = (window) => {
|
|
510
|
+
if (!window || window !== primaryHostWindow() || !isHostWindow(window)) return null;
|
|
490
511
|
const state = stateFor(window);
|
|
491
512
|
if (state.view && !state.view.webContents.isDestroyed()) return state;
|
|
492
513
|
const view = new WebContentsView({
|
|
@@ -532,6 +553,7 @@ if (
|
|
|
532
553
|
};
|
|
533
554
|
const show = (window, bounds) => {
|
|
534
555
|
const state = createView(window);
|
|
556
|
+
if (!state) return;
|
|
535
557
|
state.bounds = bounds;
|
|
536
558
|
state.view.setBounds(bounds);
|
|
537
559
|
state.view.setVisible(true);
|
|
@@ -542,6 +564,7 @@ if (
|
|
|
542
564
|
record("board-shown");
|
|
543
565
|
};
|
|
544
566
|
const layoutBoard = (window, bounds = null) => {
|
|
567
|
+
if (window !== primaryHostWindow()) return;
|
|
545
568
|
const state = windows.get(window.id);
|
|
546
569
|
if (!state?.visible || !state.view || state.view.webContents.isDestroyed()) return;
|
|
547
570
|
if (bounds) state.bounds = bounds;
|
|
@@ -572,32 +595,25 @@ if (
|
|
|
572
595
|
};
|
|
573
596
|
const layoutNativeNav = (window) => {
|
|
574
597
|
const state = windows.get(window.id);
|
|
575
|
-
if (!state?.nativeNav || state.nativeNav.isDestroyed()) return;
|
|
598
|
+
if (!state?.nativeNav || state.nativeNav.webContents.isDestroyed()) return;
|
|
576
599
|
const content = window.getContentBounds();
|
|
577
|
-
state.nativeNav.setBounds({ x:
|
|
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 });
|
|
578
601
|
};
|
|
579
|
-
const ensureNativeNav = () => {
|
|
580
|
-
// Renderer probes finish independently.
|
|
581
|
-
//
|
|
582
|
-
//
|
|
602
|
+
const ensureNativeNav = (probeWindow) => {
|
|
603
|
+
// Renderer probes finish independently. Only the canonical host may
|
|
604
|
+
// install the singleton fallback; an auxiliary renderer cannot redirect
|
|
605
|
+
// it or acquire its own authenticated board view.
|
|
583
606
|
const window = fallbackHostWindow();
|
|
584
|
-
if (!window) return;
|
|
607
|
+
if (!window || probeWindow !== window) return;
|
|
585
608
|
const state = stateFor(window);
|
|
586
|
-
if (state.nativeNav && !state.nativeNav.isDestroyed()) return;
|
|
609
|
+
if (state.nativeNav && !state.nativeNav.webContents.isDestroyed()) return;
|
|
587
610
|
// Auxiliary vendor windows (for example Codex's hidden avatar overlay)
|
|
588
611
|
// are also top-level BrowserWindows. Never give them their own Tasks
|
|
589
|
-
// fallback: duplicate
|
|
590
|
-
// and can cover the primary shell.
|
|
612
|
+
// fallback: duplicate views can cover the primary shell.
|
|
591
613
|
if ([...windows.values()].some((candidate) => (
|
|
592
|
-
candidate.nativeNav && !candidate.nativeNav.isDestroyed()
|
|
614
|
+
candidate.nativeNav && !candidate.nativeNav.webContents.isDestroyed()
|
|
593
615
|
))) return;
|
|
594
|
-
const nativeNav =
|
|
595
|
-
parent: window,
|
|
596
|
-
frame: false,
|
|
597
|
-
show: false,
|
|
598
|
-
resizable: false,
|
|
599
|
-
skipTaskbar: true,
|
|
600
|
-
backgroundColor: "#292929",
|
|
616
|
+
const nativeNav = new WebContentsView({
|
|
601
617
|
webPreferences: {
|
|
602
618
|
preload: process.env.IMPEL_DESKTOP_TASKS_VIEW_PRELOAD,
|
|
603
619
|
contextIsolation: true,
|
|
@@ -608,15 +624,19 @@ if (
|
|
|
608
624
|
state.nativeNav = nativeNav;
|
|
609
625
|
internalContents.add(nativeNav.webContents);
|
|
610
626
|
nativeNavOwners.set(nativeNav.webContents, window);
|
|
627
|
+
window.contentView.addChildView(nativeNav);
|
|
628
|
+
nativeNav.setVisible(false);
|
|
629
|
+
nativeNav.setBackgroundColor("#292929");
|
|
611
630
|
nativeNav.webContents.setWindowOpenHandler(() => ({ action: "deny" }));
|
|
612
631
|
nativeNav.webContents.on("will-navigate", (event, url) => { if (url !== nativeNavUrl) event.preventDefault(); });
|
|
613
632
|
nativeNav.webContents.on("destroyed", () => {
|
|
614
633
|
internalContents.delete(nativeNav.webContents);
|
|
615
634
|
nativeNavOwners.delete(nativeNav.webContents);
|
|
635
|
+
if (state.nativeNav === nativeNav) state.nativeNav = null;
|
|
616
636
|
});
|
|
617
637
|
nativeNav.webContents.loadURL(nativeNavUrl).catch(() => record("native-navigation-load-failed"));
|
|
618
638
|
layoutNativeNav(window);
|
|
619
|
-
nativeNav.
|
|
639
|
+
nativeNav.setVisible(true);
|
|
620
640
|
record("native-navigation-installed");
|
|
621
641
|
};
|
|
622
642
|
const removeNativeNav = (window) => {
|
|
@@ -626,13 +646,18 @@ if (
|
|
|
626
646
|
state.nativeNav = null;
|
|
627
647
|
internalContents.delete(nativeNav.webContents);
|
|
628
648
|
nativeNavOwners.delete(nativeNav.webContents);
|
|
629
|
-
try {
|
|
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);
|
|
656
|
+
};
|
|
657
|
+
const ownerWindow = (contents) => {
|
|
658
|
+
const window = BrowserWindow.fromWebContents(contents) || contents.getOwnerBrowserWindow?.() || null;
|
|
659
|
+
return isHostWindow(window) ? window : null;
|
|
630
660
|
};
|
|
631
|
-
const ownerWindow = (contents) => BrowserWindow.fromWebContents(contents)
|
|
632
|
-
|| contents.getOwnerBrowserWindow?.()
|
|
633
|
-
|| BrowserWindow.getFocusedWindow()
|
|
634
|
-
|| BrowserWindow.getAllWindows().find((window) => !window.isDestroyed())
|
|
635
|
-
|| null;
|
|
636
661
|
const attachContents = (contents) => {
|
|
637
662
|
if (!contents || contents.isDestroyed() || contents.__impelDesktopTasksAttached || internalContents.has(contents)) return;
|
|
638
663
|
contents.__impelDesktopTasksAttached = true;
|
|
@@ -641,28 +666,33 @@ if (
|
|
|
641
666
|
Promise.all((contents.mainFrame?.framesInSubtree || []).map((frame) => frame.executeJavaScript(navSource, true)))
|
|
642
667
|
.then((results) => {
|
|
643
668
|
const window = ownerWindow(contents);
|
|
644
|
-
if (!window) return;
|
|
669
|
+
if (!window || window !== primaryHostWindow()) return;
|
|
645
670
|
const state = stateFor(window);
|
|
671
|
+
const shellReady = results.some((result) => result?.shellReady);
|
|
646
672
|
if (results.some((result) => result?.anchor)) {
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
else if (!state.hasDomNavigation && !hasDomNavigation) ensureNativeNav();
|
|
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");
|
|
652
677
|
})
|
|
653
678
|
.catch(() => record("navigation-injection-failed"));
|
|
654
679
|
};
|
|
655
680
|
contents.on("did-finish-load", () => inject());
|
|
656
681
|
contents.on("did-frame-finish-load", () => inject());
|
|
657
682
|
if (!contents.isLoadingMainFrame()) setTimeout(() => inject(), 0);
|
|
683
|
+
let handledNavigation = null;
|
|
658
684
|
const handleNavigation = (event, url) => {
|
|
659
685
|
if (!String(url).startsWith("impel-tasks://")) return;
|
|
660
686
|
event.preventDefault();
|
|
661
687
|
const window = ownerWindow(contents);
|
|
662
|
-
if (!window) return;
|
|
688
|
+
if (!window || window !== primaryHostWindow()) return;
|
|
689
|
+
if (handledNavigation === url) return;
|
|
690
|
+
handledNavigation = url;
|
|
691
|
+
queueMicrotask(() => { if (handledNavigation === url) handledNavigation = null; });
|
|
663
692
|
let action;
|
|
664
693
|
try { action = new URL(url).hostname; } catch { return; }
|
|
665
|
-
if (action === "
|
|
694
|
+
if (action === "ready") markDomNavigation(window);
|
|
695
|
+
else if (action === "hide") hide(window);
|
|
666
696
|
else if (action === "show") show(window, parseBounds(url, window));
|
|
667
697
|
else if (action === "layout") layoutBoard(window, parseBounds(url, window));
|
|
668
698
|
};
|
|
@@ -670,40 +700,42 @@ if (
|
|
|
670
700
|
contents.on("will-frame-navigate", handleNavigation);
|
|
671
701
|
};
|
|
672
702
|
const attachWindow = (window) => {
|
|
673
|
-
if (!window || window.isDestroyed() ||
|
|
703
|
+
if (!window || window.isDestroyed() || window.getParentWindow?.() || window.__impelDesktopTasksWindowAttached) return;
|
|
674
704
|
window.__impelDesktopTasksWindowAttached = true;
|
|
675
705
|
attachContents(window.webContents);
|
|
676
706
|
window.on("resize", () => { layoutNativeNav(window); layoutBoard(window); });
|
|
677
707
|
window.on("focus", () => {
|
|
678
708
|
const state = windows.get(window.id);
|
|
679
|
-
state?.nativeNav?.
|
|
709
|
+
state?.nativeNav?.setVisible(true);
|
|
680
710
|
if (state?.visible) state.view?.setVisible(true);
|
|
681
711
|
});
|
|
682
712
|
window.on("blur", () => setTimeout(() => {
|
|
683
713
|
const state = windows.get(window.id);
|
|
684
|
-
if (BrowserWindow.getFocusedWindow() === state?.nativeNav) return;
|
|
685
714
|
// Keep the only path to Tasks available while the app is frontmost.
|
|
686
|
-
//
|
|
687
|
-
//
|
|
688
|
-
if (state?.nativeNav && (!window.isVisible() || window.isMinimized())) state.nativeNav.
|
|
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);
|
|
689
718
|
}, 150));
|
|
690
719
|
window.on("minimize", () => {
|
|
691
720
|
const state = windows.get(window.id);
|
|
692
|
-
state?.nativeNav?.
|
|
721
|
+
state?.nativeNav?.setVisible(false);
|
|
693
722
|
state?.view?.setVisible(false);
|
|
694
723
|
});
|
|
695
724
|
window.on("restore", () => {
|
|
696
725
|
const state = windows.get(window.id);
|
|
697
|
-
state?.nativeNav?.
|
|
726
|
+
state?.nativeNav?.setVisible(true);
|
|
698
727
|
if (state?.visible) state.view?.setVisible(true);
|
|
699
728
|
});
|
|
700
729
|
window.on("closed", () => {
|
|
701
730
|
const state = windows.get(window.id);
|
|
702
|
-
if (
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
731
|
+
if (state) {
|
|
732
|
+
try { if (state.view) window.contentView.removeChildView(state.view); } catch {}
|
|
733
|
+
try { state.view?.webContents.close(); } catch {}
|
|
734
|
+
try { if (state.nativeNav) window.contentView.removeChildView(state.nativeNav); } catch {}
|
|
735
|
+
try { state.nativeNav?.webContents.close(); } catch {}
|
|
736
|
+
}
|
|
706
737
|
windows.delete(window.id);
|
|
738
|
+
if (primaryHost === window) primaryHost = null;
|
|
707
739
|
});
|
|
708
740
|
};
|
|
709
741
|
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
|
-
//
|
|
5
|
-
//
|
|
6
|
-
export const CURRENT_CONFIG_VERSION =
|
|
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;
|