impel-cli 0.20.33 → 0.20.34

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,15 @@
1
1
  # Release notes
2
2
 
3
+ ## 0.20.34 — Stabilize desktop Tasks navigation
4
+
5
+ - Places Tasks immediately after Claude's persistent Home/Code switcher in both
6
+ modes and uses the New row as its visual template, so the destination no
7
+ longer jumps above the switcher when the selected mode changes.
8
+ - Prefers matching controls inside the host sidebar, preventing similarly named
9
+ content controls from moving the injected destination into the main surface.
10
+ - Preserves ChatGPT/Codex placement immediately before Pull requests and
11
+ rebuilds existing managed bundles onto the corrected navigation contract.
12
+
3
13
  ## 0.20.33 — Faster Codex native-agent relay
4
14
 
5
15
  - Uses the ultra-fast Codex Spark model at low reasoning for fixed-binding
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "impel-cli",
3
- "version": "0.20.33",
3
+ "version": "0.20.34",
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
@@ -299,7 +299,7 @@ export const CURRENT_CONFIG_VERSION = 32;
299
299
  // — which is what made every `impel update` re-trigger macOS permission
300
300
  // prompts. Bump this ONLY when a code change alters the bytes of a built
301
301
  // bundle; leave it alone for changes that don't touch bundle contents.
302
- export const BUNDLE_BUILD_FINGERPRINT = "bundle-2026-08-08.4";
302
+ export const BUNDLE_BUILD_FINGERPRINT = "bundle-2026-08-09.3";
303
303
 
304
304
  /** Parse the tenant's install manifest, or null when absent/corrupt. */
305
305
  export function readTenantManifest(homeDir = os.homedir(), tenantId = null) {
@@ -2,6 +2,19 @@ import path from "node:path";
2
2
 
3
3
  export const DESKTOP_TASKS_RESOURCE_URI = "ui://impel/tasks/v1/index.html";
4
4
 
5
+ // Claude's mode switch is the stable landmark across Home and Code, but its
6
+ // plain New row is the visual template. ChatGPT/Codex keeps the established
7
+ // position immediately before Pull requests.
8
+ export const DESKTOP_TASKS_NAV_ANCHOR_RULES = Object.freeze([
9
+ { pattern: "^pull requests?\\b", position: "before" },
10
+ { pattern: "^code\\b", position: "after", template: "^new\\b" },
11
+ { pattern: "^(?:start\\s+)?new\\s+(?:chat|thread)\\b", position: "before" },
12
+ { pattern: "^new\\b", position: "before" },
13
+ { pattern: "^scheduled\\b", position: "before" },
14
+ { pattern: "^plugins?\\b", position: "before" },
15
+ { pattern: "^cowork\\b", position: "before" },
16
+ ]);
17
+
5
18
  export function desktopTasksAssetPaths(root) {
6
19
  const directory = path.join(root, "desktop-tasks");
7
20
  return {
@@ -12,10 +25,17 @@ export function desktopTasksAssetPaths(root) {
12
25
  };
13
26
  }
14
27
 
28
+ const NAV_ANCHOR_RULES_SOURCE = JSON.stringify(DESKTOP_TASKS_NAV_ANCHOR_RULES);
29
+
15
30
  const NAVIGATION_SOURCE = String.raw`(() => {
16
31
  const NAV_ID = "impel-desktop-tasks-nav";
17
32
  const NAV_SLOT_ID = "impel-desktop-tasks-nav-slot";
18
33
  const ACTIVE_CLASS = "impel-desktop-tasks-active";
34
+ const NAV_ANCHOR_RULES = ${NAV_ANCHOR_RULES_SOURCE}.map((rule) => ({
35
+ ...rule,
36
+ pattern: new RegExp(rule.pattern, "i"),
37
+ template: rule.template ? new RegExp(rule.template, "i") : null,
38
+ }));
19
39
  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>';
20
40
  if (window.__impelDesktopTasksCleanup) window.__impelDesktopTasksCleanup();
21
41
 
@@ -56,6 +76,12 @@ const NAVIGATION_SOURCE = String.raw`(() => {
56
76
 
57
77
  const findControl = (pattern) => {
58
78
  const matches = controls().filter((element) => pattern.test(label(element)));
79
+ const sidebar = findSidebar();
80
+ if (sidebar) {
81
+ const sidebarMatch = matches.find((element) => sidebar.contains(element) && visible(element))
82
+ || matches.find((element) => sidebar.contains(element));
83
+ if (sidebarMatch) return sidebarMatch;
84
+ }
59
85
  return matches.find(visible) || matches[0] || null;
60
86
  };
61
87
 
@@ -134,9 +160,9 @@ const NAVIGATION_SOURCE = String.raw`(() => {
134
160
  let changed = false;
135
161
  while (walker.nextNode()) {
136
162
  const text = walker.currentNode.nodeValue || "";
137
- if (!/(?:new\s+(?:chat|thread)|pull requests?|scheduled|plugins?|skills?|cowork|code)\b/i.test(text)) continue;
163
+ if (!/(?:new(?:\s+(?:chat|thread))?|pull requests?|scheduled|plugins?|skills?|cowork|code)\b/i.test(text)) continue;
138
164
  walker.currentNode.nodeValue = text.replace(
139
- /(?:new\s+(?:chat|thread)|pull requests?|scheduled|plugins?|skills?|cowork|code)\b/i,
165
+ /(?:new(?:\s+(?:chat|thread))?|pull requests?|scheduled|plugins?|skills?|cowork|code)\b/i,
140
166
  "Tasks",
141
167
  );
142
168
  changed = true;
@@ -195,32 +221,38 @@ const NAVIGATION_SOURCE = String.raw`(() => {
195
221
 
196
222
  const placement = (control) => {
197
223
  if (!control) return null;
224
+ const controlRect = control.getBoundingClientRect();
198
225
  let child = control;
199
226
  let parent = control.parentElement;
200
227
  while (parent && parent !== document.body) {
201
- const style = getComputedStyle(parent);
202
- if (style.display === "flex" && style.flexDirection === "column") {
203
- return { stack: parent, child };
204
- }
228
+ const rect = parent.getBoundingClientRect();
229
+ const sameVisualRow = Math.abs(rect.top - controlRect.top) <= 8
230
+ && Math.abs(rect.bottom - controlRect.bottom) <= 8;
231
+ if (!sameVisualRow) break;
205
232
  child = parent;
206
233
  parent = parent.parentElement;
207
234
  }
208
- return null;
235
+ return parent && parent !== document.body ? { stack: parent, child } : null;
209
236
  };
210
237
 
211
238
  const ensureNav = () => {
212
- const anchor = findControl(/^pull requests?\b/i)
213
- || findControl(/^(?:start\s+)?new\s+(?:chat|thread)\b/i)
214
- || findControl(/^scheduled\b/i)
215
- || findControl(/^plugins?\b/i)
216
- || findControl(/^cowork\b/i)
217
- || findControl(/^code\b/i);
239
+ let anchorRule = null;
240
+ let anchor = null;
241
+ for (const rule of NAV_ANCHOR_RULES) {
242
+ anchor = findControl(rule.pattern);
243
+ if (anchor) {
244
+ anchorRule = rule;
245
+ break;
246
+ }
247
+ }
248
+ const template = anchorRule?.template ? findControl(anchorRule.template) || anchor : anchor;
218
249
  lastAnchor = anchor;
219
- if (!nav?.isConnected) nav = makeNav(anchor);
250
+ if (!nav?.isConnected) nav = makeNav(template);
220
251
  const target = placement(anchor);
252
+ const templateTarget = placement(template);
221
253
  if (target) {
222
254
  if (!slot?.isConnected || (slot !== nav && !slot.contains(nav))) {
223
- slot = target.child === anchor ? nav : target.child.cloneNode(false);
255
+ slot = templateTarget?.child === template ? nav : templateTarget?.child.cloneNode(false) || nav;
224
256
  if (slot !== nav) {
225
257
  slot.id = NAV_SLOT_ID;
226
258
  slot.removeAttribute("aria-label");
@@ -228,11 +260,14 @@ const NAVIGATION_SOURCE = String.raw`(() => {
228
260
  slot.append(nav);
229
261
  }
230
262
  }
231
- if (slot.parentElement !== target.stack || slot.nextElementSibling !== target.child) {
232
- target.stack.insertBefore(slot, target.child);
263
+ const after = anchorRule?.position === "after";
264
+ const correctlyPlaced = slot.parentElement === target.stack
265
+ && (after ? slot.previousElementSibling === target.child : slot.nextElementSibling === target.child);
266
+ if (!correctlyPlaced) {
267
+ target.stack.insertBefore(slot, after ? target.child.nextElementSibling : target.child);
233
268
  }
234
269
  } else if (anchor?.parentElement) {
235
- anchor.insertAdjacentElement("afterend", nav);
270
+ anchor.insertAdjacentElement(anchorRule?.position === "after" ? "afterend" : "beforebegin", nav);
236
271
  slot = nav;
237
272
  } else if (!nav.isConnected) {
238
273
  slot = null;