impel-cli 0.20.49 → 0.20.51
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 +35 -0
- package/package.json +1 -1
- package/src/apps.js +6 -2
- package/src/desktopTasks.js +253 -36
- package/src/managedProfileVersion.js +4 -3
package/RELEASE_NOTES.md
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
# Release notes
|
|
2
2
|
|
|
3
|
+
## 0.20.51 — Restore Claude Tasks in the sidebar
|
|
4
|
+
|
|
5
|
+
- Restores one embedded Tasks row in managed Claude's existing sidebar,
|
|
6
|
+
directly after the Home/Code switcher and before New and Customize.
|
|
7
|
+
- Uses the same confined `WebContentsView` board lifecycle as ChatGPT while
|
|
8
|
+
keeping the native fallback disabled, so Tasks cannot cover Claude during
|
|
9
|
+
startup or create a second control window.
|
|
10
|
+
- Promotes Claude's real pinned-app renderer smoke from boot-only to a required
|
|
11
|
+
UI contract: working surface, exact row placement, interactive tenant-bound
|
|
12
|
+
board, restored host navigation, screenshots, and credential/egress checks.
|
|
13
|
+
- Advances the managed profile contract to v42 so installed Claude apps rewrite
|
|
14
|
+
the repaired preload and restart without rebuilding their vendor bundles.
|
|
15
|
+
|
|
16
|
+
## 0.20.50 — Restore ChatGPT Tasks safely
|
|
17
|
+
|
|
18
|
+
- Restores one embedded Tasks entry in the managed ChatGPT sidebar while
|
|
19
|
+
keeping Claude's production preload inert and preserving normal ChatGPT
|
|
20
|
+
startup and navigation.
|
|
21
|
+
- Disables the native fallback in the production ChatGPT profile, so Tasks
|
|
22
|
+
cannot create a second control view or cover the vendor app during startup.
|
|
23
|
+
- Debounces sidebar discovery, makes attached-control checks constant-time,
|
|
24
|
+
and removes the outer reinjection retry chain that could repeatedly scan the
|
|
25
|
+
renderer while its sidebar anchor was late.
|
|
26
|
+
- Injects and accepts Tasks navigation only in the main vendor frame; subframes
|
|
27
|
+
cannot open, resize, or bootstrap the authenticated board.
|
|
28
|
+
- Deduplicates paired Electron navigation events and reuses the loaded board,
|
|
29
|
+
keeping one tenant-bound credential exchange across ordinary hide/reopen
|
|
30
|
+
cycles while allowing an expired or failed board session to recover.
|
|
31
|
+
- Advances the managed profile contract to v41 so existing macOS ChatGPT apps
|
|
32
|
+
rewrite and reload the repaired preload without rebuilding vendor bundles.
|
|
33
|
+
- Adds a known-bad `0.20.48` control and a real packaged-app gate with screenshot
|
|
34
|
+
artifacts for the normal surface, one Tasks tab, and the interactive board;
|
|
35
|
+
it also externally observes the board target becoming compositor-hidden when
|
|
36
|
+
host navigation is restored, with zero fallback controls and one bootstrap.
|
|
37
|
+
|
|
3
38
|
## 0.20.49 — Restore managed app startup
|
|
4
39
|
|
|
5
40
|
- Restores normal ChatGPT and Claude startup by disabling the experimental
|
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: true },
|
|
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";
|
|
@@ -104,6 +105,9 @@ const NAVIGATION_SOURCE = String.raw`(() => {
|
|
|
104
105
|
let active = false;
|
|
105
106
|
let lastAnchor = null;
|
|
106
107
|
let readySignalled = false;
|
|
108
|
+
let ensureTimer = null;
|
|
109
|
+
const signalSequenceKey = "__impelDesktopTasksSignalSequence";
|
|
110
|
+
const navigationNonce = ${JSON.stringify(NAVIGATION_NONCE_PLACEHOLDER)};
|
|
107
111
|
const roots = () => {
|
|
108
112
|
const result = [document];
|
|
109
113
|
for (let index = 0; index < result.length; index += 1) {
|
|
@@ -157,6 +161,10 @@ const NAVIGATION_SOURCE = String.raw`(() => {
|
|
|
157
161
|
};
|
|
158
162
|
const signal = (action) => {
|
|
159
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);
|
|
160
168
|
location.href = "impel-tasks://" + action + "?" + query;
|
|
161
169
|
};
|
|
162
170
|
const select = (selected) => {
|
|
@@ -210,6 +218,10 @@ const NAVIGATION_SOURCE = String.raw`(() => {
|
|
|
210
218
|
return button;
|
|
211
219
|
};
|
|
212
220
|
const ensureNav = () => {
|
|
221
|
+
if (nav?.isConnected) {
|
|
222
|
+
select(active);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
213
225
|
let rule = null;
|
|
214
226
|
let anchor = null;
|
|
215
227
|
for (const candidate of RULES) {
|
|
@@ -253,11 +265,19 @@ const NAVIGATION_SOURCE = String.raw`(() => {
|
|
|
253
265
|
const onResize = () => { if (active) signal("layout"); };
|
|
254
266
|
window.addEventListener("click", onClick, true);
|
|
255
267
|
window.addEventListener("resize", onResize);
|
|
256
|
-
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);
|
|
257
276
|
observer.observe(document.body, { childList: true, subtree: true });
|
|
258
277
|
ensureNav();
|
|
259
278
|
window.__impelDesktopTasksCleanup = () => {
|
|
260
279
|
observer.disconnect();
|
|
280
|
+
if (ensureTimer !== null) clearTimeout(ensureTimer);
|
|
261
281
|
window.removeEventListener("click", onClick, true);
|
|
262
282
|
window.removeEventListener("resize", onResize);
|
|
263
283
|
nav?.remove();
|
|
@@ -266,16 +286,40 @@ const NAVIGATION_SOURCE = String.raw`(() => {
|
|
|
266
286
|
delete window.__impelDesktopTasksSetActive;
|
|
267
287
|
};
|
|
268
288
|
const shellControls = controls().filter((element) => visible(element) && Boolean(label(element)));
|
|
289
|
+
const namedControl = (name) => shellControls.find((element) => [
|
|
290
|
+
element.getAttribute("aria-label"),
|
|
291
|
+
element.getAttribute("title"),
|
|
292
|
+
element.textContent,
|
|
293
|
+
].some((value) => (value || "").replace(/\s+/g, " ").trim() === name)) || null;
|
|
294
|
+
const bounds = (element) => {
|
|
295
|
+
if (!element) return null;
|
|
296
|
+
const value = element.getBoundingClientRect();
|
|
297
|
+
return {
|
|
298
|
+
top: Math.round(value.top),
|
|
299
|
+
bottom: Math.round(value.bottom),
|
|
300
|
+
left: Math.round(value.left),
|
|
301
|
+
right: Math.round(value.right),
|
|
302
|
+
};
|
|
303
|
+
};
|
|
269
304
|
return {
|
|
270
305
|
anchor: lastAnchor ? label(lastAnchor) : null,
|
|
271
306
|
connected: Boolean(nav?.isConnected),
|
|
272
307
|
shellReady: shellControls.length >= 2,
|
|
308
|
+
placement: {
|
|
309
|
+
tasks: bounds(nav),
|
|
310
|
+
code: bounds(namedControl("Code")),
|
|
311
|
+
new: bounds(namedControl("New")),
|
|
312
|
+
customize: bounds(namedControl("Customize")),
|
|
313
|
+
},
|
|
273
314
|
};
|
|
274
315
|
})();`;
|
|
275
316
|
|
|
276
317
|
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>`;
|
|
277
318
|
|
|
278
|
-
export function desktopTasksMainPreload(appUrl, {
|
|
319
|
+
export function desktopTasksMainPreload(appUrl, {
|
|
320
|
+
enableEmbed = false,
|
|
321
|
+
enableNativeFallback = false,
|
|
322
|
+
} = {}) {
|
|
279
323
|
const appOrigin = desktopTasksAppOrigin(appUrl);
|
|
280
324
|
if (!enableEmbed) return `"use strict";\n`;
|
|
281
325
|
const parserSource = parseDesktopTasksCredential.toString();
|
|
@@ -294,22 +338,33 @@ if (
|
|
|
294
338
|
const fs = require("node:fs");
|
|
295
339
|
const path = require("node:path");
|
|
296
340
|
const { spawn } = require("node:child_process");
|
|
341
|
+
const { randomBytes } = require("node:crypto");
|
|
297
342
|
const navSource = ${JSON.stringify(NAVIGATION_SOURCE)};
|
|
343
|
+
const navigationNoncePlaceholder = ${JSON.stringify(NAVIGATION_NONCE_PLACEHOLDER)};
|
|
298
344
|
const nativeNavUrl = "data:text/html;charset=utf-8," + encodeURIComponent(${JSON.stringify(NATIVE_NAVIGATION_HTML)});
|
|
299
345
|
const appOrigin = ${JSON.stringify(appOrigin)};
|
|
300
346
|
const bootstrapUrl = appOrigin + ${JSON.stringify(DESKTOP_TASKS_BOOTSTRAP_PATH)};
|
|
301
347
|
const tasksUrl = appOrigin + ${JSON.stringify(DESKTOP_TASKS_BOARD_PATH)};
|
|
302
348
|
const readyMarker = ${JSON.stringify(DESKTOP_TASKS_READY_MARKER)};
|
|
349
|
+
const nativeFallbackEnabled = ${JSON.stringify(enableNativeFallback)};
|
|
350
|
+
const smokeMode = process.env.IMPEL_DESKTOP_TASKS_SMOKE === "1";
|
|
351
|
+
const smokeDirectory = process.env.IMPEL_DESKTOP_TASKS_SMOKE_DIR || "";
|
|
352
|
+
const boardSessionMaxAgeMs = 7 * 60 * 60 * 1000;
|
|
303
353
|
const tenantId = process.env.IMPEL_DESKTOP_TASKS_TENANT;
|
|
304
354
|
const nativeShowChannel = "impel-desktop-tasks:native-show";
|
|
305
355
|
const runtimeStatusPath = path.join(__dirname, "runtime-status.json");
|
|
306
356
|
const escapeRegex = ${escapeRegexSource};
|
|
307
357
|
const parseCredential = ${parserSource};
|
|
308
|
-
const
|
|
358
|
+
const runtimeHistory = [];
|
|
359
|
+
const record = (stage, details = null) => {
|
|
309
360
|
try {
|
|
361
|
+
runtimeHistory.push({ stage, details, at: new Date().toISOString() });
|
|
362
|
+
if (runtimeHistory.length > 32) runtimeHistory.shift();
|
|
310
363
|
fs.writeFileSync(runtimeStatusPath, JSON.stringify({
|
|
311
364
|
schemaVersion: 2,
|
|
312
365
|
stage,
|
|
366
|
+
details,
|
|
367
|
+
history: runtimeHistory,
|
|
313
368
|
processType: process.type || null,
|
|
314
369
|
electronVersion: process.versions.electron || null,
|
|
315
370
|
pid: process.pid,
|
|
@@ -317,6 +372,14 @@ if (
|
|
|
317
372
|
}, null, 2) + "\\n", { mode: 0o600 });
|
|
318
373
|
} catch {}
|
|
319
374
|
};
|
|
375
|
+
const captureSmokeProof = async (name, capturer) => {
|
|
376
|
+
if (!smokeMode || !smokeDirectory || typeof capturer?.capturePage !== "function") return;
|
|
377
|
+
try {
|
|
378
|
+
fs.mkdirSync(smokeDirectory, { recursive: true });
|
|
379
|
+
const image = await capturer.capturePage();
|
|
380
|
+
fs.writeFileSync(path.join(smokeDirectory, name), image.toPNG(), { mode: 0o600 });
|
|
381
|
+
} catch { record("smoke-capture-failed", { name }); }
|
|
382
|
+
};
|
|
320
383
|
const token = () => new Promise((resolve, reject) => {
|
|
321
384
|
const childEnvironment = { ...process.env };
|
|
322
385
|
for (const name of ["ANTHROPIC_API_KEY", "CODEX_ACCESS_TOKEN", "CODEX_API_KEY", "OPENAI_API_KEY"]) {
|
|
@@ -383,7 +446,7 @@ if (
|
|
|
383
446
|
const stateFor = (window) => {
|
|
384
447
|
let state = windows.get(window.id);
|
|
385
448
|
if (!state) {
|
|
386
|
-
state = { view: null, bounds: null, visible: false, nativeNav: null, hasDomNavigation: false, loading: null, bootstrapping: false };
|
|
449
|
+
state = { window, view: null, bounds: null, visible: false, nativeNav: null, hasDomNavigation: false, loaded: false, loadedAt: 0, loading: null, bootstrapping: false, smokeStarted: false };
|
|
387
450
|
windows.set(window.id, state);
|
|
388
451
|
}
|
|
389
452
|
return state;
|
|
@@ -425,7 +488,16 @@ if (
|
|
|
425
488
|
const errorDocument = "data:text/html;charset=utf-8," + encodeURIComponent(
|
|
426
489
|
"<!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
490
|
);
|
|
491
|
+
const loadErrorDocument = async (state) => {
|
|
492
|
+
const contents = state.view?.webContents;
|
|
493
|
+
if (!contents || contents.isDestroyed()) return;
|
|
494
|
+
await contents.loadURL(errorDocument).catch(() => {});
|
|
495
|
+
};
|
|
428
496
|
const loadBoard = async (state) => {
|
|
497
|
+
const contents = state.view?.webContents;
|
|
498
|
+
if (!contents || contents.isDestroyed()) return;
|
|
499
|
+
state.loaded = false;
|
|
500
|
+
state.loadedAt = 0;
|
|
429
501
|
state.bootstrapping = true;
|
|
430
502
|
let pat = null;
|
|
431
503
|
try {
|
|
@@ -439,27 +511,61 @@ if (
|
|
|
439
511
|
].join("\\n"),
|
|
440
512
|
postData: [{ type: "rawData", bytes: Buffer.alloc(0) }],
|
|
441
513
|
};
|
|
442
|
-
await
|
|
443
|
-
if (
|
|
444
|
-
const ready = await
|
|
514
|
+
await contents.loadURL(bootstrapUrl, request);
|
|
515
|
+
if (contents.isDestroyed() || contents.getURL() !== bootstrapUrl) throw new Error("bootstrap redirected");
|
|
516
|
+
const ready = await contents.executeJavaScript(
|
|
445
517
|
"document.body?.textContent?.trim() === " + JSON.stringify(readyMarker),
|
|
446
518
|
true,
|
|
447
519
|
);
|
|
448
520
|
if (!ready) throw new Error("bootstrap marker missing");
|
|
449
521
|
} catch {
|
|
450
522
|
record("board-bootstrap-failed");
|
|
451
|
-
await state
|
|
523
|
+
await loadErrorDocument(state);
|
|
452
524
|
return;
|
|
453
525
|
} finally {
|
|
454
526
|
pat = null;
|
|
455
527
|
state.bootstrapping = false;
|
|
456
528
|
}
|
|
457
529
|
try {
|
|
458
|
-
await
|
|
530
|
+
await contents.loadURL(tasksUrl);
|
|
531
|
+
if (contents.isDestroyed()) return;
|
|
532
|
+
state.loaded = true;
|
|
533
|
+
state.loadedAt = Date.now();
|
|
459
534
|
record("board-loaded");
|
|
535
|
+
if (smokeMode && state.smokeStarted) {
|
|
536
|
+
let boardProof = null;
|
|
537
|
+
const proofDeadline = Date.now() + 15000;
|
|
538
|
+
while (!contents.isDestroyed() && Date.now() < proofDeadline) {
|
|
539
|
+
try {
|
|
540
|
+
boardProof = await contents.executeJavaScript(
|
|
541
|
+
"(() => { const ready = document.querySelector('[data-impel-desktop-board-ready=\\\"true\\\"]') !== null; const action = document.getElementById('board-action'); if (action) action.click(); return {ready,actionClicked:Boolean(action),interactive:Boolean(document.getElementById('board-result')?.textContent?.trim())}; })()",
|
|
542
|
+
true,
|
|
543
|
+
);
|
|
544
|
+
} catch {}
|
|
545
|
+
if (boardProof?.ready && boardProof?.interactive) break;
|
|
546
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
547
|
+
}
|
|
548
|
+
record("board-smoke-verified", boardProof);
|
|
549
|
+
await captureSmokeProof("claude-tasks-board.png", contents);
|
|
550
|
+
let hostClick = null;
|
|
551
|
+
try {
|
|
552
|
+
hostClick = await state.window.webContents.mainFrame.executeJavaScript(
|
|
553
|
+
"(() => { const norm = value => (value || '').replace(/\\s+/g, ' ').trim(); const visible = element => { const rect = element.getBoundingClientRect(); const style = getComputedStyle(element); return rect.width > 0 && rect.height > 0 && style.display !== 'none' && style.visibility !== 'hidden'; }; const controls = [...document.querySelectorAll('button, a, [role=\\\"button\\\"], [role=\\\"link\\\"], [tabindex]')]; const hit = controls.find(element => visible(element) && [element.getAttribute('aria-label'), element.getAttribute('title'), element.textContent].some(value => norm(value) === 'New')); if (!hit) return {clicked:false}; hit.click(); return {clicked:true}; })()",
|
|
554
|
+
true,
|
|
555
|
+
);
|
|
556
|
+
} catch {}
|
|
557
|
+
const restoreDeadline = Date.now() + 3000;
|
|
558
|
+
while (state.visible && Date.now() < restoreDeadline) {
|
|
559
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
560
|
+
}
|
|
561
|
+
record("navigation-smoke-restored", { hostClick, boardHidden: !state.visible });
|
|
562
|
+
await captureSmokeProof("claude-restored.png", state.window);
|
|
563
|
+
}
|
|
460
564
|
} catch {
|
|
565
|
+
state.loaded = false;
|
|
566
|
+
state.loadedAt = 0;
|
|
461
567
|
record("board-load-failed");
|
|
462
|
-
await state
|
|
568
|
+
await loadErrorDocument(state);
|
|
463
569
|
}
|
|
464
570
|
};
|
|
465
571
|
const deactivateNavigation = () => {
|
|
@@ -468,9 +574,10 @@ if (
|
|
|
468
574
|
}
|
|
469
575
|
for (const contents of webContents.getAllWebContents()) {
|
|
470
576
|
if (internalContents.has(contents) || contents.isDestroyed()) continue;
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
577
|
+
contents.mainFrame?.executeJavaScript?.(
|
|
578
|
+
"window.__impelDesktopTasksSetActive?.(false)",
|
|
579
|
+
true,
|
|
580
|
+
)?.catch(() => {});
|
|
474
581
|
}
|
|
475
582
|
};
|
|
476
583
|
const isHostWindow = (window) => {
|
|
@@ -498,6 +605,7 @@ if (
|
|
|
498
605
|
state.view?.setVisible(false);
|
|
499
606
|
window.webContents.focus();
|
|
500
607
|
deactivateNavigation();
|
|
608
|
+
record("board-hidden");
|
|
501
609
|
};
|
|
502
610
|
const isPrimaryWindow = (window) => {
|
|
503
611
|
return isHostWindow(window) && window === primaryHostWindow() && window.isVisible?.() !== false;
|
|
@@ -520,6 +628,8 @@ if (
|
|
|
520
628
|
},
|
|
521
629
|
});
|
|
522
630
|
state.view = view;
|
|
631
|
+
state.loaded = false;
|
|
632
|
+
state.loadedAt = 0;
|
|
523
633
|
internalContents.add(view.webContents);
|
|
524
634
|
configureSession(view.webContents.session);
|
|
525
635
|
window.contentView.addChildView(view);
|
|
@@ -547,7 +657,12 @@ if (
|
|
|
547
657
|
view.webContents.on("destroyed", () => {
|
|
548
658
|
internalContents.delete(view.webContents);
|
|
549
659
|
try { window.contentView.removeChildView(view); } catch {}
|
|
550
|
-
if (state.view === view)
|
|
660
|
+
if (state.view === view) {
|
|
661
|
+
state.view = null;
|
|
662
|
+
state.loaded = false;
|
|
663
|
+
state.loadedAt = 0;
|
|
664
|
+
state.loading = null;
|
|
665
|
+
}
|
|
551
666
|
});
|
|
552
667
|
return state;
|
|
553
668
|
};
|
|
@@ -560,7 +675,20 @@ if (
|
|
|
560
675
|
state.view.webContents.focus();
|
|
561
676
|
if (state.visible) return;
|
|
562
677
|
state.visible = true;
|
|
563
|
-
|
|
678
|
+
let boardRoute = false;
|
|
679
|
+
try {
|
|
680
|
+
const current = new URL(state.view.webContents.getURL());
|
|
681
|
+
boardRoute = current.origin === appOrigin
|
|
682
|
+
&& (current.pathname === tasksUrl.slice(appOrigin.length)
|
|
683
|
+
|| current.pathname.startsWith(tasksUrl.slice(appOrigin.length) + "/"));
|
|
684
|
+
} catch {}
|
|
685
|
+
const reusableBoard = state.loaded
|
|
686
|
+
&& Date.now() - state.loadedAt < boardSessionMaxAgeMs
|
|
687
|
+
&& boardRoute;
|
|
688
|
+
if (!reusableBoard) state.loaded = false;
|
|
689
|
+
if (!state.loaded && !state.loading) {
|
|
690
|
+
state.loading = loadBoard(state).finally(() => { state.loading = null; });
|
|
691
|
+
}
|
|
564
692
|
record("board-shown");
|
|
565
693
|
};
|
|
566
694
|
const layoutBoard = (window, bounds = null) => {
|
|
@@ -661,43 +789,132 @@ if (
|
|
|
661
789
|
const attachContents = (contents) => {
|
|
662
790
|
if (!contents || contents.isDestroyed() || contents.__impelDesktopTasksAttached || internalContents.has(contents)) return;
|
|
663
791
|
contents.__impelDesktopTasksAttached = true;
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
792
|
+
record("renderer-attached", {
|
|
793
|
+
type: typeof contents.getType === "function" ? contents.getType() : null,
|
|
794
|
+
hasMainFrame: Boolean(contents.mainFrame),
|
|
795
|
+
});
|
|
796
|
+
const navigationNonce = randomBytes(32).toString("base64url");
|
|
797
|
+
const contentsNavSource = navSource.replace(
|
|
798
|
+
JSON.stringify(navigationNoncePlaceholder),
|
|
799
|
+
JSON.stringify(navigationNonce),
|
|
800
|
+
);
|
|
801
|
+
let injectionPending = false;
|
|
802
|
+
let navigationReady = false;
|
|
803
|
+
let injectionAttempts = 0;
|
|
804
|
+
let injectionTimer = null;
|
|
805
|
+
const inject = () => {
|
|
806
|
+
if (navigationReady || injectionPending || internalContents.has(contents) || contents.isDestroyed()) return;
|
|
807
|
+
injectionAttempts += 1;
|
|
808
|
+
const frame = contents.mainFrame;
|
|
809
|
+
if (!frame || typeof frame.executeJavaScript !== "function") {
|
|
810
|
+
if (injectionAttempts < 120) scheduleInject(500);
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
injectionPending = true;
|
|
814
|
+
frame.executeJavaScript(contentsNavSource, true)
|
|
815
|
+
.then(async (result) => {
|
|
668
816
|
const window = ownerWindow(contents);
|
|
669
817
|
if (!window || window !== primaryHostWindow()) return;
|
|
670
818
|
const state = stateFor(window);
|
|
671
|
-
|
|
672
|
-
|
|
819
|
+
record("navigation-probed", result || null);
|
|
820
|
+
const placementReady = Boolean(
|
|
821
|
+
result?.placement?.tasks
|
|
822
|
+
&& result?.placement?.code
|
|
823
|
+
&& result?.placement?.new
|
|
824
|
+
&& result?.placement?.customize,
|
|
825
|
+
);
|
|
826
|
+
navigationReady = Boolean(result?.connected && (!smokeMode || placementReady));
|
|
827
|
+
if (result?.anchor) {
|
|
673
828
|
markDomNavigation(window);
|
|
674
|
-
} else if (
|
|
675
|
-
|
|
676
|
-
|
|
829
|
+
} else if (result?.shellReady && !state.hasDomNavigation && !hasDomNavigation && nativeFallbackEnabled) {
|
|
830
|
+
ensureNativeNav(window);
|
|
831
|
+
}
|
|
832
|
+
if (
|
|
833
|
+
smokeMode
|
|
834
|
+
&& placementReady
|
|
835
|
+
&& !state.smokeStarted
|
|
836
|
+
) {
|
|
837
|
+
state.smokeStarted = true;
|
|
838
|
+
await captureSmokeProof("claude-navigation.png", window);
|
|
839
|
+
let clicked = false;
|
|
840
|
+
try {
|
|
841
|
+
clicked = await frame.executeJavaScript(
|
|
842
|
+
"Boolean(document.getElementById('impel-desktop-tasks-nav')?.click?.() || true)",
|
|
843
|
+
true,
|
|
844
|
+
);
|
|
845
|
+
} catch {}
|
|
846
|
+
record("navigation-smoke-clicked", { clicked: Boolean(clicked), placement: result.placement || null });
|
|
847
|
+
}
|
|
677
848
|
})
|
|
678
|
-
.catch(() => record("navigation-injection-failed"))
|
|
849
|
+
.catch(() => record("navigation-injection-failed"))
|
|
850
|
+
.finally(() => {
|
|
851
|
+
injectionPending = false;
|
|
852
|
+
if (!navigationReady && injectionAttempts < 120 && !contents.isDestroyed()) scheduleInject(500);
|
|
853
|
+
});
|
|
854
|
+
};
|
|
855
|
+
// Claude can create its renderer while this main preload is still being
|
|
856
|
+
// installed. In that race isLoadingMainFrame() is true but the one
|
|
857
|
+
// did-finish-load edge has already crossed, leaving the injection dormant
|
|
858
|
+
// forever. Probe immediately and at each renderer-ready edge; navSource
|
|
859
|
+
// is idempotent and its MutationObserver handles the later SPA mount.
|
|
860
|
+
const scheduleInject = (delay = 0) => {
|
|
861
|
+
if (navigationReady || injectionPending || injectionTimer !== null) return;
|
|
862
|
+
injectionTimer = setTimeout(() => {
|
|
863
|
+
injectionTimer = null;
|
|
864
|
+
inject();
|
|
865
|
+
}, delay);
|
|
679
866
|
};
|
|
680
|
-
contents.on("
|
|
681
|
-
contents.on("did-
|
|
682
|
-
|
|
683
|
-
|
|
867
|
+
contents.on("dom-ready", () => scheduleInject());
|
|
868
|
+
contents.on("did-finish-load", () => scheduleInject());
|
|
869
|
+
contents.once("destroyed", () => {
|
|
870
|
+
if (injectionTimer !== null) clearTimeout(injectionTimer);
|
|
871
|
+
injectionTimer = null;
|
|
872
|
+
});
|
|
873
|
+
scheduleInject();
|
|
874
|
+
const handledNavigationEvents = new Set();
|
|
875
|
+
let handledLegacyNavigation = null;
|
|
684
876
|
const handleNavigation = (event, url) => {
|
|
685
877
|
if (!String(url).startsWith("impel-tasks://")) return;
|
|
686
878
|
event.preventDefault();
|
|
687
879
|
const window = ownerWindow(contents);
|
|
688
880
|
if (!window || window !== primaryHostWindow()) return;
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
881
|
+
let navigation;
|
|
882
|
+
try { navigation = new URL(url); } catch { return; }
|
|
883
|
+
if (navigation.searchParams.get("nonce") !== navigationNonce) return;
|
|
884
|
+
const action = navigation.hostname;
|
|
885
|
+
const eventId = navigation.searchParams.get("event");
|
|
886
|
+
if (eventId) {
|
|
887
|
+
const navigationKey = action + ":" + eventId;
|
|
888
|
+
if (handledNavigationEvents.has(navigationKey)) return;
|
|
889
|
+
handledNavigationEvents.add(navigationKey);
|
|
890
|
+
if (handledNavigationEvents.size > 128) {
|
|
891
|
+
handledNavigationEvents.delete(handledNavigationEvents.values().next().value);
|
|
892
|
+
}
|
|
893
|
+
} else {
|
|
894
|
+
if (handledLegacyNavigation === url) return;
|
|
895
|
+
handledLegacyNavigation = url;
|
|
896
|
+
queueMicrotask(() => { if (handledLegacyNavigation === url) handledLegacyNavigation = null; });
|
|
897
|
+
}
|
|
694
898
|
if (action === "ready") markDomNavigation(window);
|
|
695
899
|
else if (action === "hide") hide(window);
|
|
696
900
|
else if (action === "show") show(window, parseBounds(url, window));
|
|
697
901
|
else if (action === "layout") layoutBoard(window, parseBounds(url, window));
|
|
698
902
|
};
|
|
699
|
-
|
|
700
|
-
|
|
903
|
+
const eventUrl = (event, deprecatedUrl) => (
|
|
904
|
+
typeof event?.url === "string" ? event.url : String(deprecatedUrl || "")
|
|
905
|
+
);
|
|
906
|
+
contents.on("will-navigate", (event, deprecatedUrl) => {
|
|
907
|
+
handleNavigation(event, eventUrl(event, deprecatedUrl));
|
|
908
|
+
});
|
|
909
|
+
contents.on("will-frame-navigate", (event, deprecatedUrl, _isInPlace, deprecatedIsMainFrame) => {
|
|
910
|
+
const url = eventUrl(event, deprecatedUrl);
|
|
911
|
+
if (!url.startsWith("impel-tasks://")) return;
|
|
912
|
+
event.preventDefault();
|
|
913
|
+
const isMainFrame = typeof event?.isMainFrame === "boolean"
|
|
914
|
+
? event.isMainFrame
|
|
915
|
+
: deprecatedIsMainFrame === true;
|
|
916
|
+
if (isMainFrame) handleNavigation(event, url);
|
|
917
|
+
});
|
|
701
918
|
};
|
|
702
919
|
const attachWindow = (window) => {
|
|
703
920
|
if (!window || window.isDestroyed() || window.getParentWindow?.() || window.__impelDesktopTasksWindowAttached) return;
|
|
@@ -1,6 +1,7 @@
|
|
|
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
|
+
// v42 restores Claude's embedded Tasks entry through its real sidebar, using
|
|
5
|
+
// the same fallback-free, tenant-bound board lifecycle already enabled for
|
|
6
|
+
// managed ChatGPT profiles.
|
|
7
|
+
export const CURRENT_CONFIG_VERSION = 42;
|