impel-cli 0.20.48 → 0.20.50
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 +40 -0
- package/package.json +1 -1
- package/src/apps.js +6 -2
- package/src/desktopTasks.js +172 -72
- package/src/managedProfileVersion.js +5 -3
package/RELEASE_NOTES.md
CHANGED
|
@@ -1,5 +1,45 @@
|
|
|
1
1
|
# Release notes
|
|
2
2
|
|
|
3
|
+
## 0.20.50 — Restore ChatGPT Tasks safely
|
|
4
|
+
|
|
5
|
+
- Restores one embedded Tasks entry in the managed ChatGPT sidebar while
|
|
6
|
+
keeping Claude's production preload inert and preserving normal ChatGPT
|
|
7
|
+
startup and navigation.
|
|
8
|
+
- Disables the native fallback in the production ChatGPT profile, so Tasks
|
|
9
|
+
cannot create a second control view or cover the vendor app during startup.
|
|
10
|
+
- Debounces sidebar discovery, makes attached-control checks constant-time,
|
|
11
|
+
and removes the outer reinjection retry chain that could repeatedly scan the
|
|
12
|
+
renderer while its sidebar anchor was late.
|
|
13
|
+
- Injects and accepts Tasks navigation only in the main vendor frame; subframes
|
|
14
|
+
cannot open, resize, or bootstrap the authenticated board.
|
|
15
|
+
- Deduplicates paired Electron navigation events and reuses the loaded board,
|
|
16
|
+
keeping one tenant-bound credential exchange across ordinary hide/reopen
|
|
17
|
+
cycles while allowing an expired or failed board session to recover.
|
|
18
|
+
- Advances the managed profile contract to v41 so existing macOS ChatGPT apps
|
|
19
|
+
rewrite and reload the repaired preload without rebuilding vendor bundles.
|
|
20
|
+
- Adds a known-bad `0.20.48` control and a real packaged-app gate with screenshot
|
|
21
|
+
artifacts for the normal surface, one Tasks tab, and the interactive board;
|
|
22
|
+
it also externally observes the board target becoming compositor-hidden when
|
|
23
|
+
host navigation is restored, with zero fallback controls and one bootstrap.
|
|
24
|
+
|
|
25
|
+
## 0.20.49 — Restore managed app startup
|
|
26
|
+
|
|
27
|
+
- Restores normal ChatGPT and Claude startup by disabling the experimental
|
|
28
|
+
embedded Tasks UI in managed vendor apps. The fallback's Electron window and
|
|
29
|
+
DOM-injection paths no longer run in production.
|
|
30
|
+
- Retains tenant-bound Tasks through the CLI and MCP while the desktop entry
|
|
31
|
+
point is redesigned against an explicitly supported vendor integration seam.
|
|
32
|
+
- Makes every production-generated main preload an exact inert stub. The
|
|
33
|
+
reviewed prototype remains test-only and cannot be enabled by a managed app
|
|
34
|
+
environment variable.
|
|
35
|
+
- Advances the managed profile contract to v40 so every existing macOS app
|
|
36
|
+
rewrites and reloads the corrected external Tasks preload. Vendor pins and
|
|
37
|
+
built bundle bytes remain unchanged.
|
|
38
|
+
- Adds a production-default contract proving the embedded path is inert, while
|
|
39
|
+
retaining deterministic coverage for the prototype behind its opt-in. The
|
|
40
|
+
pinned managed-app smoke also requires the normal working surface, the exact
|
|
41
|
+
inert preload bytes, zero injected controls, and zero Tasks web requests.
|
|
42
|
+
|
|
3
43
|
## 0.20.48 — Confine Tasks to the primary app window
|
|
4
44
|
|
|
5
45
|
- Fixes the remaining `0.20.47` regression that let an auxiliary ChatGPT or
|
package/package.json
CHANGED
package/src/apps.js
CHANGED
|
@@ -660,9 +660,12 @@ export function appPaths(homeDir = os.homedir(), tenantId = null, {
|
|
|
660
660
|
};
|
|
661
661
|
}
|
|
662
662
|
|
|
663
|
-
function writeDesktopTasksProfileAssets(root, appUrl) {
|
|
663
|
+
function writeDesktopTasksProfileAssets(root, appUrl, { enableEmbed = false } = {}) {
|
|
664
664
|
const assets = desktopTasksAssetPaths(root);
|
|
665
|
-
writeAtomic(assets.mainPreload, desktopTasksMainPreload(appUrl
|
|
665
|
+
writeAtomic(assets.mainPreload, desktopTasksMainPreload(appUrl, {
|
|
666
|
+
enableEmbed,
|
|
667
|
+
enableNativeFallback: false,
|
|
668
|
+
}), 0o600);
|
|
666
669
|
writeAtomic(assets.viewPreload, desktopTasksViewPreload(), 0o600);
|
|
667
670
|
// This exact path is Impel-owned. Remove the former local MCP Apps host when
|
|
668
671
|
// a profile is regenerated; old released clients keep the immutable server
|
|
@@ -879,6 +882,7 @@ export function installManagedAppFiles({
|
|
|
879
882
|
writeDesktopTasksProfileAssets(
|
|
880
883
|
paths[target].root,
|
|
881
884
|
config.appUrl || RUNTIME_BRAND.controlPlane.defaultOrigin,
|
|
885
|
+
{ enableEmbed: target === "chatgpt" },
|
|
882
886
|
);
|
|
883
887
|
// Non-bundle targets are the background-refresh / fast-open path: configs,
|
|
884
888
|
// token helper, catalog, and manifest only. Bundle swaps require the app
|
package/src/desktopTasks.js
CHANGED
|
@@ -87,6 +87,7 @@ export function parseDesktopTasksCredential(output, tenantId, {
|
|
|
87
87
|
|
|
88
88
|
const NAV_ANCHOR_RULES_SOURCE = JSON.stringify(DESKTOP_TASKS_NAV_ANCHOR_RULES);
|
|
89
89
|
const TASKS_ICON = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="3" y="3" width="18" height="18" rx="2"></rect><path d="M9 3v18"></path><path d="M15 3v18"></path></svg>';
|
|
90
|
+
const NAVIGATION_NONCE_PLACEHOLDER = "IMPEL_DESKTOP_TASKS_NAVIGATION_NONCE_PLACEHOLDER";
|
|
90
91
|
|
|
91
92
|
const NAVIGATION_SOURCE = String.raw`(() => {
|
|
92
93
|
const NAV_ID = "impel-desktop-tasks-nav";
|
|
@@ -103,6 +104,10 @@ const NAVIGATION_SOURCE = String.raw`(() => {
|
|
|
103
104
|
let slot = null;
|
|
104
105
|
let active = false;
|
|
105
106
|
let lastAnchor = null;
|
|
107
|
+
let readySignalled = false;
|
|
108
|
+
let ensureTimer = null;
|
|
109
|
+
const signalSequenceKey = "__impelDesktopTasksSignalSequence";
|
|
110
|
+
const navigationNonce = ${JSON.stringify(NAVIGATION_NONCE_PLACEHOLDER)};
|
|
106
111
|
const roots = () => {
|
|
107
112
|
const result = [document];
|
|
108
113
|
for (let index = 0; index < result.length; index += 1) {
|
|
@@ -156,6 +161,10 @@ const NAVIGATION_SOURCE = String.raw`(() => {
|
|
|
156
161
|
};
|
|
157
162
|
const signal = (action) => {
|
|
158
163
|
const query = new URLSearchParams(Object.entries(mainBounds()).map(([key, value]) => [key, String(value)]));
|
|
164
|
+
const signalSequence = Number(window[signalSequenceKey] || 0) + 1;
|
|
165
|
+
window[signalSequenceKey] = signalSequence;
|
|
166
|
+
query.set("event", Date.now().toString(36) + "-" + signalSequence.toString(36));
|
|
167
|
+
query.set("nonce", navigationNonce);
|
|
159
168
|
location.href = "impel-tasks://" + action + "?" + query;
|
|
160
169
|
};
|
|
161
170
|
const select = (selected) => {
|
|
@@ -209,6 +218,10 @@ const NAVIGATION_SOURCE = String.raw`(() => {
|
|
|
209
218
|
return button;
|
|
210
219
|
};
|
|
211
220
|
const ensureNav = () => {
|
|
221
|
+
if (nav?.isConnected) {
|
|
222
|
+
select(active);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
212
225
|
let rule = null;
|
|
213
226
|
let anchor = null;
|
|
214
227
|
for (const candidate of RULES) {
|
|
@@ -233,6 +246,10 @@ const NAVIGATION_SOURCE = String.raw`(() => {
|
|
|
233
246
|
slot = nav;
|
|
234
247
|
}
|
|
235
248
|
select(active);
|
|
249
|
+
if (nav?.isConnected && !readySignalled) {
|
|
250
|
+
readySignalled = true;
|
|
251
|
+
setTimeout(() => signal("ready"), 0);
|
|
252
|
+
}
|
|
236
253
|
};
|
|
237
254
|
const style = document.createElement("style");
|
|
238
255
|
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}";
|
|
@@ -248,11 +265,19 @@ const NAVIGATION_SOURCE = String.raw`(() => {
|
|
|
248
265
|
const onResize = () => { if (active) signal("layout"); };
|
|
249
266
|
window.addEventListener("click", onClick, true);
|
|
250
267
|
window.addEventListener("resize", onResize);
|
|
251
|
-
const
|
|
268
|
+
const scheduleEnsure = () => {
|
|
269
|
+
if (ensureTimer !== null) return;
|
|
270
|
+
ensureTimer = setTimeout(() => {
|
|
271
|
+
ensureTimer = null;
|
|
272
|
+
ensureNav();
|
|
273
|
+
}, 250);
|
|
274
|
+
};
|
|
275
|
+
const observer = new MutationObserver(scheduleEnsure);
|
|
252
276
|
observer.observe(document.body, { childList: true, subtree: true });
|
|
253
277
|
ensureNav();
|
|
254
278
|
window.__impelDesktopTasksCleanup = () => {
|
|
255
279
|
observer.disconnect();
|
|
280
|
+
if (ensureTimer !== null) clearTimeout(ensureTimer);
|
|
256
281
|
window.removeEventListener("click", onClick, true);
|
|
257
282
|
window.removeEventListener("resize", onResize);
|
|
258
283
|
nav?.remove();
|
|
@@ -260,13 +285,22 @@ const NAVIGATION_SOURCE = String.raw`(() => {
|
|
|
260
285
|
style.remove();
|
|
261
286
|
delete window.__impelDesktopTasksSetActive;
|
|
262
287
|
};
|
|
263
|
-
|
|
288
|
+
const shellControls = controls().filter((element) => visible(element) && Boolean(label(element)));
|
|
289
|
+
return {
|
|
290
|
+
anchor: lastAnchor ? label(lastAnchor) : null,
|
|
291
|
+
connected: Boolean(nav?.isConnected),
|
|
292
|
+
shellReady: shellControls.length >= 2,
|
|
293
|
+
};
|
|
264
294
|
})();`;
|
|
265
295
|
|
|
266
296
|
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
297
|
|
|
268
|
-
export function desktopTasksMainPreload(appUrl
|
|
298
|
+
export function desktopTasksMainPreload(appUrl, {
|
|
299
|
+
enableEmbed = false,
|
|
300
|
+
enableNativeFallback = false,
|
|
301
|
+
} = {}) {
|
|
269
302
|
const appOrigin = desktopTasksAppOrigin(appUrl);
|
|
303
|
+
if (!enableEmbed) return `"use strict";\n`;
|
|
270
304
|
const parserSource = parseDesktopTasksCredential.toString();
|
|
271
305
|
const escapeRegexSource = escapeRegex.toString();
|
|
272
306
|
return `"use strict";
|
|
@@ -283,12 +317,16 @@ if (
|
|
|
283
317
|
const fs = require("node:fs");
|
|
284
318
|
const path = require("node:path");
|
|
285
319
|
const { spawn } = require("node:child_process");
|
|
320
|
+
const { randomBytes } = require("node:crypto");
|
|
286
321
|
const navSource = ${JSON.stringify(NAVIGATION_SOURCE)};
|
|
322
|
+
const navigationNoncePlaceholder = ${JSON.stringify(NAVIGATION_NONCE_PLACEHOLDER)};
|
|
287
323
|
const nativeNavUrl = "data:text/html;charset=utf-8," + encodeURIComponent(${JSON.stringify(NATIVE_NAVIGATION_HTML)});
|
|
288
324
|
const appOrigin = ${JSON.stringify(appOrigin)};
|
|
289
325
|
const bootstrapUrl = appOrigin + ${JSON.stringify(DESKTOP_TASKS_BOOTSTRAP_PATH)};
|
|
290
326
|
const tasksUrl = appOrigin + ${JSON.stringify(DESKTOP_TASKS_BOARD_PATH)};
|
|
291
327
|
const readyMarker = ${JSON.stringify(DESKTOP_TASKS_READY_MARKER)};
|
|
328
|
+
const nativeFallbackEnabled = ${JSON.stringify(enableNativeFallback)};
|
|
329
|
+
const boardSessionMaxAgeMs = 7 * 60 * 60 * 1000;
|
|
292
330
|
const tenantId = process.env.IMPEL_DESKTOP_TASKS_TENANT;
|
|
293
331
|
const nativeShowChannel = "impel-desktop-tasks:native-show";
|
|
294
332
|
const runtimeStatusPath = path.join(__dirname, "runtime-status.json");
|
|
@@ -364,26 +402,15 @@ if (
|
|
|
364
402
|
return;
|
|
365
403
|
}
|
|
366
404
|
const windows = new Map();
|
|
367
|
-
const internalWindows = new WeakSet();
|
|
368
405
|
const internalContents = new Set();
|
|
369
406
|
const nativeNavOwners = new Map();
|
|
370
407
|
const configuredSessions = new WeakSet();
|
|
371
|
-
let creatingInternalWindow = false;
|
|
372
408
|
let hasDomNavigation = false;
|
|
373
409
|
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
410
|
const stateFor = (window) => {
|
|
384
411
|
let state = windows.get(window.id);
|
|
385
412
|
if (!state) {
|
|
386
|
-
state = { view: null, bounds: null, visible: false, nativeNav: null, hasDomNavigation: false, loading: null, bootstrapping: false };
|
|
413
|
+
state = { view: null, bounds: null, visible: false, nativeNav: null, hasDomNavigation: false, loaded: false, loadedAt: 0, loading: null, bootstrapping: false };
|
|
387
414
|
windows.set(window.id, state);
|
|
388
415
|
}
|
|
389
416
|
return state;
|
|
@@ -425,7 +452,16 @@ if (
|
|
|
425
452
|
const errorDocument = "data:text/html;charset=utf-8," + encodeURIComponent(
|
|
426
453
|
"<!doctype html><html><body style='font:14px system-ui;padding:32px'>Impel Tasks could not be loaded. Hide Tasks, check your connection, and try again.</body></html>",
|
|
427
454
|
);
|
|
455
|
+
const loadErrorDocument = async (state) => {
|
|
456
|
+
const contents = state.view?.webContents;
|
|
457
|
+
if (!contents || contents.isDestroyed()) return;
|
|
458
|
+
await contents.loadURL(errorDocument).catch(() => {});
|
|
459
|
+
};
|
|
428
460
|
const loadBoard = async (state) => {
|
|
461
|
+
const contents = state.view?.webContents;
|
|
462
|
+
if (!contents || contents.isDestroyed()) return;
|
|
463
|
+
state.loaded = false;
|
|
464
|
+
state.loadedAt = 0;
|
|
429
465
|
state.bootstrapping = true;
|
|
430
466
|
let pat = null;
|
|
431
467
|
try {
|
|
@@ -439,27 +475,32 @@ if (
|
|
|
439
475
|
].join("\\n"),
|
|
440
476
|
postData: [{ type: "rawData", bytes: Buffer.alloc(0) }],
|
|
441
477
|
};
|
|
442
|
-
await
|
|
443
|
-
if (
|
|
444
|
-
const ready = await
|
|
478
|
+
await contents.loadURL(bootstrapUrl, request);
|
|
479
|
+
if (contents.isDestroyed() || contents.getURL() !== bootstrapUrl) throw new Error("bootstrap redirected");
|
|
480
|
+
const ready = await contents.executeJavaScript(
|
|
445
481
|
"document.body?.textContent?.trim() === " + JSON.stringify(readyMarker),
|
|
446
482
|
true,
|
|
447
483
|
);
|
|
448
484
|
if (!ready) throw new Error("bootstrap marker missing");
|
|
449
485
|
} catch {
|
|
450
486
|
record("board-bootstrap-failed");
|
|
451
|
-
await state
|
|
487
|
+
await loadErrorDocument(state);
|
|
452
488
|
return;
|
|
453
489
|
} finally {
|
|
454
490
|
pat = null;
|
|
455
491
|
state.bootstrapping = false;
|
|
456
492
|
}
|
|
457
493
|
try {
|
|
458
|
-
await
|
|
494
|
+
await contents.loadURL(tasksUrl);
|
|
495
|
+
if (contents.isDestroyed()) return;
|
|
496
|
+
state.loaded = true;
|
|
497
|
+
state.loadedAt = Date.now();
|
|
459
498
|
record("board-loaded");
|
|
460
499
|
} catch {
|
|
500
|
+
state.loaded = false;
|
|
501
|
+
state.loadedAt = 0;
|
|
461
502
|
record("board-load-failed");
|
|
462
|
-
await state
|
|
503
|
+
await loadErrorDocument(state);
|
|
463
504
|
}
|
|
464
505
|
};
|
|
465
506
|
const deactivateNavigation = () => {
|
|
@@ -468,17 +509,20 @@ if (
|
|
|
468
509
|
}
|
|
469
510
|
for (const contents of webContents.getAllWebContents()) {
|
|
470
511
|
if (internalContents.has(contents) || contents.isDestroyed()) continue;
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
512
|
+
contents.mainFrame?.executeJavaScript?.(
|
|
513
|
+
"window.__impelDesktopTasksSetActive?.(false)",
|
|
514
|
+
true,
|
|
515
|
+
)?.catch(() => {});
|
|
474
516
|
}
|
|
475
517
|
};
|
|
476
518
|
const isHostWindow = (window) => {
|
|
477
|
-
if (!window || window.isDestroyed() ||
|
|
519
|
+
if (!window || window.isDestroyed() || window.getParentWindow?.()) return false;
|
|
478
520
|
try {
|
|
479
|
-
const
|
|
521
|
+
const rawUrl = window.webContents.getURL();
|
|
522
|
+
if (!rawUrl || rawUrl === "about:blank") return false;
|
|
523
|
+
const url = new URL(rawUrl);
|
|
480
524
|
if (url.searchParams.has("initialRoute")) return false;
|
|
481
|
-
} catch {}
|
|
525
|
+
} catch { return false; }
|
|
482
526
|
return true;
|
|
483
527
|
};
|
|
484
528
|
const primaryHostWindow = () => {
|
|
@@ -496,6 +540,7 @@ if (
|
|
|
496
540
|
state.view?.setVisible(false);
|
|
497
541
|
window.webContents.focus();
|
|
498
542
|
deactivateNavigation();
|
|
543
|
+
record("board-hidden");
|
|
499
544
|
};
|
|
500
545
|
const isPrimaryWindow = (window) => {
|
|
501
546
|
return isHostWindow(window) && window === primaryHostWindow() && window.isVisible?.() !== false;
|
|
@@ -518,6 +563,8 @@ if (
|
|
|
518
563
|
},
|
|
519
564
|
});
|
|
520
565
|
state.view = view;
|
|
566
|
+
state.loaded = false;
|
|
567
|
+
state.loadedAt = 0;
|
|
521
568
|
internalContents.add(view.webContents);
|
|
522
569
|
configureSession(view.webContents.session);
|
|
523
570
|
window.contentView.addChildView(view);
|
|
@@ -545,7 +592,12 @@ if (
|
|
|
545
592
|
view.webContents.on("destroyed", () => {
|
|
546
593
|
internalContents.delete(view.webContents);
|
|
547
594
|
try { window.contentView.removeChildView(view); } catch {}
|
|
548
|
-
if (state.view === view)
|
|
595
|
+
if (state.view === view) {
|
|
596
|
+
state.view = null;
|
|
597
|
+
state.loaded = false;
|
|
598
|
+
state.loadedAt = 0;
|
|
599
|
+
state.loading = null;
|
|
600
|
+
}
|
|
549
601
|
});
|
|
550
602
|
return state;
|
|
551
603
|
};
|
|
@@ -558,7 +610,20 @@ if (
|
|
|
558
610
|
state.view.webContents.focus();
|
|
559
611
|
if (state.visible) return;
|
|
560
612
|
state.visible = true;
|
|
561
|
-
|
|
613
|
+
let boardRoute = false;
|
|
614
|
+
try {
|
|
615
|
+
const current = new URL(state.view.webContents.getURL());
|
|
616
|
+
boardRoute = current.origin === appOrigin
|
|
617
|
+
&& (current.pathname === tasksUrl.slice(appOrigin.length)
|
|
618
|
+
|| current.pathname.startsWith(tasksUrl.slice(appOrigin.length) + "/"));
|
|
619
|
+
} catch {}
|
|
620
|
+
const reusableBoard = state.loaded
|
|
621
|
+
&& Date.now() - state.loadedAt < boardSessionMaxAgeMs
|
|
622
|
+
&& boardRoute;
|
|
623
|
+
if (!reusableBoard) state.loaded = false;
|
|
624
|
+
if (!state.loaded && !state.loading) {
|
|
625
|
+
state.loading = loadBoard(state).finally(() => { state.loading = null; });
|
|
626
|
+
}
|
|
562
627
|
record("board-shown");
|
|
563
628
|
};
|
|
564
629
|
const layoutBoard = (window, bounds = null) => {
|
|
@@ -593,9 +658,9 @@ if (
|
|
|
593
658
|
};
|
|
594
659
|
const layoutNativeNav = (window) => {
|
|
595
660
|
const state = windows.get(window.id);
|
|
596
|
-
if (!state?.nativeNav || state.nativeNav.isDestroyed()) return;
|
|
661
|
+
if (!state?.nativeNav || state.nativeNav.webContents.isDestroyed()) return;
|
|
597
662
|
const content = window.getContentBounds();
|
|
598
|
-
state.nativeNav.setBounds({ x:
|
|
663
|
+
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
664
|
};
|
|
600
665
|
const ensureNativeNav = (probeWindow) => {
|
|
601
666
|
// Renderer probes finish independently. Only the canonical host may
|
|
@@ -604,21 +669,14 @@ if (
|
|
|
604
669
|
const window = fallbackHostWindow();
|
|
605
670
|
if (!window || probeWindow !== window) return;
|
|
606
671
|
const state = stateFor(window);
|
|
607
|
-
if (state.nativeNav && !state.nativeNav.isDestroyed()) return;
|
|
672
|
+
if (state.nativeNav && !state.nativeNav.webContents.isDestroyed()) return;
|
|
608
673
|
// Auxiliary vendor windows (for example Codex's hidden avatar overlay)
|
|
609
674
|
// are also top-level BrowserWindows. Never give them their own Tasks
|
|
610
|
-
// fallback: duplicate
|
|
611
|
-
// and can cover the primary shell.
|
|
675
|
+
// fallback: duplicate views can cover the primary shell.
|
|
612
676
|
if ([...windows.values()].some((candidate) => (
|
|
613
|
-
candidate.nativeNav && !candidate.nativeNav.isDestroyed()
|
|
677
|
+
candidate.nativeNav && !candidate.nativeNav.webContents.isDestroyed()
|
|
614
678
|
))) return;
|
|
615
|
-
const nativeNav =
|
|
616
|
-
parent: window,
|
|
617
|
-
frame: false,
|
|
618
|
-
show: false,
|
|
619
|
-
resizable: false,
|
|
620
|
-
skipTaskbar: true,
|
|
621
|
-
backgroundColor: "#292929",
|
|
679
|
+
const nativeNav = new WebContentsView({
|
|
622
680
|
webPreferences: {
|
|
623
681
|
preload: process.env.IMPEL_DESKTOP_TASKS_VIEW_PRELOAD,
|
|
624
682
|
contextIsolation: true,
|
|
@@ -629,15 +687,19 @@ if (
|
|
|
629
687
|
state.nativeNav = nativeNav;
|
|
630
688
|
internalContents.add(nativeNav.webContents);
|
|
631
689
|
nativeNavOwners.set(nativeNav.webContents, window);
|
|
690
|
+
window.contentView.addChildView(nativeNav);
|
|
691
|
+
nativeNav.setVisible(false);
|
|
692
|
+
nativeNav.setBackgroundColor("#292929");
|
|
632
693
|
nativeNav.webContents.setWindowOpenHandler(() => ({ action: "deny" }));
|
|
633
694
|
nativeNav.webContents.on("will-navigate", (event, url) => { if (url !== nativeNavUrl) event.preventDefault(); });
|
|
634
695
|
nativeNav.webContents.on("destroyed", () => {
|
|
635
696
|
internalContents.delete(nativeNav.webContents);
|
|
636
697
|
nativeNavOwners.delete(nativeNav.webContents);
|
|
698
|
+
if (state.nativeNav === nativeNav) state.nativeNav = null;
|
|
637
699
|
});
|
|
638
700
|
nativeNav.webContents.loadURL(nativeNavUrl).catch(() => record("native-navigation-load-failed"));
|
|
639
701
|
layoutNativeNav(window);
|
|
640
|
-
nativeNav.
|
|
702
|
+
nativeNav.setVisible(true);
|
|
641
703
|
record("native-navigation-installed");
|
|
642
704
|
};
|
|
643
705
|
const removeNativeNav = (window) => {
|
|
@@ -647,7 +709,13 @@ if (
|
|
|
647
709
|
state.nativeNav = null;
|
|
648
710
|
internalContents.delete(nativeNav.webContents);
|
|
649
711
|
nativeNavOwners.delete(nativeNav.webContents);
|
|
650
|
-
try {
|
|
712
|
+
try { window.contentView.removeChildView(nativeNav); } catch {}
|
|
713
|
+
try { nativeNav.webContents.close(); } catch {}
|
|
714
|
+
};
|
|
715
|
+
const markDomNavigation = (window) => {
|
|
716
|
+
hasDomNavigation = true;
|
|
717
|
+
stateFor(window).hasDomNavigation = true;
|
|
718
|
+
for (const owner of BrowserWindow.getAllWindows()) removeNativeNav(owner);
|
|
651
719
|
};
|
|
652
720
|
const ownerWindow = (contents) => {
|
|
653
721
|
const window = BrowserWindow.fromWebContents(contents) || contents.getOwnerBrowserWindow?.() || null;
|
|
@@ -656,69 +724,100 @@ if (
|
|
|
656
724
|
const attachContents = (contents) => {
|
|
657
725
|
if (!contents || contents.isDestroyed() || contents.__impelDesktopTasksAttached || internalContents.has(contents)) return;
|
|
658
726
|
contents.__impelDesktopTasksAttached = true;
|
|
659
|
-
const
|
|
727
|
+
const navigationNonce = randomBytes(32).toString("base64url");
|
|
728
|
+
const contentsNavSource = navSource.replace(
|
|
729
|
+
JSON.stringify(navigationNoncePlaceholder),
|
|
730
|
+
JSON.stringify(navigationNonce),
|
|
731
|
+
);
|
|
732
|
+
const inject = () => {
|
|
660
733
|
if (internalContents.has(contents)) return;
|
|
661
|
-
|
|
662
|
-
|
|
734
|
+
const frame = contents.mainFrame;
|
|
735
|
+
if (!frame || typeof frame.executeJavaScript !== "function") return;
|
|
736
|
+
frame.executeJavaScript(contentsNavSource, true)
|
|
737
|
+
.then((result) => {
|
|
663
738
|
const window = ownerWindow(contents);
|
|
664
739
|
if (!window || window !== primaryHostWindow()) return;
|
|
665
740
|
const state = stateFor(window);
|
|
666
|
-
if (
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
}
|
|
671
|
-
else if (!state.hasDomNavigation && !hasDomNavigation) ensureNativeNav(window);
|
|
741
|
+
if (result?.anchor) {
|
|
742
|
+
markDomNavigation(window);
|
|
743
|
+
} else if (result?.shellReady && !state.hasDomNavigation && !hasDomNavigation && nativeFallbackEnabled) {
|
|
744
|
+
ensureNativeNav(window);
|
|
745
|
+
}
|
|
672
746
|
})
|
|
673
747
|
.catch(() => record("navigation-injection-failed"));
|
|
674
748
|
};
|
|
675
749
|
contents.on("did-finish-load", () => inject());
|
|
676
|
-
contents.on("did-frame-finish-load", () => inject());
|
|
677
750
|
if (!contents.isLoadingMainFrame()) setTimeout(() => inject(), 0);
|
|
678
|
-
|
|
751
|
+
const handledNavigationEvents = new Set();
|
|
752
|
+
let handledLegacyNavigation = null;
|
|
679
753
|
const handleNavigation = (event, url) => {
|
|
680
754
|
if (!String(url).startsWith("impel-tasks://")) return;
|
|
681
755
|
event.preventDefault();
|
|
682
756
|
const window = ownerWindow(contents);
|
|
683
757
|
if (!window || window !== primaryHostWindow()) return;
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
if (
|
|
758
|
+
let navigation;
|
|
759
|
+
try { navigation = new URL(url); } catch { return; }
|
|
760
|
+
if (navigation.searchParams.get("nonce") !== navigationNonce) return;
|
|
761
|
+
const action = navigation.hostname;
|
|
762
|
+
const eventId = navigation.searchParams.get("event");
|
|
763
|
+
if (eventId) {
|
|
764
|
+
const navigationKey = action + ":" + eventId;
|
|
765
|
+
if (handledNavigationEvents.has(navigationKey)) return;
|
|
766
|
+
handledNavigationEvents.add(navigationKey);
|
|
767
|
+
if (handledNavigationEvents.size > 128) {
|
|
768
|
+
handledNavigationEvents.delete(handledNavigationEvents.values().next().value);
|
|
769
|
+
}
|
|
770
|
+
} else {
|
|
771
|
+
if (handledLegacyNavigation === url) return;
|
|
772
|
+
handledLegacyNavigation = url;
|
|
773
|
+
queueMicrotask(() => { if (handledLegacyNavigation === url) handledLegacyNavigation = null; });
|
|
774
|
+
}
|
|
775
|
+
if (action === "ready") markDomNavigation(window);
|
|
776
|
+
else if (action === "hide") hide(window);
|
|
690
777
|
else if (action === "show") show(window, parseBounds(url, window));
|
|
691
778
|
else if (action === "layout") layoutBoard(window, parseBounds(url, window));
|
|
692
779
|
};
|
|
693
|
-
|
|
694
|
-
|
|
780
|
+
const eventUrl = (event, deprecatedUrl) => (
|
|
781
|
+
typeof event?.url === "string" ? event.url : String(deprecatedUrl || "")
|
|
782
|
+
);
|
|
783
|
+
contents.on("will-navigate", (event, deprecatedUrl) => {
|
|
784
|
+
handleNavigation(event, eventUrl(event, deprecatedUrl));
|
|
785
|
+
});
|
|
786
|
+
contents.on("will-frame-navigate", (event, deprecatedUrl, _isInPlace, deprecatedIsMainFrame) => {
|
|
787
|
+
const url = eventUrl(event, deprecatedUrl);
|
|
788
|
+
if (!url.startsWith("impel-tasks://")) return;
|
|
789
|
+
event.preventDefault();
|
|
790
|
+
const isMainFrame = typeof event?.isMainFrame === "boolean"
|
|
791
|
+
? event.isMainFrame
|
|
792
|
+
: deprecatedIsMainFrame === true;
|
|
793
|
+
if (isMainFrame) handleNavigation(event, url);
|
|
794
|
+
});
|
|
695
795
|
};
|
|
696
796
|
const attachWindow = (window) => {
|
|
697
|
-
if (!window || window.isDestroyed() ||
|
|
797
|
+
if (!window || window.isDestroyed() || window.getParentWindow?.() || window.__impelDesktopTasksWindowAttached) return;
|
|
698
798
|
window.__impelDesktopTasksWindowAttached = true;
|
|
699
799
|
attachContents(window.webContents);
|
|
700
800
|
window.on("resize", () => { layoutNativeNav(window); layoutBoard(window); });
|
|
701
801
|
window.on("focus", () => {
|
|
702
802
|
const state = windows.get(window.id);
|
|
703
|
-
state?.nativeNav?.
|
|
803
|
+
state?.nativeNav?.setVisible(true);
|
|
704
804
|
if (state?.visible) state.view?.setVisible(true);
|
|
705
805
|
});
|
|
706
806
|
window.on("blur", () => setTimeout(() => {
|
|
707
807
|
const state = windows.get(window.id);
|
|
708
|
-
if (BrowserWindow.getFocusedWindow() === state?.nativeNav) return;
|
|
709
808
|
// Keep the only path to Tasks available while the app is frontmost.
|
|
710
|
-
//
|
|
711
|
-
//
|
|
712
|
-
if (state?.nativeNav && (!window.isVisible() || window.isMinimized())) state.nativeNav.
|
|
809
|
+
// Moving focus into the fallback view must not hide it before the click
|
|
810
|
+
// arrives; only a hidden or minimized host hides the affordance.
|
|
811
|
+
if (state?.nativeNav && (!window.isVisible() || window.isMinimized())) state.nativeNav.setVisible(false);
|
|
713
812
|
}, 150));
|
|
714
813
|
window.on("minimize", () => {
|
|
715
814
|
const state = windows.get(window.id);
|
|
716
|
-
state?.nativeNav?.
|
|
815
|
+
state?.nativeNav?.setVisible(false);
|
|
717
816
|
state?.view?.setVisible(false);
|
|
718
817
|
});
|
|
719
818
|
window.on("restore", () => {
|
|
720
819
|
const state = windows.get(window.id);
|
|
721
|
-
state?.nativeNav?.
|
|
820
|
+
state?.nativeNav?.setVisible(true);
|
|
722
821
|
if (state?.visible) state.view?.setVisible(true);
|
|
723
822
|
});
|
|
724
823
|
window.on("closed", () => {
|
|
@@ -726,7 +825,8 @@ if (
|
|
|
726
825
|
if (state) {
|
|
727
826
|
try { if (state.view) window.contentView.removeChildView(state.view); } catch {}
|
|
728
827
|
try { state.view?.webContents.close(); } catch {}
|
|
729
|
-
try { state.nativeNav
|
|
828
|
+
try { if (state.nativeNav) window.contentView.removeChildView(state.nativeNav); } catch {}
|
|
829
|
+
try { state.nativeNav?.webContents.close(); } catch {}
|
|
730
830
|
}
|
|
731
831
|
windows.delete(window.id);
|
|
732
832
|
if (primaryHost === window) primaryHost = null;
|
|
@@ -1,6 +1,8 @@
|
|
|
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
|
-
|
|
4
|
+
// v41 restores ChatGPT's embedded Tasks entry through its real sidebar only.
|
|
5
|
+
// The native fallback stays disabled, and a valid authenticated board is
|
|
6
|
+
// reused across hide/show cycles so it cannot obscure or repeatedly bootstrap
|
|
7
|
+
// the host.
|
|
8
|
+
export const CURRENT_CONFIG_VERSION = 41;
|