impel-cli 0.20.50 → 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 CHANGED
@@ -1,5 +1,18 @@
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
+
3
16
  ## 0.20.50 — Restore ChatGPT Tasks safely
4
17
 
5
18
  - Restores one embedded Tasks entry in the managed ChatGPT sidebar while
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "impel-cli",
3
- "version": "0.20.50",
3
+ "version": "0.20.51",
4
4
  "description": "Prepare isolated Claude and Codex workspaces for every accessible Impel tenant",
5
5
  "type": "module",
6
6
  "bin": {
package/src/apps.js CHANGED
@@ -882,7 +882,7 @@ export function installManagedAppFiles({
882
882
  writeDesktopTasksProfileAssets(
883
883
  paths[target].root,
884
884
  config.appUrl || RUNTIME_BRAND.controlPlane.defaultOrigin,
885
- { enableEmbed: target === "chatgpt" },
885
+ { enableEmbed: true },
886
886
  );
887
887
  // Non-bundle targets are the background-refresh / fast-open path: configs,
888
888
  // token helper, catalog, and manifest only. Bundle swaps require the app
@@ -286,10 +286,31 @@ const NAVIGATION_SOURCE = String.raw`(() => {
286
286
  delete window.__impelDesktopTasksSetActive;
287
287
  };
288
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
+ };
289
304
  return {
290
305
  anchor: lastAnchor ? label(lastAnchor) : null,
291
306
  connected: Boolean(nav?.isConnected),
292
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
+ },
293
314
  };
294
315
  })();`;
295
316
 
@@ -326,17 +347,24 @@ if (
326
347
  const tasksUrl = appOrigin + ${JSON.stringify(DESKTOP_TASKS_BOARD_PATH)};
327
348
  const readyMarker = ${JSON.stringify(DESKTOP_TASKS_READY_MARKER)};
328
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 || "";
329
352
  const boardSessionMaxAgeMs = 7 * 60 * 60 * 1000;
330
353
  const tenantId = process.env.IMPEL_DESKTOP_TASKS_TENANT;
331
354
  const nativeShowChannel = "impel-desktop-tasks:native-show";
332
355
  const runtimeStatusPath = path.join(__dirname, "runtime-status.json");
333
356
  const escapeRegex = ${escapeRegexSource};
334
357
  const parseCredential = ${parserSource};
335
- const record = (stage) => {
358
+ const runtimeHistory = [];
359
+ const record = (stage, details = null) => {
336
360
  try {
361
+ runtimeHistory.push({ stage, details, at: new Date().toISOString() });
362
+ if (runtimeHistory.length > 32) runtimeHistory.shift();
337
363
  fs.writeFileSync(runtimeStatusPath, JSON.stringify({
338
364
  schemaVersion: 2,
339
365
  stage,
366
+ details,
367
+ history: runtimeHistory,
340
368
  processType: process.type || null,
341
369
  electronVersion: process.versions.electron || null,
342
370
  pid: process.pid,
@@ -344,6 +372,14 @@ if (
344
372
  }, null, 2) + "\\n", { mode: 0o600 });
345
373
  } catch {}
346
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
+ };
347
383
  const token = () => new Promise((resolve, reject) => {
348
384
  const childEnvironment = { ...process.env };
349
385
  for (const name of ["ANTHROPIC_API_KEY", "CODEX_ACCESS_TOKEN", "CODEX_API_KEY", "OPENAI_API_KEY"]) {
@@ -410,7 +446,7 @@ if (
410
446
  const stateFor = (window) => {
411
447
  let state = windows.get(window.id);
412
448
  if (!state) {
413
- state = { view: null, bounds: null, visible: false, nativeNav: null, hasDomNavigation: false, loaded: false, loadedAt: 0, 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 };
414
450
  windows.set(window.id, state);
415
451
  }
416
452
  return state;
@@ -496,6 +532,35 @@ if (
496
532
  state.loaded = true;
497
533
  state.loadedAt = Date.now();
498
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
+ }
499
564
  } catch {
500
565
  state.loaded = false;
501
566
  state.loadedAt = 0;
@@ -724,30 +789,88 @@ if (
724
789
  const attachContents = (contents) => {
725
790
  if (!contents || contents.isDestroyed() || contents.__impelDesktopTasksAttached || internalContents.has(contents)) return;
726
791
  contents.__impelDesktopTasksAttached = true;
792
+ record("renderer-attached", {
793
+ type: typeof contents.getType === "function" ? contents.getType() : null,
794
+ hasMainFrame: Boolean(contents.mainFrame),
795
+ });
727
796
  const navigationNonce = randomBytes(32).toString("base64url");
728
797
  const contentsNavSource = navSource.replace(
729
798
  JSON.stringify(navigationNoncePlaceholder),
730
799
  JSON.stringify(navigationNonce),
731
800
  );
801
+ let injectionPending = false;
802
+ let navigationReady = false;
803
+ let injectionAttempts = 0;
804
+ let injectionTimer = null;
732
805
  const inject = () => {
733
- if (internalContents.has(contents)) return;
806
+ if (navigationReady || injectionPending || internalContents.has(contents) || contents.isDestroyed()) return;
807
+ injectionAttempts += 1;
734
808
  const frame = contents.mainFrame;
735
- if (!frame || typeof frame.executeJavaScript !== "function") return;
809
+ if (!frame || typeof frame.executeJavaScript !== "function") {
810
+ if (injectionAttempts < 120) scheduleInject(500);
811
+ return;
812
+ }
813
+ injectionPending = true;
736
814
  frame.executeJavaScript(contentsNavSource, true)
737
- .then((result) => {
815
+ .then(async (result) => {
738
816
  const window = ownerWindow(contents);
739
817
  if (!window || window !== primaryHostWindow()) return;
740
818
  const state = stateFor(window);
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));
741
827
  if (result?.anchor) {
742
828
  markDomNavigation(window);
743
829
  } else if (result?.shellReady && !state.hasDomNavigation && !hasDomNavigation && nativeFallbackEnabled) {
744
830
  ensureNativeNav(window);
745
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
+ }
746
848
  })
747
- .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
+ });
748
854
  };
749
- contents.on("did-finish-load", () => inject());
750
- if (!contents.isLoadingMainFrame()) setTimeout(() => inject(), 0);
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);
866
+ };
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();
751
874
  const handledNavigationEvents = new Set();
752
875
  let handledLegacyNavigation = null;
753
876
  const handleNavigation = (event, url) => {
@@ -1,8 +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
- // 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;
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;