impel-cli 0.20.46-beta.0 → 0.20.46
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/README.md +14 -8
- package/RELEASE_NOTES.md +65 -0
- package/docs/native-agent-host-capability-matrix.md +3 -3
- package/package.json +1 -1
- package/src/agents.js +4 -4
- package/src/apps.js +76 -28
- package/src/codexSecurity.js +0 -18
- package/src/commands/apps.js +25 -11
- package/src/desktopTasks.js +420 -787
- package/src/doctor.js +17 -1
- package/src/managedProfileVersion.js +3 -1
package/src/desktopTasks.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
import { RUNTIME_BRAND } from "./runtimeBrand.js";
|
|
4
|
+
|
|
5
|
+
export const DESKTOP_TASKS_BOOTSTRAP_PATH = "/api/cli/desktop-session";
|
|
6
|
+
export const DESKTOP_TASKS_BOARD_PATH = "/tasks";
|
|
7
|
+
export const DESKTOP_TASKS_READY_MARKER = "IMPEL_DESKTOP_SESSION_READY";
|
|
4
8
|
|
|
5
9
|
// Claude's mode switch is the stable landmark across Home and Code, but its
|
|
6
10
|
// plain New row is the visual template. ChatGPT/Codex keeps the established
|
|
@@ -21,130 +25,139 @@ export function desktopTasksAssetPaths(root) {
|
|
|
21
25
|
directory,
|
|
22
26
|
mainPreload: path.join(directory, "main-preload.cjs"),
|
|
23
27
|
viewPreload: path.join(directory, "view-preload.cjs"),
|
|
28
|
+
// Kept as an owned cleanup target while older bundle launch metadata still
|
|
29
|
+
// contains the ignored environment key. No host document is generated.
|
|
24
30
|
hostHtml: path.join(directory, "host.html"),
|
|
25
31
|
};
|
|
26
32
|
}
|
|
27
33
|
|
|
34
|
+
export function desktopTasksAppOrigin(value) {
|
|
35
|
+
let url;
|
|
36
|
+
try {
|
|
37
|
+
url = new URL(String(value));
|
|
38
|
+
} catch {
|
|
39
|
+
throw new Error("desktop Tasks app URL is invalid");
|
|
40
|
+
}
|
|
41
|
+
if (
|
|
42
|
+
!["http:", "https:"].includes(url.protocol)
|
|
43
|
+
|| url.username
|
|
44
|
+
|| url.password
|
|
45
|
+
) {
|
|
46
|
+
throw new Error("desktop Tasks app URL must be an HTTP(S) origin without credentials");
|
|
47
|
+
}
|
|
48
|
+
return url.origin;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function escapeRegex(value) {
|
|
52
|
+
return String(value).replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function parseDesktopTasksCredential(output, tenantId, {
|
|
56
|
+
patPrefix = RUNTIME_BRAND.auth.patPrefix,
|
|
57
|
+
tenantPrefix = RUNTIME_BRAND.auth.tenantPrefix,
|
|
58
|
+
} = {}) {
|
|
59
|
+
if (typeof output !== "string" || output.length === 0 || output.length > 8192) {
|
|
60
|
+
throw new Error("desktop Tasks token helper returned invalid output");
|
|
61
|
+
}
|
|
62
|
+
const line = output.endsWith("\n") ? output.slice(0, -1) : output;
|
|
63
|
+
if (!line || /[\r\n\u0000-\u001f\u007f]/u.test(line)) {
|
|
64
|
+
throw new Error("desktop Tasks token helper returned invalid output");
|
|
65
|
+
}
|
|
66
|
+
const pattern = new RegExp(
|
|
67
|
+
`^${escapeRegex(tenantPrefix)}([A-Za-z0-9_-]+)\\.(${escapeRegex(patPrefix)}[A-Za-z0-9_-]+(?:\\.[A-Za-z0-9_-]+)?)$`,
|
|
68
|
+
"u",
|
|
69
|
+
);
|
|
70
|
+
const match = pattern.exec(line);
|
|
71
|
+
if (!match) throw new Error("desktop Tasks token helper returned invalid output");
|
|
72
|
+
|
|
73
|
+
let decodedTenant;
|
|
74
|
+
try {
|
|
75
|
+
decodedTenant = Buffer.from(match[1], "base64url").toString("utf8");
|
|
76
|
+
} catch {
|
|
77
|
+
throw new Error("desktop Tasks token helper returned invalid output");
|
|
78
|
+
}
|
|
79
|
+
if (
|
|
80
|
+
Buffer.from(decodedTenant, "utf8").toString("base64url") !== match[1]
|
|
81
|
+
|| decodedTenant !== tenantId
|
|
82
|
+
) {
|
|
83
|
+
throw new Error("desktop Tasks token helper tenant did not match");
|
|
84
|
+
}
|
|
85
|
+
return match[2];
|
|
86
|
+
}
|
|
87
|
+
|
|
28
88
|
const NAV_ANCHOR_RULES_SOURCE = JSON.stringify(DESKTOP_TASKS_NAV_ANCHOR_RULES);
|
|
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>';
|
|
29
90
|
|
|
30
91
|
const NAVIGATION_SOURCE = String.raw`(() => {
|
|
31
92
|
const NAV_ID = "impel-desktop-tasks-nav";
|
|
32
|
-
const
|
|
93
|
+
const SLOT_ID = "impel-desktop-tasks-nav-slot";
|
|
33
94
|
const ACTIVE_CLASS = "impel-desktop-tasks-active";
|
|
34
|
-
const
|
|
95
|
+
const RULES = ${NAV_ANCHOR_RULES_SOURCE}.map((rule) => ({
|
|
35
96
|
...rule,
|
|
36
97
|
pattern: new RegExp(rule.pattern, "i"),
|
|
37
98
|
template: rule.template ? new RegExp(rule.template, "i") : null,
|
|
38
99
|
}));
|
|
39
|
-
|
|
40
|
-
if (window.__impelDesktopTasksCleanup) window.__impelDesktopTasksCleanup();
|
|
100
|
+
window.__impelDesktopTasksCleanup?.();
|
|
41
101
|
|
|
42
102
|
let nav = null;
|
|
43
103
|
let slot = null;
|
|
44
104
|
let active = false;
|
|
45
105
|
let lastAnchor = null;
|
|
46
|
-
|
|
106
|
+
const roots = () => {
|
|
107
|
+
const result = [document];
|
|
108
|
+
for (let index = 0; index < result.length; index += 1) {
|
|
109
|
+
for (const element of result[index].querySelectorAll("*")) {
|
|
110
|
+
if (element.shadowRoot && !result.includes(element.shadowRoot)) result.push(element.shadowRoot);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return result;
|
|
114
|
+
};
|
|
115
|
+
const all = (selector) => roots().flatMap((root) => [...root.querySelectorAll(selector)]);
|
|
47
116
|
const label = (element) => [
|
|
48
117
|
element.getAttribute("aria-label"),
|
|
49
118
|
element.getAttribute("title"),
|
|
50
119
|
element.textContent,
|
|
51
120
|
].filter(Boolean).join(" ").replace(/\s+/g, " ").trim();
|
|
52
|
-
|
|
53
121
|
const visible = (element) => {
|
|
54
122
|
const rect = element.getBoundingClientRect();
|
|
55
123
|
const style = getComputedStyle(element);
|
|
56
|
-
return rect.width > 0 && rect.height > 0 &&
|
|
57
|
-
&& rect.left < innerWidth && rect.top < innerHeight
|
|
58
|
-
&& style.display !== "none" && style.visibility !== "hidden";
|
|
59
|
-
};
|
|
60
|
-
|
|
61
|
-
const roots = () => {
|
|
62
|
-
const found = [document];
|
|
63
|
-
for (let index = 0; index < found.length; index += 1) {
|
|
64
|
-
for (const element of found[index].querySelectorAll("*")) {
|
|
65
|
-
if (element.shadowRoot && !found.includes(element.shadowRoot)) found.push(element.shadowRoot);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
return found;
|
|
124
|
+
return rect.width > 0 && rect.height > 0 && style.display !== "none" && style.visibility !== "hidden";
|
|
69
125
|
};
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
const controls = () => queryAll("button, a, [role=\"button\"], [role=\"link\"], [tabindex]")
|
|
75
|
-
.filter((element) => !element.closest("#" + NAV_ID + ", #" + NAV_SLOT_ID));
|
|
76
|
-
|
|
126
|
+
const sidebar = () => all("aside.app-shell-left-panel, aside, [data-app-shell-left-panel], nav[aria-label]")
|
|
127
|
+
.find(visible) || null;
|
|
128
|
+
const controls = () => all('button, a, [role="button"], [role="link"], [tabindex]')
|
|
129
|
+
.filter((element) => !element.closest("#" + NAV_ID + ", #" + SLOT_ID));
|
|
77
130
|
const findControl = (pattern) => {
|
|
78
|
-
const
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
return matches.find(visible) || matches[0] || null;
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
const findSidebar = () => {
|
|
89
|
-
const explicit = queryFirst("aside.app-shell-left-panel, aside, [data-app-shell-left-panel], nav[aria-label]");
|
|
90
|
-
if (explicit && visible(explicit)) return explicit;
|
|
91
|
-
return null;
|
|
131
|
+
const candidates = controls().filter((element) => pattern.test(label(element)));
|
|
132
|
+
const side = sidebar();
|
|
133
|
+
return candidates.find((element) => side?.contains(element) && visible(element))
|
|
134
|
+
|| candidates.find((element) => side?.contains(element))
|
|
135
|
+
|| candidates.find(visible)
|
|
136
|
+
|| candidates[0]
|
|
137
|
+
|| null;
|
|
92
138
|
};
|
|
93
|
-
|
|
94
|
-
const findMain = () => {
|
|
95
|
-
const candidates = [
|
|
96
|
-
queryFirst("main[data-app-shell-main-surface]"),
|
|
97
|
-
queryFirst("main.main-surface"),
|
|
98
|
-
queryFirst("main"),
|
|
99
|
-
queryFirst("[role=\"main\"]"),
|
|
100
|
-
].filter(Boolean);
|
|
101
|
-
return candidates.find(visible) || candidates[0] || null;
|
|
102
|
-
};
|
|
103
|
-
|
|
104
139
|
const mainBounds = () => {
|
|
105
|
-
const main =
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
const side = sidebar?.getBoundingClientRect();
|
|
140
|
+
const main = all('main[data-app-shell-main-surface], main.main-surface, main, [role="main"]')
|
|
141
|
+
.find(visible);
|
|
142
|
+
const side = sidebar()?.getBoundingClientRect();
|
|
109
143
|
const anchor = lastAnchor?.getBoundingClientRect();
|
|
110
|
-
const inferred = anchor
|
|
111
|
-
&& anchor.left < 48
|
|
112
|
-
&& anchor.right >= 180
|
|
113
|
-
&& anchor.right < innerWidth * 0.55
|
|
114
|
-
? anchor.right + 8
|
|
115
|
-
: 0;
|
|
116
144
|
const sidebarRight = side && side.left <= 8 && side.width < innerWidth * 0.55
|
|
117
145
|
? side.right
|
|
118
|
-
:
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
rect = {
|
|
123
|
-
left: sidebarRight,
|
|
124
|
-
top: rect.top,
|
|
125
|
-
width: innerWidth - sidebarRight,
|
|
126
|
-
height: rect.height,
|
|
127
|
-
};
|
|
128
|
-
}
|
|
146
|
+
: anchor && anchor.left < 48 && anchor.right < innerWidth * 0.55
|
|
147
|
+
? anchor.right + 8
|
|
148
|
+
: 0;
|
|
149
|
+
const rect = main?.getBoundingClientRect();
|
|
129
150
|
return {
|
|
130
|
-
x: Math.max(0, Math.round(rect.left)),
|
|
131
|
-
y: Math.max(0, Math.round(rect.top)),
|
|
132
|
-
width: Math.max(320, Math.round(
|
|
133
|
-
height: Math.max(320, Math.round(rect.height)),
|
|
151
|
+
x: Math.max(0, Math.round(rect && rect.width >= 240 ? Math.max(rect.left, sidebarRight) : sidebarRight)),
|
|
152
|
+
y: Math.max(0, Math.round(rect && rect.height >= 240 ? rect.top : 0)),
|
|
153
|
+
width: Math.max(320, Math.round(innerWidth - sidebarRight)),
|
|
154
|
+
height: Math.max(320, Math.round(rect && rect.height >= 240 ? rect.height : innerHeight)),
|
|
134
155
|
};
|
|
135
156
|
};
|
|
136
|
-
|
|
137
157
|
const signal = (action) => {
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
x: String(bounds.x),
|
|
141
|
-
y: String(bounds.y),
|
|
142
|
-
width: String(bounds.width),
|
|
143
|
-
height: String(bounds.height),
|
|
144
|
-
});
|
|
145
|
-
location.href = "impel-tasks://" + action + "?" + query.toString();
|
|
158
|
+
const query = new URLSearchParams(Object.entries(mainBounds()).map(([key, value]) => [key, String(value)]));
|
|
159
|
+
location.href = "impel-tasks://" + action + "?" + query;
|
|
146
160
|
};
|
|
147
|
-
|
|
148
161
|
const select = (selected) => {
|
|
149
162
|
active = selected;
|
|
150
163
|
if (!nav) return;
|
|
@@ -154,195 +167,90 @@ const NAVIGATION_SOURCE = String.raw`(() => {
|
|
|
154
167
|
nav.classList.toggle(ACTIVE_CLASS, selected);
|
|
155
168
|
};
|
|
156
169
|
window.__impelDesktopTasksSetActive = select;
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
let
|
|
161
|
-
while (
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
changed = true;
|
|
170
|
+
const visualRow = (control) => {
|
|
171
|
+
const controlRect = control.getBoundingClientRect();
|
|
172
|
+
let child = control;
|
|
173
|
+
let parent = control.parentElement;
|
|
174
|
+
while (parent && parent !== document.body) {
|
|
175
|
+
const rect = parent.getBoundingClientRect();
|
|
176
|
+
const sameVisualRow = Math.abs(rect.top - controlRect.top) <= 8
|
|
177
|
+
&& Math.abs(rect.bottom - controlRect.bottom) <= 8;
|
|
178
|
+
if (!sameVisualRow) break;
|
|
179
|
+
child = parent;
|
|
180
|
+
parent = parent.parentElement;
|
|
169
181
|
}
|
|
170
|
-
return
|
|
182
|
+
return parent && parent !== document.body ? { parent, child } : null;
|
|
171
183
|
};
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
for (const candidate of [
|
|
184
|
+
const makeNav = (template) => {
|
|
185
|
+
const button = template.cloneNode(true);
|
|
186
|
+
for (const candidate of [button, ...button.querySelectorAll("*")]) {
|
|
175
187
|
for (const attribute of [...candidate.attributes]) {
|
|
176
188
|
if (attribute.name.startsWith("data-app-action-") || attribute.name === "data-testid") {
|
|
177
189
|
candidate.removeAttribute(attribute.name);
|
|
178
190
|
}
|
|
179
191
|
}
|
|
180
192
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
};
|
|
186
|
-
|
|
187
|
-
const stripTemplateGlyphs = (element) => {
|
|
188
|
-
element.querySelectorAll("svg, [aria-hidden=\"true\"]").forEach((candidate) => candidate.remove());
|
|
189
|
-
const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
|
|
190
|
-
const glyphs = [];
|
|
191
|
-
while (walker.nextNode()) {
|
|
192
|
-
if (/^(?:\+|+|<\/>|<>)$/u.test((walker.currentNode.nodeValue || "").trim())) {
|
|
193
|
-
glyphs.push(walker.currentNode);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
glyphs.forEach((candidate) => {
|
|
197
|
-
const parent = candidate.parentElement;
|
|
198
|
-
candidate.remove();
|
|
199
|
-
if (
|
|
200
|
-
parent
|
|
201
|
-
&& parent !== element
|
|
202
|
-
&& !parent.textContent?.trim()
|
|
203
|
-
&& !parent.querySelector("svg, img")
|
|
204
|
-
) parent.remove();
|
|
205
|
-
});
|
|
206
|
-
[...element.querySelectorAll("*")].reverse().forEach((candidate) => {
|
|
207
|
-
if (
|
|
208
|
-
!candidate.textContent?.trim()
|
|
209
|
-
&& !candidate.querySelector("svg, img, input")
|
|
210
|
-
) candidate.remove();
|
|
211
|
-
});
|
|
212
|
-
};
|
|
213
|
-
|
|
214
|
-
const makeNav = (template) => {
|
|
215
|
-
const button = template ? template.cloneNode(true) : document.createElement("button");
|
|
216
|
-
stripVendorActions(button);
|
|
217
|
-
stripTemplateGlyphs(button);
|
|
193
|
+
button.querySelectorAll('svg, [aria-hidden="true"], kbd, [data-slot="shortcut"]').forEach((node) => node.remove());
|
|
194
|
+
button.querySelectorAll("[id]").forEach((node) => node.removeAttribute("id"));
|
|
195
|
+
button.removeAttribute("href");
|
|
196
|
+
button.removeAttribute("target");
|
|
218
197
|
button.id = NAV_ID;
|
|
219
198
|
button.type = "button";
|
|
220
199
|
button.setAttribute("aria-label", "Tasks");
|
|
221
200
|
button.setAttribute("title", "Tasks");
|
|
222
|
-
button.
|
|
223
|
-
|
|
224
|
-
button.insertAdjacentHTML("afterbegin", TASKS_ICON);
|
|
201
|
+
button.textContent = "Tasks";
|
|
202
|
+
button.insertAdjacentHTML("afterbegin", ${JSON.stringify(TASKS_ICON)});
|
|
225
203
|
button.addEventListener("click", (event) => {
|
|
226
204
|
event.preventDefault();
|
|
227
205
|
event.stopPropagation();
|
|
228
206
|
select(true);
|
|
229
207
|
signal("show");
|
|
230
208
|
});
|
|
231
|
-
button.addEventListener("keydown", (event) => {
|
|
232
|
-
if (event.key !== "Enter" && event.key !== " ") return;
|
|
233
|
-
event.preventDefault();
|
|
234
|
-
select(true);
|
|
235
|
-
signal("show");
|
|
236
|
-
});
|
|
237
209
|
return button;
|
|
238
210
|
};
|
|
239
|
-
|
|
240
|
-
const placement = (control) => {
|
|
241
|
-
if (!control) return null;
|
|
242
|
-
const controlRect = control.getBoundingClientRect();
|
|
243
|
-
let child = control;
|
|
244
|
-
let parent = control.parentElement;
|
|
245
|
-
while (parent && parent !== document.body) {
|
|
246
|
-
const rect = parent.getBoundingClientRect();
|
|
247
|
-
const sameVisualRow = Math.abs(rect.top - controlRect.top) <= 8
|
|
248
|
-
&& Math.abs(rect.bottom - controlRect.bottom) <= 8;
|
|
249
|
-
if (!sameVisualRow) break;
|
|
250
|
-
child = parent;
|
|
251
|
-
parent = parent.parentElement;
|
|
252
|
-
}
|
|
253
|
-
return parent && parent !== document.body ? { stack: parent, child } : null;
|
|
254
|
-
};
|
|
255
|
-
|
|
256
211
|
const ensureNav = () => {
|
|
257
|
-
let
|
|
212
|
+
let rule = null;
|
|
258
213
|
let anchor = null;
|
|
259
|
-
for (const
|
|
260
|
-
anchor = findControl(
|
|
261
|
-
if (anchor) {
|
|
262
|
-
anchorRule = rule;
|
|
263
|
-
break;
|
|
264
|
-
}
|
|
214
|
+
for (const candidate of RULES) {
|
|
215
|
+
anchor = findControl(candidate.pattern);
|
|
216
|
+
if (anchor) { rule = candidate; break; }
|
|
265
217
|
}
|
|
266
|
-
const template =
|
|
218
|
+
const template = rule?.template ? findControl(rule.template) : anchor;
|
|
267
219
|
lastAnchor = anchor;
|
|
268
|
-
// Claude mounts its Home/Code switcher before the New row. Do not clone the
|
|
269
|
-
// switcher as a temporary fallback: its Code glyph would remain in the
|
|
270
|
-
// injected control after the intended visual template appears.
|
|
271
220
|
if (!anchor || !template) return;
|
|
272
221
|
if (!nav?.isConnected) nav = makeNav(template);
|
|
273
|
-
const target =
|
|
274
|
-
const templateTarget =
|
|
222
|
+
const target = visualRow(anchor);
|
|
223
|
+
const templateTarget = visualRow(template);
|
|
275
224
|
if (target) {
|
|
276
225
|
if (!slot?.isConnected || (slot !== nav && !slot.contains(nav))) {
|
|
277
226
|
slot = templateTarget?.child === template ? nav : templateTarget?.child.cloneNode(false) || nav;
|
|
278
|
-
if (slot !== nav) {
|
|
279
|
-
slot.id = NAV_SLOT_ID;
|
|
280
|
-
slot.removeAttribute("aria-label");
|
|
281
|
-
slot.removeAttribute("title");
|
|
282
|
-
slot.append(nav);
|
|
283
|
-
}
|
|
227
|
+
if (slot !== nav) { slot.id = SLOT_ID; slot.append(nav); }
|
|
284
228
|
}
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
target.stack.insertBefore(slot, after ? target.child.nextElementSibling : target.child);
|
|
290
|
-
}
|
|
291
|
-
} else if (anchor?.parentElement) {
|
|
292
|
-
anchor.insertAdjacentElement(anchorRule?.position === "after" ? "afterend" : "beforebegin", nav);
|
|
229
|
+
const reference = rule.position === "after" ? target.child.nextElementSibling : target.child;
|
|
230
|
+
if (slot.parentElement !== target.parent || slot !== reference) target.parent.insertBefore(slot, reference);
|
|
231
|
+
} else if (anchor.parentElement) {
|
|
232
|
+
anchor.insertAdjacentElement(rule.position === "after" ? "afterend" : "beforebegin", nav);
|
|
293
233
|
slot = nav;
|
|
294
|
-
} else if (!nav.isConnected) {
|
|
295
|
-
slot = null;
|
|
296
234
|
}
|
|
297
235
|
select(active);
|
|
298
236
|
};
|
|
299
|
-
|
|
300
237
|
const style = document.createElement("style");
|
|
301
|
-
style.
|
|
302
|
-
style.textContent = [
|
|
303
|
-
"#" + NAV_ID + " svg { width: 16px; height: 16px; flex: 0 0 auto; }",
|
|
304
|
-
"#" + NAV_ID + "." + ACTIVE_CLASS + " { background: color-mix(in srgb, CanvasText 10%, transparent); }",
|
|
305
|
-
"#" + NAV_ID + ":focus-visible { outline: 2px solid #7c8cff; outline-offset: 2px; }",
|
|
306
|
-
].join("\n");
|
|
238
|
+
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}";
|
|
307
239
|
document.head.append(style);
|
|
308
|
-
|
|
309
240
|
const onClick = (event) => {
|
|
310
241
|
if (!active || event.target instanceof Element && event.target.closest("#" + NAV_ID)) return;
|
|
311
|
-
const
|
|
312
|
-
if (
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
const onResize = () => {
|
|
317
|
-
if (active) signal("layout");
|
|
242
|
+
const side = sidebar();
|
|
243
|
+
if (side && event.target instanceof Node && side.contains(event.target)) {
|
|
244
|
+
select(false);
|
|
245
|
+
setTimeout(() => signal("hide"), 0);
|
|
246
|
+
}
|
|
318
247
|
};
|
|
248
|
+
const onResize = () => { if (active) signal("layout"); };
|
|
319
249
|
window.addEventListener("click", onClick, true);
|
|
320
250
|
window.addEventListener("resize", onResize);
|
|
321
|
-
|
|
322
|
-
const observer = new MutationObserver((records) => {
|
|
323
|
-
const external = records.some((record) => {
|
|
324
|
-
const target = record.target instanceof Element ? record.target : record.target?.parentElement;
|
|
325
|
-
return target && !target.closest("#" + NAV_ID + ", #" + NAV_SLOT_ID);
|
|
326
|
-
});
|
|
327
|
-
if (external) ensureNav();
|
|
328
|
-
});
|
|
251
|
+
const observer = new MutationObserver(() => ensureNav());
|
|
329
252
|
observer.observe(document.body, { childList: true, subtree: true });
|
|
330
253
|
ensureNav();
|
|
331
|
-
|
|
332
|
-
const navRect = nav?.getBoundingClientRect();
|
|
333
|
-
const result = {
|
|
334
|
-
connected: Boolean(nav?.isConnected),
|
|
335
|
-
visible: Boolean(nav && visible(nav)),
|
|
336
|
-
text: nav?.textContent?.replace(/\s+/g, " ").trim() || null,
|
|
337
|
-
anchor: lastAnchor ? label(lastAnchor) : null,
|
|
338
|
-
bounds: navRect ? {
|
|
339
|
-
x: Math.round(navRect.x),
|
|
340
|
-
y: Math.round(navRect.y),
|
|
341
|
-
width: Math.round(navRect.width),
|
|
342
|
-
height: Math.round(navRect.height),
|
|
343
|
-
} : null,
|
|
344
|
-
};
|
|
345
|
-
|
|
346
254
|
window.__impelDesktopTasksCleanup = () => {
|
|
347
255
|
observer.disconnect();
|
|
348
256
|
window.removeEventListener("click", onClick, true);
|
|
@@ -352,50 +260,15 @@ const NAVIGATION_SOURCE = String.raw`(() => {
|
|
|
352
260
|
style.remove();
|
|
353
261
|
delete window.__impelDesktopTasksSetActive;
|
|
354
262
|
};
|
|
355
|
-
return
|
|
263
|
+
return { anchor: lastAnchor ? label(lastAnchor) : null, connected: Boolean(nav?.isConnected) };
|
|
356
264
|
})();`;
|
|
357
265
|
|
|
358
|
-
const NATIVE_NAVIGATION_HTML = `<!doctype html>
|
|
359
|
-
<html lang="en">
|
|
360
|
-
<head>
|
|
361
|
-
<meta charset="utf-8">
|
|
362
|
-
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
363
|
-
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'">
|
|
364
|
-
<style>
|
|
365
|
-
:root { color-scheme: light dark; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
|
|
366
|
-
* { box-sizing: border-box; }
|
|
367
|
-
html, body { width: 100%; height: 100%; margin: 0; overflow: hidden; background: transparent; }
|
|
368
|
-
button {
|
|
369
|
-
width: 100%; height: 100%; display: flex; align-items: center; gap: 9px;
|
|
370
|
-
border: 0; border-radius: 8px; padding: 0 12px;
|
|
371
|
-
background: color-mix(in srgb, Canvas 92%, CanvasText 8%); color: CanvasText;
|
|
372
|
-
font: 13px/1 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
373
|
-
text-align: left; cursor: pointer;
|
|
374
|
-
}
|
|
375
|
-
button:hover, button[data-active="true"] { background: color-mix(in srgb, CanvasText 13%, Canvas); }
|
|
376
|
-
button:focus-visible { outline: 2px solid #7c8cff; outline-offset: -2px; }
|
|
377
|
-
svg { width: 16px; height: 16px; flex: 0 0 auto; }
|
|
378
|
-
</style>
|
|
379
|
-
</head>
|
|
380
|
-
<body>
|
|
381
|
-
<button id="tasks" type="button" aria-label="Tasks">
|
|
382
|
-
<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>
|
|
383
|
-
<span>Tasks</span>
|
|
384
|
-
</button>
|
|
385
|
-
<script>
|
|
386
|
-
const button = document.getElementById("tasks");
|
|
387
|
-
button.addEventListener("pointerdown", (event) => {
|
|
388
|
-
if (event.button !== 0) return;
|
|
389
|
-
event.preventDefault();
|
|
390
|
-
button.dataset.active = "true";
|
|
391
|
-
window.impelDesktopTasks.show();
|
|
392
|
-
});
|
|
393
|
-
window.__impelDesktopTasksSetActive = (active) => { button.dataset.active = String(Boolean(active)); };
|
|
394
|
-
</script>
|
|
395
|
-
</body>
|
|
396
|
-
</html>`;
|
|
266
|
+
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>`;
|
|
397
267
|
|
|
398
|
-
export function desktopTasksMainPreload() {
|
|
268
|
+
export function desktopTasksMainPreload(appUrl) {
|
|
269
|
+
const appOrigin = desktopTasksAppOrigin(appUrl);
|
|
270
|
+
const parserSource = parseDesktopTasksCredential.toString();
|
|
271
|
+
const escapeRegexSource = escapeRegex.toString();
|
|
399
272
|
return `"use strict";
|
|
400
273
|
|
|
401
274
|
if (
|
|
@@ -403,36 +276,29 @@ if (
|
|
|
403
276
|
&& process.env.IMPEL_DESKTOP_TASKS_NODE
|
|
404
277
|
&& process.env.IMPEL_DESKTOP_TASKS_CLI
|
|
405
278
|
&& process.env.IMPEL_DESKTOP_TASKS_TENANT
|
|
406
|
-
&& process.env.IMPEL_DESKTOP_TASKS_HOST_HTML
|
|
407
279
|
&& process.env.IMPEL_DESKTOP_TASKS_VIEW_PRELOAD
|
|
408
280
|
&& !globalThis.__impelDesktopTasksMainInstalled
|
|
409
281
|
) {
|
|
410
282
|
globalThis.__impelDesktopTasksMainInstalled = true;
|
|
411
283
|
const fs = require("node:fs");
|
|
412
284
|
const path = require("node:path");
|
|
413
|
-
const readline = require("node:readline");
|
|
414
285
|
const { spawn } = require("node:child_process");
|
|
415
|
-
const { pathToFileURL } = require("node:url");
|
|
416
286
|
const navSource = ${JSON.stringify(NAVIGATION_SOURCE)};
|
|
417
287
|
const nativeNavUrl = "data:text/html;charset=utf-8," + encodeURIComponent(${JSON.stringify(NATIVE_NAVIGATION_HTML)});
|
|
418
|
-
const
|
|
419
|
-
const
|
|
288
|
+
const appOrigin = ${JSON.stringify(appOrigin)};
|
|
289
|
+
const bootstrapUrl = appOrigin + ${JSON.stringify(DESKTOP_TASKS_BOOTSTRAP_PATH)};
|
|
290
|
+
const tasksUrl = appOrigin + ${JSON.stringify(DESKTOP_TASKS_BOARD_PATH)};
|
|
291
|
+
const readyMarker = ${JSON.stringify(DESKTOP_TASKS_READY_MARKER)};
|
|
420
292
|
const tenantId = process.env.IMPEL_DESKTOP_TASKS_TENANT;
|
|
421
|
-
const hostHtml = process.env.IMPEL_DESKTOP_TASKS_HOST_HTML;
|
|
422
|
-
const viewPreload = process.env.IMPEL_DESKTOP_TASKS_VIEW_PRELOAD;
|
|
423
|
-
const resourceUri = ${JSON.stringify(DESKTOP_TASKS_RESOURCE_URI)};
|
|
424
|
-
const rpcChannel = "impel-desktop-tasks:rpc";
|
|
425
|
-
const openLinkChannel = "impel-desktop-tasks:open-link";
|
|
426
|
-
const closeChannel = "impel-desktop-tasks:close";
|
|
427
293
|
const nativeShowChannel = "impel-desktop-tasks:native-show";
|
|
428
|
-
const runtimeStatusPath = path.join(
|
|
429
|
-
const
|
|
294
|
+
const runtimeStatusPath = path.join(__dirname, "runtime-status.json");
|
|
295
|
+
const escapeRegex = ${escapeRegexSource};
|
|
296
|
+
const parseCredential = ${parserSource};
|
|
297
|
+
const record = (stage) => {
|
|
430
298
|
try {
|
|
431
|
-
const detail = error instanceof Error ? error.message : error == null ? null : String(error);
|
|
432
299
|
fs.writeFileSync(runtimeStatusPath, JSON.stringify({
|
|
433
|
-
schemaVersion:
|
|
300
|
+
schemaVersion: 2,
|
|
434
301
|
stage,
|
|
435
|
-
detail,
|
|
436
302
|
processType: process.type || null,
|
|
437
303
|
electronVersion: process.versions.electron || null,
|
|
438
304
|
pid: process.pid,
|
|
@@ -440,366 +306,344 @@ if (
|
|
|
440
306
|
}, null, 2) + "\\n", { mode: 0o600 });
|
|
441
307
|
} catch {}
|
|
442
308
|
};
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
this.starting = null;
|
|
448
|
-
this.pending = new Map();
|
|
449
|
-
this.sequence = 0;
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
failPending(message) {
|
|
453
|
-
for (const entry of this.pending.values()) entry.reject(new Error(message));
|
|
454
|
-
this.pending.clear();
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
send(method, params) {
|
|
458
|
-
if (!this.child?.stdin?.writable) return Promise.reject(new Error("Tasks bridge is unavailable."));
|
|
459
|
-
const id = ++this.sequence;
|
|
460
|
-
return new Promise((resolve, reject) => {
|
|
461
|
-
const timer = setTimeout(() => {
|
|
462
|
-
this.pending.delete(id);
|
|
463
|
-
try {
|
|
464
|
-
this.child?.stdin?.write(JSON.stringify({
|
|
465
|
-
jsonrpc: "2.0",
|
|
466
|
-
method: "notifications/cancelled",
|
|
467
|
-
params: { requestId: id, reason: "desktop Tasks request timed out" },
|
|
468
|
-
}) + "\\n");
|
|
469
|
-
} catch {}
|
|
470
|
-
reject(new Error("Tasks request timed out."));
|
|
471
|
-
}, 75000);
|
|
472
|
-
this.pending.set(id, {
|
|
473
|
-
resolve: (value) => { clearTimeout(timer); resolve(value); },
|
|
474
|
-
reject: (error) => { clearTimeout(timer); reject(error); },
|
|
475
|
-
});
|
|
476
|
-
this.child.stdin.write(JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\\n");
|
|
477
|
-
});
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
async ensureStarted() {
|
|
481
|
-
if (this.child?.stdin?.writable) return;
|
|
482
|
-
if (this.starting) return this.starting;
|
|
483
|
-
this.starting = (async () => {
|
|
484
|
-
if (!nodePath || !cliPath || !tenantId) throw new Error("Tasks bridge configuration is incomplete.");
|
|
485
|
-
const child = spawn(nodePath, [cliPath, "mcp", "--target", "tasks", "--tenant", tenantId], {
|
|
486
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
487
|
-
env: { ...process.env, IMPEL_MANAGED_MCP: "1" },
|
|
488
|
-
});
|
|
489
|
-
this.child = child;
|
|
490
|
-
child.stderr.resume();
|
|
491
|
-
const lines = readline.createInterface({ input: child.stdout, crlfDelay: Infinity });
|
|
492
|
-
lines.on("line", (line) => {
|
|
493
|
-
let message;
|
|
494
|
-
try { message = JSON.parse(line); } catch { return; }
|
|
495
|
-
const entry = this.pending.get(message.id);
|
|
496
|
-
if (!entry) return;
|
|
497
|
-
this.pending.delete(message.id);
|
|
498
|
-
if (message.error) entry.reject(new Error(message.error.message || "Tasks request failed."));
|
|
499
|
-
else entry.resolve(message.result);
|
|
500
|
-
});
|
|
501
|
-
const closed = () => {
|
|
502
|
-
if (this.child === child) this.child = null;
|
|
503
|
-
this.failPending("Tasks bridge exited.");
|
|
504
|
-
};
|
|
505
|
-
child.once("error", closed);
|
|
506
|
-
child.once("exit", closed);
|
|
507
|
-
try {
|
|
508
|
-
await this.send("initialize", {
|
|
509
|
-
protocolVersion: "2025-06-18",
|
|
510
|
-
capabilities: {},
|
|
511
|
-
clientInfo: { name: "impel-desktop-tasks", version: "1.0.0" },
|
|
512
|
-
});
|
|
513
|
-
child.stdin.write(JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }) + "\\n");
|
|
514
|
-
} catch (error) {
|
|
515
|
-
if (this.child === child) this.child = null;
|
|
516
|
-
try { child.kill(); } catch {}
|
|
517
|
-
throw error;
|
|
518
|
-
}
|
|
519
|
-
})().finally(() => { this.starting = null; });
|
|
520
|
-
return this.starting;
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
async request(method, params) {
|
|
524
|
-
await this.ensureStarted();
|
|
525
|
-
return this.send(method, params);
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
close() {
|
|
529
|
-
const child = this.child;
|
|
530
|
-
this.child = null;
|
|
531
|
-
this.failPending("Tasks bridge closed.");
|
|
532
|
-
try { child?.kill(); } catch {}
|
|
309
|
+
const token = () => new Promise((resolve, reject) => {
|
|
310
|
+
const childEnvironment = { ...process.env };
|
|
311
|
+
for (const name of ["ANTHROPIC_API_KEY", "CODEX_ACCESS_TOKEN", "CODEX_API_KEY", "OPENAI_API_KEY"]) {
|
|
312
|
+
delete childEnvironment[name];
|
|
533
313
|
}
|
|
534
|
-
|
|
314
|
+
const child = spawn(
|
|
315
|
+
process.env.IMPEL_DESKTOP_TASKS_NODE,
|
|
316
|
+
[process.env.IMPEL_DESKTOP_TASKS_CLI, "token", "--tenant", tenantId],
|
|
317
|
+
{ stdio: ["ignore", "pipe", "pipe"], windowsHide: true, env: childEnvironment },
|
|
318
|
+
);
|
|
319
|
+
let stdout = "";
|
|
320
|
+
let stderrBytes = 0;
|
|
321
|
+
let settled = false;
|
|
322
|
+
const finish = (error, value) => {
|
|
323
|
+
if (settled) return;
|
|
324
|
+
settled = true;
|
|
325
|
+
clearTimeout(timer);
|
|
326
|
+
if (error) reject(error); else resolve(value);
|
|
327
|
+
};
|
|
328
|
+
const timer = setTimeout(() => {
|
|
329
|
+
try { child.kill(); } catch {}
|
|
330
|
+
finish(new Error("desktop Tasks token helper timed out"));
|
|
331
|
+
}, 15000);
|
|
332
|
+
child.stdout.on("data", (chunk) => {
|
|
333
|
+
stdout += chunk.toString("utf8");
|
|
334
|
+
if (stdout.length > 8192) {
|
|
335
|
+
try { child.kill(); } catch {}
|
|
336
|
+
finish(new Error("desktop Tasks token helper output was too large"));
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
child.stderr.on("data", (chunk) => {
|
|
340
|
+
stderrBytes += chunk.length;
|
|
341
|
+
if (stderrBytes > 8192) {
|
|
342
|
+
try { child.kill(); } catch {}
|
|
343
|
+
finish(new Error("desktop Tasks token helper output was too large"));
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
child.once("error", () => finish(new Error("desktop Tasks token helper failed")));
|
|
347
|
+
child.once("close", (code) => {
|
|
348
|
+
if (code !== 0) return finish(new Error("desktop Tasks token helper failed"));
|
|
349
|
+
try {
|
|
350
|
+
finish(null, parseCredential(stdout, tenantId, {
|
|
351
|
+
patPrefix: ${JSON.stringify(RUNTIME_BRAND.auth.patPrefix)},
|
|
352
|
+
tenantPrefix: ${JSON.stringify(RUNTIME_BRAND.auth.tenantPrefix)},
|
|
353
|
+
}));
|
|
354
|
+
} catch {
|
|
355
|
+
finish(new Error("desktop Tasks token helper returned invalid output"));
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
});
|
|
535
359
|
|
|
536
360
|
const install = (electron) => {
|
|
537
|
-
const {
|
|
538
|
-
|
|
539
|
-
BrowserWindow,
|
|
540
|
-
WebContentsView,
|
|
541
|
-
ipcMain,
|
|
542
|
-
shell,
|
|
543
|
-
webContents: electronWebContents,
|
|
544
|
-
} = electron;
|
|
545
|
-
if (!app || !BrowserWindow || !WebContentsView || !ipcMain || !electronWebContents) {
|
|
361
|
+
const { app, BrowserWindow, WebContentsView, ipcMain, shell, webContents } = electron;
|
|
362
|
+
if (!app || !BrowserWindow || !WebContentsView || !ipcMain || !webContents) {
|
|
546
363
|
record("unsupported-electron-surface");
|
|
547
364
|
return;
|
|
548
365
|
}
|
|
549
|
-
const client = new TasksMcpClient();
|
|
550
366
|
const windows = new Map();
|
|
551
|
-
const
|
|
552
|
-
const internalViewContents = new Set();
|
|
367
|
+
const internalContents = new Set();
|
|
553
368
|
const nativeNavOwners = new Map();
|
|
554
|
-
const
|
|
555
|
-
let hasAnyDomNavigation = false;
|
|
369
|
+
const configuredSessions = new WeakSet();
|
|
556
370
|
let creatingInternalWindow = false;
|
|
371
|
+
let hasDomNavigation = false;
|
|
557
372
|
const createInternalWindow = (options) => {
|
|
558
373
|
creatingInternalWindow = true;
|
|
559
|
-
try {
|
|
560
|
-
|
|
561
|
-
} finally {
|
|
562
|
-
creatingInternalWindow = false;
|
|
563
|
-
}
|
|
564
|
-
};
|
|
565
|
-
|
|
566
|
-
const parseBounds = (url, window) => {
|
|
567
|
-
const parsed = new URL(url);
|
|
568
|
-
const content = window.getContentBounds();
|
|
569
|
-
const interactiveTop = process.platform === "darwin" ? 48 : 0;
|
|
570
|
-
const number = (name, fallback) => {
|
|
571
|
-
const value = Number.parseInt(parsed.searchParams.get(name) || "", 10);
|
|
572
|
-
return Number.isFinite(value) ? value : fallback;
|
|
573
|
-
};
|
|
574
|
-
const x = Math.max(0, Math.min(content.width - 240, number("x", 0)));
|
|
575
|
-
const y = Math.max(
|
|
576
|
-
interactiveTop,
|
|
577
|
-
Math.min(content.height - 240, number("y", 0)),
|
|
578
|
-
);
|
|
579
|
-
return {
|
|
580
|
-
x,
|
|
581
|
-
y,
|
|
582
|
-
width: Math.max(240, Math.min(content.width - x, number("width", content.width - x))),
|
|
583
|
-
height: Math.max(240, Math.min(content.height - y, number("height", content.height - y))),
|
|
584
|
-
};
|
|
374
|
+
try { return new BrowserWindow(options); }
|
|
375
|
+
finally { creatingInternalWindow = false; }
|
|
585
376
|
};
|
|
586
|
-
|
|
587
377
|
const stateFor = (window) => {
|
|
588
378
|
let state = windows.get(window.id);
|
|
589
379
|
if (!state) {
|
|
590
|
-
state = {
|
|
591
|
-
view: null,
|
|
592
|
-
bounds: null,
|
|
593
|
-
visible: false,
|
|
594
|
-
nativeNav: null,
|
|
595
|
-
hasDomNavigation: false,
|
|
596
|
-
};
|
|
380
|
+
state = { view: null, bounds: null, visible: false, nativeNav: null, hasDomNavigation: false, loading: null, bootstrapping: false };
|
|
597
381
|
windows.set(window.id, state);
|
|
598
382
|
}
|
|
599
383
|
return state;
|
|
600
384
|
};
|
|
601
|
-
|
|
385
|
+
const exactOrigin = (rawUrl) => {
|
|
386
|
+
try { return new URL(rawUrl).origin === appOrigin; }
|
|
387
|
+
catch { return false; }
|
|
388
|
+
};
|
|
389
|
+
const external = (rawUrl) => {
|
|
390
|
+
try {
|
|
391
|
+
const url = new URL(rawUrl);
|
|
392
|
+
if (url.protocol === "https:" && !url.username && !url.password && url.origin !== appOrigin) {
|
|
393
|
+
shell.openExternal(url.href).catch(() => record("external-link-failed"));
|
|
394
|
+
}
|
|
395
|
+
} catch {}
|
|
396
|
+
};
|
|
397
|
+
const configureSession = (session) => {
|
|
398
|
+
if (configuredSessions.has(session)) return;
|
|
399
|
+
configuredSessions.add(session);
|
|
400
|
+
session.setPermissionCheckHandler(() => false);
|
|
401
|
+
session.setPermissionRequestHandler((_contents, _permission, callback) => callback(false));
|
|
402
|
+
session.on("will-download", (event) => event.preventDefault());
|
|
403
|
+
const allowedOrigins = new Set([
|
|
404
|
+
appOrigin,
|
|
405
|
+
"https://api.liveblocks.io",
|
|
406
|
+
"wss://api.liveblocks.io",
|
|
407
|
+
"https://api.dicebear.com",
|
|
408
|
+
"https://avatars.githubusercontent.com",
|
|
409
|
+
]);
|
|
410
|
+
session.webRequest.onBeforeRequest({ urls: ["<all_urls>"] }, (details, callback) => {
|
|
411
|
+
let allowed = false;
|
|
412
|
+
try {
|
|
413
|
+
const url = new URL(details.url);
|
|
414
|
+
allowed = ["data:", "blob:"].includes(url.protocol) || allowedOrigins.has(url.origin);
|
|
415
|
+
} catch {}
|
|
416
|
+
callback({ cancel: !allowed });
|
|
417
|
+
});
|
|
418
|
+
};
|
|
419
|
+
const errorDocument = "data:text/html;charset=utf-8," + encodeURIComponent(
|
|
420
|
+
"<!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>",
|
|
421
|
+
);
|
|
422
|
+
const loadBoard = async (state) => {
|
|
423
|
+
state.bootstrapping = true;
|
|
424
|
+
let pat = null;
|
|
425
|
+
try {
|
|
426
|
+
pat = await token();
|
|
427
|
+
const request = {
|
|
428
|
+
extraHeaders: [
|
|
429
|
+
"Authorization: Bearer " + pat,
|
|
430
|
+
"X-Impel-Tenant: " + tenantId,
|
|
431
|
+
"Cache-Control: no-store",
|
|
432
|
+
"Pragma: no-cache",
|
|
433
|
+
].join("\\n"),
|
|
434
|
+
postData: [{ type: "rawData", bytes: Buffer.alloc(0) }],
|
|
435
|
+
};
|
|
436
|
+
await state.view.webContents.loadURL(bootstrapUrl, request);
|
|
437
|
+
if (state.view.webContents.getURL() !== bootstrapUrl) throw new Error("bootstrap redirected");
|
|
438
|
+
const ready = await state.view.webContents.executeJavaScript(
|
|
439
|
+
"document.body?.textContent?.trim() === " + JSON.stringify(readyMarker),
|
|
440
|
+
true,
|
|
441
|
+
);
|
|
442
|
+
if (!ready) throw new Error("bootstrap marker missing");
|
|
443
|
+
} catch {
|
|
444
|
+
record("board-bootstrap-failed");
|
|
445
|
+
await state.view.webContents.loadURL(errorDocument).catch(() => {});
|
|
446
|
+
return;
|
|
447
|
+
} finally {
|
|
448
|
+
pat = null;
|
|
449
|
+
state.bootstrapping = false;
|
|
450
|
+
}
|
|
451
|
+
try {
|
|
452
|
+
await state.view.webContents.loadURL(tasksUrl);
|
|
453
|
+
record("board-loaded");
|
|
454
|
+
} catch {
|
|
455
|
+
record("board-load-failed");
|
|
456
|
+
await state.view.webContents.loadURL(errorDocument).catch(() => {});
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
const deactivateNavigation = () => {
|
|
460
|
+
for (const state of windows.values()) {
|
|
461
|
+
state.nativeNav?.webContents.executeJavaScript("window.__impelDesktopTasksSetActive?.(false)", true).catch(() => {});
|
|
462
|
+
}
|
|
463
|
+
for (const contents of webContents.getAllWebContents()) {
|
|
464
|
+
if (internalContents.has(contents) || contents.isDestroyed()) continue;
|
|
465
|
+
for (const frame of contents.mainFrame?.framesInSubtree || []) {
|
|
466
|
+
frame.executeJavaScript("window.__impelDesktopTasksSetActive?.(false)", true).catch(() => {});
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
const hide = (window) => {
|
|
471
|
+
const state = windows.get(window.id);
|
|
472
|
+
if (!state) return;
|
|
473
|
+
state.visible = false;
|
|
474
|
+
state.view?.setVisible(false);
|
|
475
|
+
window.webContents.focus();
|
|
476
|
+
deactivateNavigation();
|
|
477
|
+
};
|
|
478
|
+
const isPrimaryWindow = (window) => {
|
|
479
|
+
if (!window || window.isDestroyed() || window.getParentWindow?.()) return false;
|
|
480
|
+
try {
|
|
481
|
+
const url = new URL(window.webContents.getURL());
|
|
482
|
+
if (url.searchParams.has("initialRoute")) return false;
|
|
483
|
+
} catch {}
|
|
484
|
+
return window.isVisible?.() !== false;
|
|
485
|
+
};
|
|
602
486
|
const createView = (window) => {
|
|
603
487
|
const state = stateFor(window);
|
|
604
488
|
if (state.view && !state.view.webContents.isDestroyed()) return state;
|
|
605
489
|
const view = new WebContentsView({
|
|
606
490
|
webPreferences: {
|
|
607
|
-
preload: viewPreload,
|
|
608
491
|
contextIsolation: true,
|
|
609
492
|
nodeIntegration: false,
|
|
610
493
|
sandbox: true,
|
|
611
494
|
spellcheck: false,
|
|
495
|
+
partition: "persist:impel-desktop-tasks",
|
|
612
496
|
},
|
|
613
497
|
});
|
|
498
|
+
state.view = view;
|
|
499
|
+
internalContents.add(view.webContents);
|
|
500
|
+
configureSession(view.webContents.session);
|
|
614
501
|
window.contentView.addChildView(view);
|
|
615
502
|
view.setVisible(false);
|
|
616
503
|
view.setBackgroundColor("#181818");
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
504
|
+
view.webContents.setWindowOpenHandler(({ url }) => {
|
|
505
|
+
if (exactOrigin(url)) view.webContents.loadURL(url).catch(() => record("board-navigation-failed"));
|
|
506
|
+
else external(url);
|
|
507
|
+
return { action: "deny" };
|
|
508
|
+
});
|
|
620
509
|
view.webContents.on("will-navigate", (event, url) => {
|
|
621
|
-
if (url
|
|
510
|
+
if (exactOrigin(url)) return;
|
|
511
|
+
event.preventDefault();
|
|
512
|
+
external(url);
|
|
513
|
+
});
|
|
514
|
+
view.webContents.on("will-redirect", (event, url) => {
|
|
515
|
+
if (state.bootstrapping || !exactOrigin(url)) event.preventDefault();
|
|
516
|
+
});
|
|
517
|
+
view.webContents.on("before-input-event", (event, input) => {
|
|
518
|
+
if (input.key === "Escape" && input.type === "keyDown") {
|
|
519
|
+
event.preventDefault();
|
|
520
|
+
hide(window);
|
|
521
|
+
}
|
|
622
522
|
});
|
|
623
523
|
view.webContents.on("destroyed", () => {
|
|
624
|
-
|
|
625
|
-
internalViewContents.delete(view.webContents);
|
|
524
|
+
internalContents.delete(view.webContents);
|
|
626
525
|
try { window.contentView.removeChildView(view); } catch {}
|
|
627
526
|
if (state.view === view) state.view = null;
|
|
628
527
|
});
|
|
629
|
-
view.webContents.loadURL(hostUrl).catch((error) => record("view-load-failed", error));
|
|
630
|
-
state.view = view;
|
|
631
528
|
return state;
|
|
632
529
|
};
|
|
633
|
-
|
|
634
530
|
const show = (window, bounds) => {
|
|
635
531
|
const state = createView(window);
|
|
636
532
|
state.bounds = bounds;
|
|
637
|
-
state.visible = true;
|
|
638
533
|
state.view.setBounds(bounds);
|
|
639
534
|
state.view.setVisible(true);
|
|
640
535
|
state.view.webContents.focus();
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
boardVisible: state.visible,
|
|
646
|
-
boardFocused: state.view.webContents.isFocused(),
|
|
647
|
-
});
|
|
648
|
-
record("board-shown", JSON.stringify(boardStatus()));
|
|
649
|
-
setTimeout(() => {
|
|
650
|
-
if (state.view && !state.view.webContents.isDestroyed()) record("board-visible-check", JSON.stringify(boardStatus()));
|
|
651
|
-
}, 2000);
|
|
652
|
-
};
|
|
653
|
-
const hide = (window) => {
|
|
654
|
-
const state = windows.get(window.id);
|
|
655
|
-
if (!state) return;
|
|
656
|
-
state.visible = false;
|
|
657
|
-
state.view?.setVisible(false);
|
|
658
|
-
window.webContents.focus();
|
|
536
|
+
if (state.visible) return;
|
|
537
|
+
state.visible = true;
|
|
538
|
+
state.loading = loadBoard(state).finally(() => { state.loading = null; });
|
|
539
|
+
record("board-shown");
|
|
659
540
|
};
|
|
660
|
-
|
|
661
|
-
const layoutBoard = (window) => {
|
|
541
|
+
const layoutBoard = (window, bounds = null) => {
|
|
662
542
|
const state = windows.get(window.id);
|
|
663
|
-
if (!state?.visible || !state.view ||
|
|
664
|
-
|
|
543
|
+
if (!state?.visible || !state.view || state.view.webContents.isDestroyed()) return;
|
|
544
|
+
if (bounds) state.bounds = bounds;
|
|
545
|
+
if (state.bounds) state.view.setBounds(state.bounds);
|
|
665
546
|
};
|
|
666
|
-
|
|
667
|
-
|
|
547
|
+
const parseBounds = (rawUrl, window) => {
|
|
548
|
+
const parsed = new URL(rawUrl);
|
|
668
549
|
const content = window.getContentBounds();
|
|
669
|
-
const x = Math.min(296, Math.max(0, content.width - 240));
|
|
670
550
|
const interactiveTop = process.platform === "darwin" ? 48 : 0;
|
|
551
|
+
const number = (name, fallback) => {
|
|
552
|
+
const value = Number.parseInt(parsed.searchParams.get(name) || "", 10);
|
|
553
|
+
return Number.isFinite(value) ? value : fallback;
|
|
554
|
+
};
|
|
555
|
+
const x = Math.max(0, Math.min(content.width - 240, number("x", 0)));
|
|
556
|
+
const y = Math.max(interactiveTop, Math.min(content.height - 240, number("y", interactiveTop)));
|
|
671
557
|
return {
|
|
672
558
|
x,
|
|
673
|
-
y
|
|
674
|
-
width: Math.max(240, content.width - x),
|
|
675
|
-
height: Math.max(240, content.height -
|
|
559
|
+
y,
|
|
560
|
+
width: Math.max(240, Math.min(content.width - x, number("width", content.width - x))),
|
|
561
|
+
height: Math.max(240, Math.min(content.height - y, number("height", content.height - y))),
|
|
676
562
|
};
|
|
677
563
|
};
|
|
678
|
-
|
|
564
|
+
const nativeBoardBounds = (window) => {
|
|
565
|
+
const content = window.getContentBounds();
|
|
566
|
+
const x = Math.min(296, Math.max(0, content.width - 240));
|
|
567
|
+
const interactiveTop = process.platform === "darwin" ? 48 : 0;
|
|
568
|
+
return { x, y: interactiveTop, width: Math.max(240, content.width - x), height: Math.max(240, content.height - interactiveTop) };
|
|
569
|
+
};
|
|
679
570
|
const layoutNativeNav = (window) => {
|
|
680
571
|
const state = windows.get(window.id);
|
|
681
572
|
if (!state?.nativeNav || state.nativeNav.isDestroyed()) return;
|
|
682
573
|
const content = window.getContentBounds();
|
|
683
|
-
state.nativeNav.setBounds({
|
|
684
|
-
x: content.x + 12,
|
|
685
|
-
y: content.y + Math.max(48, content.height - 92),
|
|
686
|
-
width: Math.min(120, Math.max(80, content.width - 24)),
|
|
687
|
-
height: 38,
|
|
688
|
-
});
|
|
574
|
+
state.nativeNav.setBounds({ x: content.x + 12, y: content.y + Math.max(48, content.height - 92), width: Math.min(120, Math.max(80, content.width - 24)), height: 38 });
|
|
689
575
|
};
|
|
690
|
-
|
|
691
576
|
const ensureNativeNav = (window) => {
|
|
692
|
-
if (!window
|
|
577
|
+
if (!isPrimaryWindow(window)) return;
|
|
693
578
|
const state = stateFor(window);
|
|
694
579
|
if (state.nativeNav && !state.nativeNav.isDestroyed()) return;
|
|
695
|
-
|
|
580
|
+
// Auxiliary vendor windows (for example Codex's hidden avatar overlay)
|
|
581
|
+
// are also top-level BrowserWindows. Never give them their own Tasks
|
|
582
|
+
// fallback: duplicate child windows interfere with first-run navigation
|
|
583
|
+
// and can cover the primary shell.
|
|
584
|
+
if ([...windows.values()].some((candidate) => (
|
|
585
|
+
candidate.nativeNav && !candidate.nativeNav.isDestroyed()
|
|
586
|
+
))) return;
|
|
587
|
+
const nativeNav = createInternalWindow({
|
|
696
588
|
parent: window,
|
|
697
589
|
frame: false,
|
|
698
|
-
transparent: false,
|
|
699
590
|
show: false,
|
|
700
591
|
resizable: false,
|
|
701
|
-
movable: false,
|
|
702
|
-
minimizable: false,
|
|
703
|
-
maximizable: false,
|
|
704
|
-
fullscreenable: false,
|
|
705
592
|
skipTaskbar: true,
|
|
706
|
-
hasShadow: false,
|
|
707
|
-
acceptFirstMouse: true,
|
|
708
593
|
backgroundColor: "#292929",
|
|
709
594
|
webPreferences: {
|
|
710
|
-
preload:
|
|
595
|
+
preload: process.env.IMPEL_DESKTOP_TASKS_VIEW_PRELOAD,
|
|
711
596
|
contextIsolation: true,
|
|
712
597
|
nodeIntegration: false,
|
|
713
598
|
sandbox: true,
|
|
714
|
-
spellcheck: false,
|
|
715
599
|
},
|
|
716
600
|
});
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
navView.webContents.on("destroyed", () => {
|
|
726
|
-
internalViewContents.delete(navView.webContents);
|
|
727
|
-
nativeNavOwners.delete(navView.webContents);
|
|
728
|
-
});
|
|
729
|
-
navView.webContents.on("did-finish-load", () => {
|
|
730
|
-
navView.webContents.executeJavaScript(
|
|
731
|
-
"typeof window.impelDesktopTasks?.show === 'function'",
|
|
732
|
-
true,
|
|
733
|
-
).then((ready) => record(ready ? "native-navigation-ready" : "native-navigation-bridge-missing"))
|
|
734
|
-
.catch((error) => record("native-navigation-check-failed", error));
|
|
601
|
+
state.nativeNav = nativeNav;
|
|
602
|
+
internalContents.add(nativeNav.webContents);
|
|
603
|
+
nativeNavOwners.set(nativeNav.webContents, window);
|
|
604
|
+
nativeNav.webContents.setWindowOpenHandler(() => ({ action: "deny" }));
|
|
605
|
+
nativeNav.webContents.on("will-navigate", (event, url) => { if (url !== nativeNavUrl) event.preventDefault(); });
|
|
606
|
+
nativeNav.webContents.on("destroyed", () => {
|
|
607
|
+
internalContents.delete(nativeNav.webContents);
|
|
608
|
+
nativeNavOwners.delete(nativeNav.webContents);
|
|
735
609
|
});
|
|
736
|
-
|
|
610
|
+
nativeNav.webContents.loadURL(nativeNavUrl).catch(() => record("native-navigation-load-failed"));
|
|
737
611
|
layoutNativeNav(window);
|
|
738
|
-
|
|
739
|
-
navView.moveTop();
|
|
612
|
+
nativeNav.showInactive();
|
|
740
613
|
record("native-navigation-installed");
|
|
741
614
|
};
|
|
742
|
-
|
|
743
615
|
const removeNativeNav = (window) => {
|
|
744
616
|
const state = windows.get(window.id);
|
|
745
|
-
|
|
746
|
-
|
|
617
|
+
const nativeNav = state?.nativeNav;
|
|
618
|
+
if (!nativeNav) return;
|
|
747
619
|
state.nativeNav = null;
|
|
748
|
-
|
|
749
|
-
nativeNavOwners.delete(
|
|
750
|
-
try {
|
|
751
|
-
navView.destroy();
|
|
752
|
-
} catch {}
|
|
753
|
-
};
|
|
754
|
-
|
|
755
|
-
const ownerWindow = (contents) => {
|
|
756
|
-
const direct = BrowserWindow.fromWebContents(contents);
|
|
757
|
-
if (direct && !direct.isDestroyed()) return direct;
|
|
758
|
-
const owner = contents.getOwnerBrowserWindow?.();
|
|
759
|
-
if (owner && !owner.isDestroyed()) return owner;
|
|
760
|
-
const focused = BrowserWindow.getFocusedWindow();
|
|
761
|
-
if (focused && !focused.isDestroyed()) return focused;
|
|
762
|
-
return BrowserWindow.getAllWindows().find((window) => !window.isDestroyed() && window.isVisible())
|
|
763
|
-
|| BrowserWindow.getAllWindows().find((window) => !window.isDestroyed())
|
|
764
|
-
|| null;
|
|
620
|
+
internalContents.delete(nativeNav.webContents);
|
|
621
|
+
nativeNavOwners.delete(nativeNav.webContents);
|
|
622
|
+
try { nativeNav.destroy(); } catch {}
|
|
765
623
|
};
|
|
766
|
-
|
|
624
|
+
const ownerWindow = (contents) => BrowserWindow.fromWebContents(contents)
|
|
625
|
+
|| contents.getOwnerBrowserWindow?.()
|
|
626
|
+
|| BrowserWindow.getFocusedWindow()
|
|
627
|
+
|| BrowserWindow.getAllWindows().find((window) => !window.isDestroyed())
|
|
628
|
+
|| null;
|
|
767
629
|
const attachContents = (contents) => {
|
|
768
|
-
if (
|
|
769
|
-
!contents
|
|
770
|
-
|| contents.isDestroyed()
|
|
771
|
-
|| contents.__impelDesktopTasksAttached
|
|
772
|
-
|| internalViewContents.has(contents)
|
|
773
|
-
) return;
|
|
630
|
+
if (!contents || contents.isDestroyed() || contents.__impelDesktopTasksAttached || internalContents.has(contents)) return;
|
|
774
631
|
contents.__impelDesktopTasksAttached = true;
|
|
775
|
-
const surface = () => ({
|
|
776
|
-
id: contents.id,
|
|
777
|
-
type: contents.getType?.() || null,
|
|
778
|
-
url: contents.getURL?.() || null,
|
|
779
|
-
});
|
|
780
632
|
const inject = (attempt = 0) => {
|
|
781
|
-
if (
|
|
782
|
-
|
|
783
|
-
Promise.all(frames.map(async (frame) => ({
|
|
784
|
-
frameUrl: frame.url,
|
|
785
|
-
result: await frame.executeJavaScript(navSource, true),
|
|
786
|
-
})))
|
|
633
|
+
if (internalContents.has(contents)) return;
|
|
634
|
+
Promise.all((contents.mainFrame?.framesInSubtree || []).map((frame) => frame.executeJavaScript(navSource, true)))
|
|
787
635
|
.then((results) => {
|
|
788
|
-
record("navigation-injected", JSON.stringify({ ...surface(), frames: results }));
|
|
789
636
|
const window = ownerWindow(contents);
|
|
790
637
|
if (!window) return;
|
|
791
638
|
const state = stateFor(window);
|
|
792
|
-
if (results.some((
|
|
793
|
-
|
|
639
|
+
if (results.some((result) => result?.anchor)) {
|
|
640
|
+
hasDomNavigation = true;
|
|
794
641
|
state.hasDomNavigation = true;
|
|
795
642
|
for (const owner of BrowserWindow.getAllWindows()) removeNativeNav(owner);
|
|
796
643
|
} else if (attempt < 3) setTimeout(() => inject(attempt + 1), 1000);
|
|
797
|
-
else if (!state.hasDomNavigation && !
|
|
644
|
+
else if (!state.hasDomNavigation && !hasDomNavigation) ensureNativeNav(window);
|
|
798
645
|
})
|
|
799
|
-
.catch((
|
|
800
|
-
"navigation-injection-failed",
|
|
801
|
-
JSON.stringify(surface()) + ": " + String(error),
|
|
802
|
-
));
|
|
646
|
+
.catch(() => record("navigation-injection-failed"));
|
|
803
647
|
};
|
|
804
648
|
contents.on("did-finish-load", () => inject());
|
|
805
649
|
contents.on("did-frame-finish-load", () => inject());
|
|
@@ -813,45 +657,29 @@ if (
|
|
|
813
657
|
try { action = new URL(url).hostname; } catch { return; }
|
|
814
658
|
if (action === "hide") hide(window);
|
|
815
659
|
else if (action === "show") show(window, parseBounds(url, window));
|
|
816
|
-
else if (action === "layout")
|
|
817
|
-
const state = windows.get(window.id);
|
|
818
|
-
if (state?.visible) show(window, parseBounds(url, window));
|
|
819
|
-
}
|
|
660
|
+
else if (action === "layout") layoutBoard(window, parseBounds(url, window));
|
|
820
661
|
};
|
|
821
662
|
contents.on("will-navigate", handleNavigation);
|
|
822
663
|
contents.on("will-frame-navigate", handleNavigation);
|
|
823
664
|
};
|
|
824
|
-
|
|
825
665
|
const attachWindow = (window) => {
|
|
826
|
-
if (
|
|
827
|
-
!window
|
|
828
|
-
|| window.isDestroyed()
|
|
829
|
-
|| creatingInternalWindow
|
|
830
|
-
|| window.getParentWindow?.()
|
|
831
|
-
|| window.__impelDesktopTasksWindowAttached
|
|
832
|
-
) return;
|
|
666
|
+
if (!window || window.isDestroyed() || creatingInternalWindow || window.getParentWindow?.() || window.__impelDesktopTasksWindowAttached) return;
|
|
833
667
|
window.__impelDesktopTasksWindowAttached = true;
|
|
834
668
|
attachContents(window.webContents);
|
|
835
|
-
window.on("resize", () => {
|
|
836
|
-
layoutNativeNav(window);
|
|
837
|
-
layoutBoard(window);
|
|
838
|
-
});
|
|
839
|
-
window.on("move", () => layoutBoard(window));
|
|
669
|
+
window.on("resize", () => { layoutNativeNav(window); layoutBoard(window); });
|
|
840
670
|
window.on("focus", () => {
|
|
841
671
|
const state = windows.get(window.id);
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
state.nativeNav.moveTop();
|
|
845
|
-
}
|
|
846
|
-
if (state?.visible && state.view && !state.view.webContents.isDestroyed()) state.view.setVisible(true);
|
|
672
|
+
state?.nativeNav?.showInactive();
|
|
673
|
+
if (state?.visible) state.view?.setVisible(true);
|
|
847
674
|
});
|
|
848
675
|
window.on("blur", () => setTimeout(() => {
|
|
849
676
|
const state = windows.get(window.id);
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
677
|
+
if (BrowserWindow.getFocusedWindow() === state?.nativeNav) return;
|
|
678
|
+
// Keep the only path to Tasks available while the app is frontmost.
|
|
679
|
+
// BrowserWindow blur also fires when a user clicks the child fallback;
|
|
680
|
+
// hiding it here made the button disappear before its click arrived.
|
|
681
|
+
if (state?.nativeNav && (!window.isVisible() || window.isMinimized())) state.nativeNav.hide();
|
|
682
|
+
}, 150));
|
|
855
683
|
window.on("minimize", () => {
|
|
856
684
|
const state = windows.get(window.id);
|
|
857
685
|
state?.nativeNav?.hide();
|
|
@@ -864,231 +692,36 @@ if (
|
|
|
864
692
|
});
|
|
865
693
|
window.on("closed", () => {
|
|
866
694
|
const state = windows.get(window.id);
|
|
867
|
-
if (state)
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
if (state.nativeNav) internalViewContents.delete(state.nativeNav.webContents);
|
|
873
|
-
if (state.nativeNav) nativeNavOwners.delete(state.nativeNav.webContents);
|
|
874
|
-
try {
|
|
875
|
-
if (state.view) {
|
|
876
|
-
window.contentView.removeChildView(state.view);
|
|
877
|
-
state.view.webContents.close();
|
|
878
|
-
}
|
|
879
|
-
if (state.nativeNav) {
|
|
880
|
-
state.nativeNav.destroy();
|
|
881
|
-
}
|
|
882
|
-
} catch {}
|
|
883
|
-
windows.delete(window.id);
|
|
884
|
-
}
|
|
695
|
+
if (!state) return;
|
|
696
|
+
try { if (state.view) window.contentView.removeChildView(state.view); } catch {}
|
|
697
|
+
try { state.view?.webContents.close(); } catch {}
|
|
698
|
+
try { state.nativeNav?.destroy(); } catch {}
|
|
699
|
+
windows.delete(window.id);
|
|
885
700
|
});
|
|
886
701
|
};
|
|
887
|
-
|
|
888
|
-
ipcMain.handle(rpcChannel, async (event, request) => {
|
|
889
|
-
if (!allowedViewContents.has(event.sender)) throw new Error("Untrusted Tasks view.");
|
|
890
|
-
if (request?.method === "resources/read" && request?.params?.uri === resourceUri) {
|
|
891
|
-
return client.request("resources/read", { uri: resourceUri });
|
|
892
|
-
}
|
|
893
|
-
if (request?.method === "tools/call") {
|
|
894
|
-
const name = request?.params?.name;
|
|
895
|
-
const allowed = new Set([
|
|
896
|
-
"show_tasks",
|
|
897
|
-
"list_tasks",
|
|
898
|
-
"get_task",
|
|
899
|
-
"list_members",
|
|
900
|
-
"create_task",
|
|
901
|
-
"update_task",
|
|
902
|
-
"delete_task",
|
|
903
|
-
]);
|
|
904
|
-
if (!allowed.has(name)) throw new Error("Unsupported Tasks tool.");
|
|
905
|
-
const args = request?.params?.arguments;
|
|
906
|
-
if (args != null && (typeof args !== "object" || Array.isArray(args))) {
|
|
907
|
-
throw new Error("Invalid Tasks tool arguments.");
|
|
908
|
-
}
|
|
909
|
-
return client.request("tools/call", { name, arguments: args || {} });
|
|
910
|
-
}
|
|
911
|
-
throw new Error("Unsupported Tasks request.");
|
|
912
|
-
});
|
|
913
|
-
ipcMain.handle(openLinkChannel, async (event, rawUrl) => {
|
|
914
|
-
if (!allowedViewContents.has(event.sender)) throw new Error("Untrusted Tasks view.");
|
|
915
|
-
const url = new URL(String(rawUrl));
|
|
916
|
-
if (url.protocol !== "https:" || url.username || url.password) throw new Error("Invalid Tasks link.");
|
|
917
|
-
await shell.openExternal(url.href);
|
|
918
|
-
return {};
|
|
919
|
-
});
|
|
920
|
-
ipcMain.on(closeChannel, (event) => {
|
|
921
|
-
if (!allowedViewContents.has(event.sender)) return;
|
|
922
|
-
for (const window of BrowserWindow.getAllWindows()) {
|
|
923
|
-
if (windows.get(window.id)?.view?.webContents === event.sender) {
|
|
924
|
-
hide(window);
|
|
925
|
-
const nativeNav = windows.get(window.id)?.nativeNav;
|
|
926
|
-
nativeNav?.webContents.executeJavaScript(
|
|
927
|
-
"window.__impelDesktopTasksSetActive?.(false);",
|
|
928
|
-
true,
|
|
929
|
-
).catch(() => {});
|
|
930
|
-
for (const contents of electronWebContents.getAllWebContents()) {
|
|
931
|
-
if (internalViewContents.has(contents) || contents.isDestroyed()) continue;
|
|
932
|
-
for (const frame of contents.mainFrame?.framesInSubtree || []) {
|
|
933
|
-
frame.executeJavaScript(
|
|
934
|
-
"window.__impelDesktopTasksSetActive?.(false);",
|
|
935
|
-
true,
|
|
936
|
-
).catch(() => {});
|
|
937
|
-
}
|
|
938
|
-
}
|
|
939
|
-
break;
|
|
940
|
-
}
|
|
941
|
-
}
|
|
942
|
-
});
|
|
943
702
|
ipcMain.on(nativeShowChannel, (event) => {
|
|
944
703
|
const window = nativeNavOwners.get(event.sender);
|
|
945
|
-
if (
|
|
946
|
-
record("native-navigation-selected");
|
|
947
|
-
show(window, nativeBoardBounds(window));
|
|
704
|
+
if (window && !window.isDestroyed()) show(window, nativeBoardBounds(window));
|
|
948
705
|
});
|
|
949
|
-
|
|
950
|
-
const hideAuxiliaryOutsideApp = () => {
|
|
951
|
-
const focused = BrowserWindow.getFocusedWindow();
|
|
952
|
-
if (focused && (windows.has(focused.id) || internalViewContents.has(focused.webContents))) return;
|
|
953
|
-
for (const state of windows.values()) {
|
|
954
|
-
state.nativeNav?.hide();
|
|
955
|
-
state.view?.setVisible(false);
|
|
956
|
-
}
|
|
957
|
-
};
|
|
958
|
-
|
|
959
706
|
app.on("browser-window-created", (_event, window) => attachWindow(window));
|
|
960
|
-
app.on("browser-window-blur", () => setTimeout(hideAuxiliaryOutsideApp, 150));
|
|
961
|
-
app.on("hide", hideAuxiliaryOutsideApp);
|
|
962
707
|
app.on("web-contents-created", (_event, contents) => setTimeout(() => attachContents(contents), 0));
|
|
963
|
-
app.on("before-quit", () => client.close());
|
|
964
708
|
for (const window of BrowserWindow.getAllWindows()) attachWindow(window);
|
|
965
|
-
for (const contents of
|
|
709
|
+
for (const contents of webContents.getAllWebContents()) attachContents(contents);
|
|
966
710
|
record("main-installed");
|
|
967
711
|
};
|
|
968
|
-
|
|
969
712
|
record("preload-loaded");
|
|
970
|
-
try {
|
|
971
|
-
|
|
972
|
-
} catch (error) {
|
|
973
|
-
record("main-install-failed", error);
|
|
974
|
-
}
|
|
713
|
+
try { install(require("electron")); }
|
|
714
|
+
catch { record("main-install-failed"); }
|
|
975
715
|
}
|
|
976
716
|
`;
|
|
977
717
|
}
|
|
978
718
|
|
|
719
|
+
/** The fallback native navigation window exposes only its one show action. */
|
|
979
720
|
export function desktopTasksViewPreload() {
|
|
980
721
|
return `"use strict";
|
|
981
|
-
|
|
982
722
|
const { contextBridge, ipcRenderer } = require("electron");
|
|
983
723
|
contextBridge.exposeInMainWorld("impelDesktopTasks", Object.freeze({
|
|
984
|
-
rpc: (request) => ipcRenderer.invoke("impel-desktop-tasks:rpc", request),
|
|
985
|
-
openLink: (url) => ipcRenderer.invoke("impel-desktop-tasks:open-link", url),
|
|
986
|
-
close: () => ipcRenderer.send("impel-desktop-tasks:close"),
|
|
987
724
|
show: () => ipcRenderer.send("impel-desktop-tasks:native-show"),
|
|
988
725
|
}));
|
|
989
726
|
`;
|
|
990
727
|
}
|
|
991
|
-
|
|
992
|
-
export function desktopTasksHostHtml() {
|
|
993
|
-
return `<!doctype html>
|
|
994
|
-
<html lang="en">
|
|
995
|
-
<head>
|
|
996
|
-
<meta charset="utf-8">
|
|
997
|
-
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
998
|
-
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; frame-src 'self' data:; img-src data:;">
|
|
999
|
-
<title>Impel Tasks</title>
|
|
1000
|
-
<style>
|
|
1001
|
-
:root { color-scheme: light dark; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
|
|
1002
|
-
* { box-sizing: border-box; }
|
|
1003
|
-
html, body { width: 100%; height: 100%; margin: 0; overflow: hidden; background: Canvas; color: CanvasText; }
|
|
1004
|
-
body { display: grid; grid-template-rows: minmax(0, 1fr); }
|
|
1005
|
-
iframe { width: 100%; height: 100%; border: 0; background: Canvas; }
|
|
1006
|
-
#status { display: grid; place-items: center; padding: 24px; color: color-mix(in srgb, CanvasText 65%, transparent); text-align: center; }
|
|
1007
|
-
#status[hidden] { display: none; }
|
|
1008
|
-
</style>
|
|
1009
|
-
</head>
|
|
1010
|
-
<body>
|
|
1011
|
-
<div id="status" role="status">Loading Impel Tasks…</div>
|
|
1012
|
-
<iframe id="widget" title="Impel Tasks" sandbox="allow-scripts allow-forms" hidden></iframe>
|
|
1013
|
-
<script>
|
|
1014
|
-
(() => {
|
|
1015
|
-
const bridge = window.impelDesktopTasks;
|
|
1016
|
-
const frame = document.getElementById("widget");
|
|
1017
|
-
const status = document.getElementById("status");
|
|
1018
|
-
let initialResult = null;
|
|
1019
|
-
let initialized = false;
|
|
1020
|
-
const send = (message) => frame.contentWindow?.postMessage(message, "*");
|
|
1021
|
-
const reply = (id, result, error) => send(error
|
|
1022
|
-
? { jsonrpc: "2.0", id, error: { code: -32000, message: error } }
|
|
1023
|
-
: { jsonrpc: "2.0", id, result });
|
|
1024
|
-
|
|
1025
|
-
window.addEventListener("message", async (event) => {
|
|
1026
|
-
if (event.source !== frame.contentWindow) return;
|
|
1027
|
-
const message = event.data;
|
|
1028
|
-
if (!message || message.jsonrpc !== "2.0") return;
|
|
1029
|
-
if (message.method === "ui/initialize") {
|
|
1030
|
-
reply(message.id, {
|
|
1031
|
-
protocolVersion: "2026-01-26",
|
|
1032
|
-
hostInfo: { name: "Impel Desktop Tasks", version: "1.0.0" },
|
|
1033
|
-
hostCapabilities: { openLinks: {}, serverTools: {} },
|
|
1034
|
-
hostContext: {
|
|
1035
|
-
theme: matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light",
|
|
1036
|
-
platform: "desktop",
|
|
1037
|
-
displayMode: "fullscreen",
|
|
1038
|
-
availableDisplayModes: ["fullscreen"],
|
|
1039
|
-
},
|
|
1040
|
-
});
|
|
1041
|
-
return;
|
|
1042
|
-
}
|
|
1043
|
-
if (message.method === "ui/notifications/initialized") {
|
|
1044
|
-
if (!initialized && initialResult) {
|
|
1045
|
-
initialized = true;
|
|
1046
|
-
send({ jsonrpc: "2.0", method: "ui/notifications/tool-result", params: initialResult });
|
|
1047
|
-
}
|
|
1048
|
-
return;
|
|
1049
|
-
}
|
|
1050
|
-
if (message.method === "tools/call") {
|
|
1051
|
-
try {
|
|
1052
|
-
const result = await bridge.rpc({ method: "tools/call", params: message.params || {} });
|
|
1053
|
-
reply(message.id, result);
|
|
1054
|
-
} catch (error) {
|
|
1055
|
-
reply(message.id, null, error instanceof Error ? error.message : "Tasks request failed.");
|
|
1056
|
-
}
|
|
1057
|
-
return;
|
|
1058
|
-
}
|
|
1059
|
-
if (message.method === "ui/open-link") {
|
|
1060
|
-
try {
|
|
1061
|
-
await bridge.openLink(message.params?.url);
|
|
1062
|
-
reply(message.id, {});
|
|
1063
|
-
} catch (error) {
|
|
1064
|
-
reply(message.id, null, error instanceof Error ? error.message : "Tasks link failed.");
|
|
1065
|
-
}
|
|
1066
|
-
return;
|
|
1067
|
-
}
|
|
1068
|
-
if (message.method === "ui/request-display-mode") {
|
|
1069
|
-
reply(message.id, { mode: "fullscreen" });
|
|
1070
|
-
}
|
|
1071
|
-
});
|
|
1072
|
-
|
|
1073
|
-
window.addEventListener("keydown", (event) => {
|
|
1074
|
-
if (event.key === "Escape") bridge.close();
|
|
1075
|
-
});
|
|
1076
|
-
Promise.all([
|
|
1077
|
-
bridge.rpc({ method: "resources/read", params: { uri: ${JSON.stringify(DESKTOP_TASKS_RESOURCE_URI)} } }),
|
|
1078
|
-
bridge.rpc({ method: "tools/call", params: { name: "show_tasks", arguments: { scope: "visible", limit: 50 } } }),
|
|
1079
|
-
]).then(([resource, result]) => {
|
|
1080
|
-
const html = resource?.contents?.find((entry) => entry?.uri === ${JSON.stringify(DESKTOP_TASKS_RESOURCE_URI)})?.text;
|
|
1081
|
-
if (typeof html !== "string" || !html.includes("<html")) throw new Error("Tasks widget resource is unavailable.");
|
|
1082
|
-
initialResult = result;
|
|
1083
|
-
status.hidden = true;
|
|
1084
|
-
frame.hidden = false;
|
|
1085
|
-
frame.srcdoc = html;
|
|
1086
|
-
}).catch((error) => {
|
|
1087
|
-
status.textContent = error instanceof Error ? error.message : "Impel Tasks could not be loaded.";
|
|
1088
|
-
});
|
|
1089
|
-
})();
|
|
1090
|
-
</script>
|
|
1091
|
-
</body>
|
|
1092
|
-
</html>
|
|
1093
|
-
`;
|
|
1094
|
-
}
|