cicy-desktop 2.1.250 → 2.1.252
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/.github/workflows/build-wsl-package.yml +1 -0
- package/package.json +5 -5
- package/src/backends/local-teams.js +23 -1
- package/src/chrome/chrome-launcher.js +10 -2
- package/src/i18n/locales/en.json +2 -1
- package/src/i18n/locales/fr.json +2 -1
- package/src/i18n/locales/ja.json +2 -1
- package/src/i18n/locales/zh-CN.json +2 -1
- package/src/main.js +61 -0
- package/src/tabbrowser/newtab-protocol.js +18 -1
- package/src/tabbrowser/panel-cells.js +292 -0
- package/src/tabbrowser/panel-preload.js +24 -0
- package/src/tabbrowser/split-panel.html +727 -0
- package/src/tabbrowser/tab-shell-preload.js +3 -0
- package/src/tabbrowser/tab-shell.html +57 -3
- package/src/tools/chrome-tools.js +39 -0
- package/src/tools/hook-gemini.js +1 -1
- package/src/tools/tab-browser-tools.js +194 -13
- package/src/tools/window-tools.js +8 -3
- package/src/utils/context-menu-options.js +3 -2
- package/src/utils/window-utils.js +50 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cicy-desktop",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.252",
|
|
4
4
|
"description": "CiCy - AI-powered operating system browser",
|
|
5
5
|
"main": "src/main.js",
|
|
6
6
|
"bin": {
|
|
@@ -139,10 +139,10 @@
|
|
|
139
139
|
"//optionalDependencies": "(2026-06 回调): mac/linux 改回 native cicy-code(:8008,colima 在 16G mac 上压垮内存)→ 重新内置 cicy-code-<plat> / cicy-mihomo-<plat>,localbin.fromBundle 零网络 seed(npm 仅作更新通道,带 npmmirror→npmjs 回退)。这些由 scripts/sync-runtime-deps.cjs 在 tag-push 时同步到最新版。npm 的 os/cpu 字段保证每个平台只装自己那份。Windows 仍走 docker(WSL :8009),它那份 cicy-code-windows 用不到但 bundle 着无害。",
|
|
140
140
|
"optionalDependencies": {
|
|
141
141
|
"electron": "41.0.3",
|
|
142
|
-
"cicy-code-darwin-x64": "2.3.
|
|
143
|
-
"cicy-code-darwin-arm64": "2.3.
|
|
144
|
-
"cicy-code-linux-x64": "2.3.
|
|
145
|
-
"cicy-code-linux-arm64": "2.3.
|
|
142
|
+
"cicy-code-darwin-x64": "2.3.266",
|
|
143
|
+
"cicy-code-darwin-arm64": "2.3.266",
|
|
144
|
+
"cicy-code-linux-x64": "2.3.266",
|
|
145
|
+
"cicy-code-linux-arm64": "2.3.266",
|
|
146
146
|
"cicy-code-windows-x64": "2.3.193",
|
|
147
147
|
"cicy-mihomo-darwin-x64": "1.10.4",
|
|
148
148
|
"cicy-mihomo-darwin-arm64": "1.10.4",
|
|
@@ -1153,6 +1153,28 @@ function avatarForUrl(url) {
|
|
|
1153
1153
|
return "";
|
|
1154
1154
|
}
|
|
1155
1155
|
|
|
1156
|
+
// Resolve the canonical team identity for a URL so every surface (team card,
|
|
1157
|
+
// normal open, restored tab) uses the same title/avatar/color seed.
|
|
1158
|
+
function teamIdentityForUrl(url) {
|
|
1159
|
+
const key = stripVolatile(url);
|
|
1160
|
+
try {
|
|
1161
|
+
const avatars = readAvatars();
|
|
1162
|
+
for (const [id, node] of Object.entries(readNodes())) {
|
|
1163
|
+
const base = node && node.base_url;
|
|
1164
|
+
if (!base) continue;
|
|
1165
|
+
const teamKey = stripVolatile(base);
|
|
1166
|
+
if (teamKey === key || key.startsWith(teamKey)) {
|
|
1167
|
+
return {
|
|
1168
|
+
id,
|
|
1169
|
+
title: localizedTeamName(node.name) || id,
|
|
1170
|
+
avatar: avatars[id] || node.avatar || "",
|
|
1171
|
+
};
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
} catch (e) {}
|
|
1175
|
+
return null;
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1156
1178
|
// Remove teams tied to a cloud ACCOUNT (remote base_url — pulled from the cloud
|
|
1157
1179
|
// via pullCustomTeams, or user-added remote nodes) so switching/leaving an account
|
|
1158
1180
|
// doesn't leak them onto the next user. LOCAL machine teams on THIS device
|
|
@@ -1166,4 +1188,4 @@ function invalidateForAccountChange() {
|
|
|
1166
1188
|
log.info("[local-teams] account changed → team-list cache invalidated (no delete; filter by user_id)");
|
|
1167
1189
|
}
|
|
1168
1190
|
|
|
1169
|
-
module.exports = { list, openTeam, reloadTeam, closeLocalWindows, addTeam, removeTeam, updateTeam, upgradeTeam, syncAllLocalTeams, invalidateForAccountChange, setAvatar, getAvatars, avatarForUrl };
|
|
1191
|
+
module.exports = { list, openTeam, reloadTeam, closeLocalWindows, addTeam, removeTeam, updateTeam, upgradeTeam, syncAllLocalTeams, invalidateForAccountChange, setAvatar, getAvatars, avatarForUrl, teamIdentityForUrl };
|
|
@@ -255,7 +255,7 @@ function closeChromeProcess(pid) {
|
|
|
255
255
|
|
|
256
256
|
// macOS / Windows focus-stealing prevention keeps the spawning Electron
|
|
257
257
|
// app on top after we launch Chrome, so the user's keystrokes go into
|
|
258
|
-
// Electron. Nudge the OS to bring Chrome forward.
|
|
258
|
+
// Electron. Nudge the OS to bring Chrome forward without opening it again.
|
|
259
259
|
function bringChromeAppToForeground(binaryPath) {
|
|
260
260
|
try {
|
|
261
261
|
if (process.platform === "darwin") {
|
|
@@ -265,7 +265,15 @@ function bringChromeAppToForeground(binaryPath) {
|
|
|
265
265
|
}
|
|
266
266
|
const match = resolved && resolved.match(/^(.+\.app)\//);
|
|
267
267
|
if (!match) return;
|
|
268
|
-
|
|
268
|
+
// `open -a <bundle>` sends an OPEN event. With a managed Chrome already
|
|
269
|
+
// running under --user-data-dir, LaunchServices may handle that event by
|
|
270
|
+
// opening the default, argument-less Chrome too — one click then produces
|
|
271
|
+
// two Chrome windows. AppleScript `activate` only raises the running app.
|
|
272
|
+
const appName = path.basename(match[1], ".app").replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
273
|
+
spawn("/usr/bin/osascript", ["-e", `tell application "${appName}" to activate`], {
|
|
274
|
+
stdio: "ignore",
|
|
275
|
+
detached: true,
|
|
276
|
+
}).unref();
|
|
269
277
|
return;
|
|
270
278
|
}
|
|
271
279
|
if (process.platform === "win32") {
|
package/src/i18n/locales/en.json
CHANGED
package/src/i18n/locales/fr.json
CHANGED
package/src/i18n/locales/ja.json
CHANGED
package/src/main.js
CHANGED
|
@@ -1307,6 +1307,49 @@ electronApp.whenReady().then(async () => {
|
|
|
1307
1307
|
}
|
|
1308
1308
|
}
|
|
1309
1309
|
|
|
1310
|
+
const profileStore = require("./profiles/profile-store");
|
|
1311
|
+
const electronProfiles = (() => {
|
|
1312
|
+
let rows = [];
|
|
1313
|
+
try { rows = profileStore.listProfiles("electron"); } catch {}
|
|
1314
|
+
// Profile 0 is the resident CiCy system window and must not be exposed in
|
|
1315
|
+
// the macOS title/top-bar launcher. Profile 9 is Chrome's source template.
|
|
1316
|
+
rows = rows.filter((p) => ![0, 9].includes(Number(p.accountIdx)));
|
|
1317
|
+
return rows;
|
|
1318
|
+
})();
|
|
1319
|
+
const chromeProfiles = (() => {
|
|
1320
|
+
try { return profileStore.listProfiles("chrome"); } catch { return []; }
|
|
1321
|
+
})();
|
|
1322
|
+
const showProfileError = (kind, e) => {
|
|
1323
|
+
dialog.showMessageBox({
|
|
1324
|
+
type: "error",
|
|
1325
|
+
message: `${kind} profile 打开失败`,
|
|
1326
|
+
detail: String((e && e.message) || e),
|
|
1327
|
+
buttons: ["OK"],
|
|
1328
|
+
});
|
|
1329
|
+
};
|
|
1330
|
+
const openElectronProfile = async (accountIdx) => {
|
|
1331
|
+
try {
|
|
1332
|
+
const tabs = require("./tools/tab-browser-tools");
|
|
1333
|
+
await tabs.openTab(accountIdx, undefined, { activate: true });
|
|
1334
|
+
const m = tabs.ensureManager(accountIdx);
|
|
1335
|
+
if (m.win.isMinimized()) m.win.restore();
|
|
1336
|
+
m.win.show();
|
|
1337
|
+
m.win.focus();
|
|
1338
|
+
} catch (e) { showProfileError("Electron", e); }
|
|
1339
|
+
};
|
|
1340
|
+
const openChromeProfile = async (accountIdx) => {
|
|
1341
|
+
try {
|
|
1342
|
+
await require("./tools/chrome-tools").launchOrActivateProfile({
|
|
1343
|
+
accountIdx,
|
|
1344
|
+
activateIfRunning: true,
|
|
1345
|
+
});
|
|
1346
|
+
} catch (e) { showProfileError("Chrome", e); }
|
|
1347
|
+
};
|
|
1348
|
+
const openNativeChrome = () => {
|
|
1349
|
+
try { require("./tools/chrome-tools").launchNativeChrome(); }
|
|
1350
|
+
catch (e) { showProfileError("Chrome", e); }
|
|
1351
|
+
};
|
|
1352
|
+
|
|
1310
1353
|
const menuTemplate = [
|
|
1311
1354
|
...(process.platform === "darwin" ? [{ role: "appMenu" }] : []),
|
|
1312
1355
|
{
|
|
@@ -1344,6 +1387,24 @@ electronApp.whenReady().then(async () => {
|
|
|
1344
1387
|
{ label: i18n.t("menu.toggleFullscreen"), role: "togglefullscreen" },
|
|
1345
1388
|
],
|
|
1346
1389
|
},
|
|
1390
|
+
{
|
|
1391
|
+
label: "Electron",
|
|
1392
|
+
submenu: electronProfiles.map((p) => ({
|
|
1393
|
+
label: `Profile ${p.accountIdx}${p.name ? ` · ${p.name}` : ""}`,
|
|
1394
|
+
click: () => openElectronProfile(Number(p.accountIdx)),
|
|
1395
|
+
})),
|
|
1396
|
+
},
|
|
1397
|
+
{
|
|
1398
|
+
label: "Chrome",
|
|
1399
|
+
submenu: [
|
|
1400
|
+
{ label: "原生 Chrome", click: openNativeChrome },
|
|
1401
|
+
{ type: "separator" },
|
|
1402
|
+
...chromeProfiles.map((p) => ({
|
|
1403
|
+
label: `Profile ${p.accountIdx}${p.gmail ? ` · ${p.gmail}` : p.note ? ` · ${p.note}` : ""}`,
|
|
1404
|
+
click: () => openChromeProfile(Number(p.accountIdx)),
|
|
1405
|
+
})),
|
|
1406
|
+
],
|
|
1407
|
+
},
|
|
1347
1408
|
{
|
|
1348
1409
|
label: i18n.t("menu.window"),
|
|
1349
1410
|
submenu: [
|
|
@@ -12,8 +12,16 @@
|
|
|
12
12
|
// If cicy-code is momentarily unreachable we fall back to a minimal inline page so
|
|
13
13
|
// a fresh tab is never blank.
|
|
14
14
|
const { protocol, session } = require("electron");
|
|
15
|
+
const fs = require("fs");
|
|
16
|
+
const path = require("path");
|
|
15
17
|
const _handled = new WeakSet(); // sessions that already have the cicyui handler
|
|
16
18
|
|
|
19
|
+
// cicyui://panel/<id> — the split-webview panel page (opened by the tab strip's
|
|
20
|
+
// top-right "+"). <id> keeps each panel tab's URL unique so addTab's
|
|
21
|
+
// origin+pathname reuse never collapses two panels into one; the page keys its
|
|
22
|
+
// persisted layout off the same path.
|
|
23
|
+
const PANEL_HTML = path.join(__dirname, "split-panel.html");
|
|
24
|
+
|
|
17
25
|
// NOTE: scheme is "cicyui", NOT "cicy" — "cicy" is already an OS deep-link
|
|
18
26
|
// protocol client (setAsDefaultProtocolClient), so navigating a webContents to
|
|
19
27
|
// cicy://… gets dispatched externally (open-url) and the page never renders.
|
|
@@ -59,6 +67,14 @@ function handlerFor(ses, partition) {
|
|
|
59
67
|
ses.protocol.handle("cicyui", async (request) => {
|
|
60
68
|
let host = "";
|
|
61
69
|
try { host = new URL(request.url).hostname; } catch (e) {}
|
|
70
|
+
if (host === "panel") {
|
|
71
|
+
// read per request (not cached) so dev edits to split-panel.html land on
|
|
72
|
+
// a simple tab reload, no Electron restart.
|
|
73
|
+
try {
|
|
74
|
+
const html = await fs.promises.readFile(PANEL_HTML, "utf8");
|
|
75
|
+
return new Response(html, { headers: { "content-type": "text/html; charset=utf-8" } });
|
|
76
|
+
} catch (e) { return new Response("panel page missing", { status: 500 }); }
|
|
77
|
+
}
|
|
62
78
|
if (host !== "newtab") return new Response("not found", { status: 404 });
|
|
63
79
|
// Fetch the single-source page from cicy-code; fall back to the inline page.
|
|
64
80
|
try {
|
|
@@ -83,4 +99,5 @@ function ensureForPartition(partition) {
|
|
|
83
99
|
try { handlerFor(session.fromPartition(partition), partition); } catch (e) {}
|
|
84
100
|
}
|
|
85
101
|
|
|
86
|
-
|
|
102
|
+
const PANEL_URL_BASE = "cicyui://panel/";
|
|
103
|
+
module.exports = { NEWTAB_URL, PANEL_URL_BASE, registerScheme, installHandler, ensureForPartition, startPageHtml };
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
// Copyright 2026 CiCy AI
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
// panel-cells.js — BrowserView-backed cells for the split panel tab.
|
|
5
|
+
//
|
|
6
|
+
// Why not <webview>: windows opened from a <webview> guest NEVER get
|
|
7
|
+
// window.opener in Electron (long-standing architectural limit), which kills
|
|
8
|
+
// every OAuth popup flow (Google GSI posts the credential back via
|
|
9
|
+
// postMessage(window.opener) — no opener, no login). Cells are therefore real
|
|
10
|
+
// BrowserViews owned by main, layered ABOVE the panel tab's own BrowserView;
|
|
11
|
+
// the panel page (split-panel.html) stays the chrome: it computes each cell's
|
|
12
|
+
// body rect and syncs [{id,url,rect}] here over IPC. Bonus: each cell has a
|
|
13
|
+
// webContentsId, so the electron_tab_* tools (eval/screenshot/navigate) work
|
|
14
|
+
// on panel cells exactly like tabs.
|
|
15
|
+
const { BrowserView, ipcMain, webContents, session } = require("electron");
|
|
16
|
+
const fs = require("fs");
|
|
17
|
+
const os = require("os");
|
|
18
|
+
const path = require("path");
|
|
19
|
+
|
|
20
|
+
// Panel tabs only exist in profile 0, whose URL toolbar is hidden. Their page
|
|
21
|
+
// starts immediately below the 40px tab strip, so native cell BrowserViews do too.
|
|
22
|
+
const CHROME_H = 40;
|
|
23
|
+
|
|
24
|
+
// Each cell picks its PROFILE (accountIdx → persist:sandbox-N), default 1.
|
|
25
|
+
// Profile 0 is hard-forced DIRECT (no proxy — the gotty terminal ws must never
|
|
26
|
+
// route into mihomo), so external sites (x.com / accounts.google.com) belong in
|
|
27
|
+
// profile ≥1: per-profile proxy from account-N.json (fallback config.proxy)
|
|
28
|
+
// with localhost ALWAYS bypassed — team pages (127.0.0.1:8008) stay direct in
|
|
29
|
+
// every profile. Team auth is token-in-URL, so cookie isolation costs nothing.
|
|
30
|
+
const DEFAULT_PROFILE = 1;
|
|
31
|
+
const partitionFor = (idx) => `persist:sandbox-${idx}`;
|
|
32
|
+
// URL → profile routing (用户定的规则): localhost/127.0.0.1 → profile 0(直连、
|
|
33
|
+
// 与团队 tab 同会话);其余一律 profile 1(走代理)。按格子的目标 URL 定,变更时重建视图。
|
|
34
|
+
function isLocalCicyCode(url) {
|
|
35
|
+
try {
|
|
36
|
+
const u = new URL(url);
|
|
37
|
+
const local = u.hostname === "127.0.0.1" || u.hostname === "localhost" || u.hostname === "[::1]" || u.hostname === "::1";
|
|
38
|
+
return local && (u.port || "80") === "8008";
|
|
39
|
+
} catch (e) {}
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
function profileForCell(url, requested) {
|
|
43
|
+
if (isLocalCicyCode(url)) return 0;
|
|
44
|
+
const idx = Number(requested);
|
|
45
|
+
return Number.isInteger(idx) && idx > 0 ? idx : DEFAULT_PROFILE;
|
|
46
|
+
}
|
|
47
|
+
const appliedProxy = new Set(); // partitions whose proxy is already configured
|
|
48
|
+
function ensureCellSessionProxy(idx) {
|
|
49
|
+
const part = partitionFor(idx);
|
|
50
|
+
if (appliedProxy.has(part)) return;
|
|
51
|
+
appliedProxy.add(part);
|
|
52
|
+
if (idx === 0) return; // profile 0 stays direct — managed by window-utils, don't touch
|
|
53
|
+
try {
|
|
54
|
+
const profileStore = require("../profiles/profile-store");
|
|
55
|
+
let rules = "";
|
|
56
|
+
try { const p = profileStore.getProfile("electron", idx); rules = profileStore.proxyRules(p && p.proxy) || ""; } catch (e) {}
|
|
57
|
+
if (!rules) { try { rules = require("../config").config.proxy || ""; } catch (e) {} }
|
|
58
|
+
if (!rules) return;
|
|
59
|
+
session.fromPartition(part)
|
|
60
|
+
.setProxy({ proxyRules: rules, proxyBypassRules: "127.0.0.1,localhost,[::1]" })
|
|
61
|
+
.catch(() => {});
|
|
62
|
+
} catch (e) {}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// tab webContents.id -> PanelCells
|
|
66
|
+
const registry = new Map();
|
|
67
|
+
|
|
68
|
+
// strip CiCyDesktop/Electron UA tokens — Google OAuth rejects Electron UAs
|
|
69
|
+
// ("this browser may not be secure").
|
|
70
|
+
function scrubUA(wc) {
|
|
71
|
+
try { wc.setUserAgent(wc.getUserAgent().replace(/\s(CiCyDesktop|Electron)\/\S+/g, "")); } catch (e) {}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
class PanelCells {
|
|
75
|
+
constructor(manager, tab) {
|
|
76
|
+
this.m = manager; // owning TabManager
|
|
77
|
+
this.tabId = tab.id; // panel tab's webContents.id
|
|
78
|
+
this.views = new Map(); // cellId(string) -> { view, url }
|
|
79
|
+
this.visible = manager.activeId === tab.id;
|
|
80
|
+
try {
|
|
81
|
+
tab.view.webContents.once("destroyed", () => this.destroyAll());
|
|
82
|
+
} catch (e) {}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
tabWc() { try { return webContents.fromId(this.tabId); } catch (e) { return null; } }
|
|
86
|
+
sendState(payload) { const wc = this.tabWc(); if (wc && !wc.isDestroyed()) { try { wc.send("panelcells:state", payload); } catch (e) {} } }
|
|
87
|
+
|
|
88
|
+
create(cellId, profileIdx) {
|
|
89
|
+
ensureCellSessionProxy(profileIdx);
|
|
90
|
+
const view = new BrowserView({
|
|
91
|
+
webPreferences: {
|
|
92
|
+
// the cell's chosen profile session: proxied external web (profile ≥1),
|
|
93
|
+
// localhost bypassed. Plain sandboxed web content: no preload, no Node.
|
|
94
|
+
partition: partitionFor(profileIdx),
|
|
95
|
+
contextIsolation: true,
|
|
96
|
+
nodeIntegration: false,
|
|
97
|
+
sandbox: true,
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
const wc = view.webContents;
|
|
101
|
+
try { view.setBackgroundColor("#0d0d0f"); } catch (e) {}
|
|
102
|
+
try { wc.cicyAccountIdx = profileIdx; } catch (e) {}
|
|
103
|
+
scrubUA(wc);
|
|
104
|
+
try { require("../utils/context-menu-options").attachContextMenu(wc); } catch (e) {}
|
|
105
|
+
try { require("../utils/window-monitor").attachTabConsole(wc); } catch (e) {}
|
|
106
|
+
|
|
107
|
+
// Popups stay IN-APP as real child windows: `action:allow` keeps
|
|
108
|
+
// window.opener (OAuth needs it to postMessage the credential back).
|
|
109
|
+
// Named reuse: GSI opens the same frameName twice expecting to land in the
|
|
110
|
+
// SAME window (Chrome behavior); Electron creates a second one — so reuse
|
|
111
|
+
// by frameName manually, else Google login shows two windows.
|
|
112
|
+
const named = new Map(); // frameName -> BrowserWindow
|
|
113
|
+
try {
|
|
114
|
+
wc.setWindowOpenHandler(({ url, frameName }) => {
|
|
115
|
+
const ex = frameName && named.get(frameName);
|
|
116
|
+
if (ex && !ex.isDestroyed()) {
|
|
117
|
+
try { ex.loadURL(url); ex.focus(); } catch (e) {}
|
|
118
|
+
return { action: "deny" };
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
action: "allow",
|
|
122
|
+
overrideBrowserWindowOptions: {
|
|
123
|
+
autoHideMenuBar: true,
|
|
124
|
+
webPreferences: { contextIsolation: true, nodeIntegration: false, sandbox: true },
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
});
|
|
128
|
+
wc.on("did-create-window", (win, details) => {
|
|
129
|
+
scrubUA(win.webContents);
|
|
130
|
+
const fn = details && details.frameName;
|
|
131
|
+
if (fn) { named.set(fn, win); win.on("closed", () => { if (named.get(fn) === win) named.delete(fn); }); }
|
|
132
|
+
});
|
|
133
|
+
} catch (e) {}
|
|
134
|
+
|
|
135
|
+
// push nav state back to the panel chrome (url input / spinner / persistence)
|
|
136
|
+
const push = () => {
|
|
137
|
+
let url = "", title = "";
|
|
138
|
+
try { url = wc.getURL(); title = wc.getTitle(); } catch (e) {}
|
|
139
|
+
this.sendState({ id: cellId, url, title, loading: false, wcId: wc.id });
|
|
140
|
+
};
|
|
141
|
+
wc.on("did-start-loading", () => this.sendState({ id: cellId, loading: true, wcId: wc.id }));
|
|
142
|
+
wc.on("did-stop-loading", push);
|
|
143
|
+
wc.on("did-navigate", push);
|
|
144
|
+
wc.on("did-navigate-in-page", push);
|
|
145
|
+
wc.on("page-title-updated", push);
|
|
146
|
+
|
|
147
|
+
const rec = { view, url: "", profile: profileIdx };
|
|
148
|
+
this.views.set(String(cellId), rec);
|
|
149
|
+
return rec;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
place(rec, rect) {
|
|
153
|
+
if (!rect) return;
|
|
154
|
+
const b = {
|
|
155
|
+
x: Math.max(0, Math.round(rect.x)),
|
|
156
|
+
y: Math.max(0, Math.round(rect.y)) + CHROME_H,
|
|
157
|
+
width: Math.max(0, Math.round(rect.w)),
|
|
158
|
+
height: Math.max(0, Math.round(rect.h)),
|
|
159
|
+
};
|
|
160
|
+
try { rec.view.setBounds(b); } catch (e) {}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// cells: [{id, url, rect:{x,y,w,h}}] in panel-page CSS coords
|
|
164
|
+
sync(cells) {
|
|
165
|
+
if (!Array.isArray(cells)) return;
|
|
166
|
+
const seen = new Set();
|
|
167
|
+
for (const c of cells.slice(0, 24)) {
|
|
168
|
+
if (c == null || c.id == null) continue;
|
|
169
|
+
const key = String(c.id);
|
|
170
|
+
seen.add(key);
|
|
171
|
+
const url = (c.url && String(c.url)) || "";
|
|
172
|
+
const prof = profileForCell(url, c.profile);
|
|
173
|
+
let rec = this.views.get(key);
|
|
174
|
+
// locality change (localhost ↔ external) → partition must change → rebuild
|
|
175
|
+
if (rec && rec.profile !== prof) { this.destroyCell(key); rec = null; }
|
|
176
|
+
if (!rec) rec = this.create(c.id, prof);
|
|
177
|
+
if (url && rec.url !== url) {
|
|
178
|
+
rec.url = url;
|
|
179
|
+
try { rec.view.webContents.loadURL(url); } catch (e) {}
|
|
180
|
+
}
|
|
181
|
+
this.place(rec, c.rect);
|
|
182
|
+
}
|
|
183
|
+
for (const key of [...this.views.keys()]) if (!seen.has(key)) this.destroyCell(key);
|
|
184
|
+
this.updateAttach();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
reload(cellId) {
|
|
188
|
+
const rec = this.views.get(String(cellId));
|
|
189
|
+
if (rec) { try { rec.view.webContents.reload(); } catch (e) {} }
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async snapshots() {
|
|
193
|
+
const out = [];
|
|
194
|
+
for (const [id, rec] of this.views) {
|
|
195
|
+
try {
|
|
196
|
+
const image = await rec.view.webContents.capturePage();
|
|
197
|
+
if (!image.isEmpty()) out.push({ id, dataUrl: image.toDataURL() });
|
|
198
|
+
} catch (e) {}
|
|
199
|
+
}
|
|
200
|
+
return out;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
updateAttach() {
|
|
204
|
+
const win = this.m.win;
|
|
205
|
+
if (!win || win.isDestroyed()) return;
|
|
206
|
+
for (const rec of this.views.values()) {
|
|
207
|
+
try {
|
|
208
|
+
if (this.visible) win.addBrowserView(rec.view); // no-op if already attached
|
|
209
|
+
else win.removeBrowserView(rec.view);
|
|
210
|
+
} catch (e) {}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
show() { this.visible = true; this.updateAttach(); }
|
|
214
|
+
hide() { this.visible = false; this.updateAttach(); }
|
|
215
|
+
|
|
216
|
+
destroyCell(key) {
|
|
217
|
+
const rec = this.views.get(key);
|
|
218
|
+
if (!rec) return;
|
|
219
|
+
try { this.m.win.removeBrowserView(rec.view); } catch (e) {}
|
|
220
|
+
try { rec.view.webContents.close(); } catch (e) { try { rec.view.webContents.destroy(); } catch (_) {} }
|
|
221
|
+
this.views.delete(key);
|
|
222
|
+
}
|
|
223
|
+
destroyAll() {
|
|
224
|
+
for (const key of [...this.views.keys()]) this.destroyCell(key);
|
|
225
|
+
registry.delete(this.tabId);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function cellsForTab(manager, tab) {
|
|
230
|
+
let pc = registry.get(tab.id);
|
|
231
|
+
if (!pc) { pc = new PanelCells(manager, tab); registry.set(tab.id, pc); }
|
|
232
|
+
return pc;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// TabManager.activate hooks — hide the outgoing tab's cells, show the incoming's.
|
|
236
|
+
function onTabShown(manager, tabId) { const pc = registry.get(tabId); if (pc) pc.show(); }
|
|
237
|
+
function onTabHidden(manager, tabId) { const pc = registry.get(tabId); if (pc) pc.hide(); }
|
|
238
|
+
|
|
239
|
+
// ── IPC (installed once) ─────────────────────────────────────────────────────
|
|
240
|
+
let installed = false;
|
|
241
|
+
function installIpc(findTab) {
|
|
242
|
+
if (installed) return;
|
|
243
|
+
installed = true;
|
|
244
|
+
// e.sender is the PANEL PAGE's webContents; findTab maps it to (manager, tab).
|
|
245
|
+
const ctx = (e) => {
|
|
246
|
+
const hit = findTab(e.sender.id);
|
|
247
|
+
return hit ? cellsForTab(hit.manager, hit.tab) : null;
|
|
248
|
+
};
|
|
249
|
+
ipcMain.on("panelcells:sync", (e, { cells }) => { const pc = ctx(e); if (pc) pc.sync(cells); });
|
|
250
|
+
ipcMain.on("panelcells:reload", (e, { id }) => { const pc = ctx(e); if (pc) pc.reload(id); });
|
|
251
|
+
ipcMain.handle("panelcells:profiles", (e) => {
|
|
252
|
+
if (!ctx(e)) return [];
|
|
253
|
+
try {
|
|
254
|
+
return require("../profiles/profile-store").listProfiles("electron")
|
|
255
|
+
.filter((p) => Number.isInteger(Number(p.accountIdx)) && Number(p.accountIdx) > 0 && Number(p.accountIdx) !== 9)
|
|
256
|
+
.map((p) => ({ accountIdx: Number(p.accountIdx), name: String(p.name || "") }));
|
|
257
|
+
} catch (err) { return []; }
|
|
258
|
+
});
|
|
259
|
+
ipcMain.handle("panelcells:snapshots", async (e) => {
|
|
260
|
+
const pc = ctx(e);
|
|
261
|
+
return pc ? pc.snapshots() : [];
|
|
262
|
+
});
|
|
263
|
+
ipcMain.handle("panelcells:agents", async (e) => {
|
|
264
|
+
if (!ctx(e)) return { ok: false, agents: [], error: "invalid panel" };
|
|
265
|
+
try {
|
|
266
|
+
const cfg = JSON.parse(fs.readFileSync(path.join(os.homedir(), "cicy-ai", "global.json"), "utf8"));
|
|
267
|
+
const token = String(cfg.api_token || "");
|
|
268
|
+
const r = await fetch("http://127.0.0.1:8008/api/tmux/panes", {
|
|
269
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
270
|
+
signal: AbortSignal.timeout(5000),
|
|
271
|
+
});
|
|
272
|
+
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
|
273
|
+
const data = await r.json();
|
|
274
|
+
const panes = data?.panes || data?.data?.panes || [];
|
|
275
|
+
return {
|
|
276
|
+
ok: true,
|
|
277
|
+
agents: panes.map((p) => ({
|
|
278
|
+
id: String(p.pane_id || "").replace(/:.*$/, ""),
|
|
279
|
+
title: String(p.title || p.pane_id || ""),
|
|
280
|
+
type: String(p.agent_type || ""),
|
|
281
|
+
})).filter((p) => p.id),
|
|
282
|
+
};
|
|
283
|
+
} catch (err) {
|
|
284
|
+
return { ok: false, agents: [], error: err.message || String(err) };
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
// divider drag in the panel page: views would swallow pointer events — detach
|
|
288
|
+
// during the drag (page shows frame-only preview), reattach + re-place on up.
|
|
289
|
+
ipcMain.on("panelcells:drag", (e, { on }) => { const pc = ctx(e); if (pc) { if (on) pc.hide(); else pc.show(); } });
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
module.exports = { installIpc, onTabShown, onTabHidden };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Copyright 2026 CiCy AI
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
// Preload for the split panel page (cicyui://panel/<id>). Exposes window.panelAPI
|
|
5
|
+
// so the page (pure chrome: headers/dividers/layout) drives main-process
|
|
6
|
+
// BrowserView cells (panel-cells.js). Sandbox-safe: only contextBridge + ipcRenderer.
|
|
7
|
+
const { contextBridge, ipcRenderer } = require("electron");
|
|
8
|
+
|
|
9
|
+
contextBridge.exposeInMainWorld("panelAPI", {
|
|
10
|
+
// full desired state, main reconciles: [{id, url, rect:{x,y,w,h}}]
|
|
11
|
+
sync: (cells) => ipcRenderer.send("panelcells:sync", { cells }),
|
|
12
|
+
reload: (id) => ipcRenderer.send("panelcells:reload", { id }),
|
|
13
|
+
agents: () => ipcRenderer.invoke("panelcells:agents"),
|
|
14
|
+
profiles: () => ipcRenderer.invoke("panelcells:profiles"),
|
|
15
|
+
snapshots: () => ipcRenderer.invoke("panelcells:snapshots"),
|
|
16
|
+
// divider drag: BrowserViews sit ABOVE the page and would swallow pointermove —
|
|
17
|
+
// detach them for the duration of the drag (frame-only preview), reattach on up.
|
|
18
|
+
dragging: (on) => ipcRenderer.send("panelcells:drag", { on: !!on }),
|
|
19
|
+
onCellState: (cb) => {
|
|
20
|
+
const h = (_e, s) => { try { cb(s); } catch (e) {} };
|
|
21
|
+
ipcRenderer.on("panelcells:state", h);
|
|
22
|
+
return () => ipcRenderer.removeListener("panelcells:state", h);
|
|
23
|
+
},
|
|
24
|
+
});
|