cicy-desktop 2.1.271 → 2.1.273
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/linux-app-release.yml +1 -1
- package/.github/workflows/mac-app-release.yml +5 -6
- package/.github/workflows/windows-exe-release.yml +5 -6
- package/package.json +5 -5
- package/src/backends/webview-preload.js +42 -0
- package/src/chrome/chrome-launcher.js +67 -4
- package/src/tabbrowser/tab-shell.html +30 -13
- package/src/tabbrowser/tab-theme.js +18 -0
- package/src/tools/chrome-tools.js +34 -10
- package/src/tools/desktop-snapshot-tools.js +5 -11
- package/src/tools/tab-browser-tools.js +74 -22
- package/src/utils/desktop-snapshot.js +3 -2
- package/test/chrome-launcher.test.js +71 -0
- package/test/tab-theme.test.js +44 -0
|
@@ -110,7 +110,7 @@ jobs:
|
|
|
110
110
|
shell: bash
|
|
111
111
|
env:
|
|
112
112
|
CSC_IDENTITY_AUTO_DISCOVERY: "false"
|
|
113
|
-
GH_TOKEN: ${{ secrets.
|
|
113
|
+
GH_TOKEN: ${{ secrets.CICY_RELEASE_GH_TOKEN }}
|
|
114
114
|
CICY_OBFUSCATE: "1" # 发版混淆 JS(prebuild 主进程 + Vite 渲染层),仅 release CI
|
|
115
115
|
run: |
|
|
116
116
|
set -euo pipefail
|
|
@@ -136,12 +136,11 @@ jobs:
|
|
|
136
136
|
- name: Ensure draft GitHub release exists
|
|
137
137
|
shell: bash
|
|
138
138
|
env:
|
|
139
|
-
GH_TOKEN: ${{ secrets.
|
|
139
|
+
GH_TOKEN: ${{ secrets.CICY_RELEASE_GH_TOKEN }}
|
|
140
140
|
run: |
|
|
141
141
|
gh release create "${{ steps.meta.outputs.tag }}" \
|
|
142
|
-
--repo "
|
|
142
|
+
--repo "cicy-ai/cicy-desktop" \
|
|
143
143
|
--title "CiCy Desktop v${{ steps.meta.outputs.version }}" \
|
|
144
|
-
--target "$GITHUB_SHA" \
|
|
145
144
|
--generate-notes \
|
|
146
145
|
--draft \
|
|
147
146
|
2>/dev/null || echo "release already exists, continuing"
|
|
@@ -149,7 +148,7 @@ jobs:
|
|
|
149
148
|
- name: Build and publish PKG installers
|
|
150
149
|
shell: bash
|
|
151
150
|
env:
|
|
152
|
-
GH_TOKEN: ${{ secrets.
|
|
151
|
+
GH_TOKEN: ${{ secrets.CICY_RELEASE_GH_TOKEN }}
|
|
153
152
|
run: |
|
|
154
153
|
set -euo pipefail
|
|
155
154
|
VER="${{ steps.meta.outputs.version }}"
|
|
@@ -184,7 +183,7 @@ jobs:
|
|
|
184
183
|
pkgbuild --root "$ROOT" --component-plist "$PLIST" --scripts "$SCR" \
|
|
185
184
|
--identifier com.cicy.desktop --version "$VER" --install-location / "$PKG"
|
|
186
185
|
gh release upload "${{ steps.meta.outputs.tag }}" "$PKG" \
|
|
187
|
-
--repo "
|
|
186
|
+
--repo "cicy-ai/cicy-desktop" --clobber
|
|
188
187
|
echo "uploaded $PKG ($(du -h "$PKG" | cut -f1))"
|
|
189
188
|
done
|
|
190
189
|
|
|
@@ -72,12 +72,11 @@ jobs:
|
|
|
72
72
|
# uploaded. Promoted to non-draft + latest in the final step.
|
|
73
73
|
shell: bash
|
|
74
74
|
env:
|
|
75
|
-
GH_TOKEN: ${{ secrets.
|
|
75
|
+
GH_TOKEN: ${{ secrets.CICY_RELEASE_GH_TOKEN }}
|
|
76
76
|
run: |
|
|
77
77
|
gh release create "${{ steps.meta.outputs.tag }}" \
|
|
78
|
-
--repo "
|
|
78
|
+
--repo "cicy-ai/cicy-desktop" \
|
|
79
79
|
--title "CiCy Desktop v${{ steps.meta.outputs.version }}" \
|
|
80
|
-
--target "$GITHUB_SHA" \
|
|
81
80
|
--generate-notes \
|
|
82
81
|
--draft \
|
|
83
82
|
2>/dev/null || echo "release already exists, continuing"
|
|
@@ -86,7 +85,7 @@ jobs:
|
|
|
86
85
|
# --publish always uploads .exe + latest.yml to the release created above.
|
|
87
86
|
shell: powershell
|
|
88
87
|
env:
|
|
89
|
-
GH_TOKEN: ${{ secrets.
|
|
88
|
+
GH_TOKEN: ${{ secrets.CICY_RELEASE_GH_TOKEN }}
|
|
90
89
|
CICY_OBFUSCATE: "1" # 发版混淆 JS(prebuild 主进程 + Vite 渲染层),仅 release CI
|
|
91
90
|
run: |
|
|
92
91
|
if (Test-Path dist) { Remove-Item -Recurse -Force dist }
|
|
@@ -177,9 +176,9 @@ jobs:
|
|
|
177
176
|
# existing clients will pick up the new version on its next poll.
|
|
178
177
|
shell: bash
|
|
179
178
|
env:
|
|
180
|
-
GH_TOKEN: ${{ secrets.
|
|
179
|
+
GH_TOKEN: ${{ secrets.CICY_RELEASE_GH_TOKEN }}
|
|
181
180
|
run: |
|
|
182
181
|
gh release edit "${{ steps.meta.outputs.tag }}" \
|
|
183
|
-
--repo "
|
|
182
|
+
--repo "cicy-ai/cicy-desktop" \
|
|
184
183
|
--draft=false \
|
|
185
184
|
--latest
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cicy-desktop",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.273",
|
|
4
4
|
"description": "CiCy - AI-powered operating system browser",
|
|
5
5
|
"main": "src/main.js",
|
|
6
6
|
"bin": {
|
|
@@ -143,10 +143,10 @@
|
|
|
143
143
|
"//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 着无害。",
|
|
144
144
|
"optionalDependencies": {
|
|
145
145
|
"electron": "41.0.3",
|
|
146
|
-
"cicy-code-darwin-x64": "2.3.
|
|
147
|
-
"cicy-code-darwin-arm64": "2.3.
|
|
148
|
-
"cicy-code-linux-x64": "2.3.
|
|
149
|
-
"cicy-code-linux-arm64": "2.3.
|
|
146
|
+
"cicy-code-darwin-x64": "2.3.556",
|
|
147
|
+
"cicy-code-darwin-arm64": "2.3.556",
|
|
148
|
+
"cicy-code-linux-x64": "2.3.556",
|
|
149
|
+
"cicy-code-linux-arm64": "2.3.556",
|
|
150
150
|
"cicy-code-windows-x64": "2.3.193",
|
|
151
151
|
"cicy-mihomo-darwin-x64": "1.10.4",
|
|
152
152
|
"cicy-mihomo-darwin-arm64": "1.10.4",
|
|
@@ -24,6 +24,48 @@
|
|
|
24
24
|
// webview gets the awaited promise
|
|
25
25
|
|
|
26
26
|
const { contextBridge, ipcRenderer } = require("electron");
|
|
27
|
+
const { resolveReportedCicyTheme } = require("../tabbrowser/tab-theme");
|
|
28
|
+
|
|
29
|
+
// cicy-code owns the theme preference (`cicy_theme`) and publishes the applied
|
|
30
|
+
// value on <html data-theme>. Report that contract to the owning tab window so
|
|
31
|
+
// its native tab strip can use the same light/dark palette. The main process
|
|
32
|
+
// accepts this signal only from a registered team tab, so other pages carrying
|
|
33
|
+
// this preload cannot recolor Desktop chrome.
|
|
34
|
+
let lastReportedTheme = "";
|
|
35
|
+
function reportCicyTheme() {
|
|
36
|
+
let documentTheme = "";
|
|
37
|
+
let savedTheme = "";
|
|
38
|
+
try { documentTheme = document.documentElement?.dataset?.theme || ""; } catch (_) {}
|
|
39
|
+
try { savedTheme = localStorage.getItem("cicy_theme") || ""; } catch (_) {}
|
|
40
|
+
const theme = resolveReportedCicyTheme(documentTheme, savedTheme);
|
|
41
|
+
if (theme === lastReportedTheme) return;
|
|
42
|
+
lastReportedTheme = theme;
|
|
43
|
+
try { ipcRenderer.send("tab-content:cicy-theme", { theme }); } catch (_) {}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function watchCicyTheme() {
|
|
47
|
+
reportCicyTheme();
|
|
48
|
+
try {
|
|
49
|
+
const root = document.documentElement;
|
|
50
|
+
if (root) {
|
|
51
|
+
new MutationObserver(reportCicyTheme).observe(root, {
|
|
52
|
+
attributes: true,
|
|
53
|
+
attributeFilter: ["data-theme"],
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
} catch (_) {}
|
|
57
|
+
try { window.addEventListener("cicy-theme-change", reportCicyTheme); } catch (_) {}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// The saved preference is already available at preload time, which prevents a
|
|
61
|
+
// dark-strip flash while a light cicy-code page mounts. DOMContentLoaded then
|
|
62
|
+
// installs the live observer for changes made from Settings.
|
|
63
|
+
reportCicyTheme();
|
|
64
|
+
if (document.readyState === "loading") {
|
|
65
|
+
window.addEventListener("DOMContentLoaded", watchCicyTheme, { once: true });
|
|
66
|
+
} else {
|
|
67
|
+
watchCicyTheme();
|
|
68
|
+
}
|
|
27
69
|
|
|
28
70
|
const relay = (type, payload) =>
|
|
29
71
|
ipcRenderer.invoke("webview:relay", { type, ...(payload || {}) });
|
|
@@ -160,7 +160,14 @@ function resolveChromeBinary(binaryPath) {
|
|
|
160
160
|
);
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
-
function buildChromeArgs({
|
|
163
|
+
function buildChromeArgs({
|
|
164
|
+
userDataDirRoot,
|
|
165
|
+
profileDirectory,
|
|
166
|
+
debuggerPort,
|
|
167
|
+
proxy,
|
|
168
|
+
url,
|
|
169
|
+
restoreLastSession = false,
|
|
170
|
+
}) {
|
|
164
171
|
const args = [
|
|
165
172
|
`--user-data-dir=${userDataDirRoot}`,
|
|
166
173
|
`--profile-directory=${profileDirectory}`,
|
|
@@ -170,6 +177,10 @@ function buildChromeArgs({ userDataDirRoot, profileDirectory, debuggerPort, prox
|
|
|
170
177
|
"--no-default-browser-check",
|
|
171
178
|
];
|
|
172
179
|
|
|
180
|
+
if (restoreLastSession) {
|
|
181
|
+
args.push("--restore-last-session");
|
|
182
|
+
}
|
|
183
|
+
|
|
173
184
|
if (proxy) {
|
|
174
185
|
args.push(`--proxy-server=${proxy}`);
|
|
175
186
|
}
|
|
@@ -181,6 +192,18 @@ function buildChromeArgs({ userDataDirRoot, profileDirectory, debuggerPort, prox
|
|
|
181
192
|
return args;
|
|
182
193
|
}
|
|
183
194
|
|
|
195
|
+
function resolveLiveProfileAction({
|
|
196
|
+
platform = process.platform,
|
|
197
|
+
liveStatus,
|
|
198
|
+
runtime,
|
|
199
|
+
windowless = false,
|
|
200
|
+
}) {
|
|
201
|
+
if (!liveStatus?.isRunning) return "launch";
|
|
202
|
+
if (windowless) return "restart-windowless";
|
|
203
|
+
if (platform === "darwin" && !runtime?.pid) return "restart-unmanaged";
|
|
204
|
+
return "reuse";
|
|
205
|
+
}
|
|
206
|
+
|
|
184
207
|
async function ensurePortAvailable(debuggerPort) {
|
|
185
208
|
const open = await isPortOpen(debuggerPort, "127.0.0.1", 500);
|
|
186
209
|
if (open) {
|
|
@@ -206,6 +229,7 @@ async function launchChrome({
|
|
|
206
229
|
url,
|
|
207
230
|
userDataDirRoot,
|
|
208
231
|
userDataBaseRoot = DEFAULT_USER_DATA_BASE_ROOT,
|
|
232
|
+
restoreLastSession = false,
|
|
209
233
|
}) {
|
|
210
234
|
const profileDirectory = getProfileDirectory(accountIdx);
|
|
211
235
|
const effectiveUserDataDirRoot = userDataDirRoot || getDefaultUserDataDirRoot(accountIdx, userDataBaseRoot);
|
|
@@ -221,6 +245,7 @@ async function launchChrome({
|
|
|
221
245
|
debuggerPort,
|
|
222
246
|
proxy,
|
|
223
247
|
url,
|
|
248
|
+
restoreLastSession,
|
|
224
249
|
});
|
|
225
250
|
const child = spawn(binaryPath, args, {
|
|
226
251
|
detached: process.platform !== "win32",
|
|
@@ -231,7 +256,7 @@ async function launchChrome({
|
|
|
231
256
|
child.unref();
|
|
232
257
|
|
|
233
258
|
const version = await waitForDebugger(debuggerPort, "127.0.0.1", 15000);
|
|
234
|
-
bringChromeAppToForeground(binaryPath);
|
|
259
|
+
bringChromeAppToForeground(binaryPath, { pid: child.pid, debuggerPort });
|
|
235
260
|
|
|
236
261
|
return {
|
|
237
262
|
pid: child.pid,
|
|
@@ -256,12 +281,49 @@ function closeChromeProcess(pid) {
|
|
|
256
281
|
// macOS / Windows focus-stealing prevention keeps the spawning Electron
|
|
257
282
|
// app on top after we launch Chrome, so the user's keystrokes go into
|
|
258
283
|
// Electron. Nudge the OS to bring Chrome forward without opening it again.
|
|
259
|
-
function
|
|
284
|
+
function getListeningPid(port) {
|
|
285
|
+
if (!Number.isInteger(port) || port <= 0 || port >= 65536) return null;
|
|
286
|
+
const candidates = process.platform === "darwin" ? ["/usr/sbin/lsof", "lsof"] : ["lsof"];
|
|
287
|
+
for (const command of candidates) {
|
|
288
|
+
try {
|
|
289
|
+
const out = execFileSync(command, ["-nP", `-tiTCP:${port}`, "-sTCP:LISTEN"], {
|
|
290
|
+
encoding: "utf8",
|
|
291
|
+
timeout: 2000,
|
|
292
|
+
});
|
|
293
|
+
const pid = Number(
|
|
294
|
+
String(out || "")
|
|
295
|
+
.trim()
|
|
296
|
+
.split(/\s+/)[0]
|
|
297
|
+
);
|
|
298
|
+
if (Number.isInteger(pid) && pid > 0) return pid;
|
|
299
|
+
} catch (_) {}
|
|
300
|
+
}
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function bringChromeAppToForeground(binaryPath, { pid, debuggerPort } = {}) {
|
|
260
305
|
try {
|
|
261
306
|
if (process.platform === "darwin") {
|
|
307
|
+
const targetPid = Number.isInteger(pid) && pid > 0 ? pid : getListeningPid(debuggerPort);
|
|
308
|
+
if (targetPid) {
|
|
309
|
+
const script =
|
|
310
|
+
"ObjC.import('AppKit'); " +
|
|
311
|
+
`const app = $.NSRunningApplication.runningApplicationWithProcessIdentifier(${targetPid}); ` +
|
|
312
|
+
"if (app) app.activateWithOptions($.NSApplicationActivateIgnoringOtherApps);";
|
|
313
|
+
spawn("/usr/bin/osascript", ["-l", "JavaScript", "-e", script], {
|
|
314
|
+
stdio: "ignore",
|
|
315
|
+
detached: true,
|
|
316
|
+
}).unref();
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
262
320
|
let resolved = binaryPath;
|
|
263
321
|
if (!resolved || !fs.existsSync(resolved)) {
|
|
264
|
-
try {
|
|
322
|
+
try {
|
|
323
|
+
resolved = resolveChromeBinary();
|
|
324
|
+
} catch (_) {
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
265
327
|
}
|
|
266
328
|
const match = resolved && resolved.match(/^(.+\.app)\//);
|
|
267
329
|
if (!match) return;
|
|
@@ -301,6 +363,7 @@ module.exports = {
|
|
|
301
363
|
getDefaultDebuggerPort,
|
|
302
364
|
resolveChromeBinary,
|
|
303
365
|
buildChromeArgs,
|
|
366
|
+
resolveLiveProfileAction,
|
|
304
367
|
launchChrome,
|
|
305
368
|
closeChromeProcess,
|
|
306
369
|
bringChromeAppToForeground,
|
|
@@ -12,9 +12,21 @@
|
|
|
12
12
|
the same color as the toolbar/content (#0A0A0A) so it merges into the
|
|
13
13
|
page, carved out of the lighter strip with outward bottom curves.
|
|
14
14
|
(content is near-black, so a darker strip would be invisible → go lighter.) */
|
|
15
|
+
color-scheme:dark;
|
|
15
16
|
--bg:#0A0A0A; --strip:#1c1c20; --toolbar:#0A0A0A;
|
|
16
17
|
--fg:#e6e6e9; --muted:#8b8b92; --line:rgba(255,255,255,.06);
|
|
17
18
|
--accent:#3b82f6; --omni:#1a1a1c;
|
|
19
|
+
--hover:rgba(255,255,255,.06); --hover-strong:rgba(255,255,255,.16);
|
|
20
|
+
--active-fg:#fff; --separator:rgba(255,255,255,.12);
|
|
21
|
+
--edit-bg:#111113; --focus-bg:#111113; --spinner-track:rgba(255,255,255,.25);
|
|
22
|
+
}
|
|
23
|
+
html[data-theme="light"]{
|
|
24
|
+
color-scheme:light;
|
|
25
|
+
--bg:#f4f4f5; --strip:#e4e4e7; --toolbar:#f4f4f5;
|
|
26
|
+
--fg:#27272a; --muted:#71717a; --line:rgba(39,39,42,.14);
|
|
27
|
+
--omni:#fff; --hover:rgba(39,39,42,.07); --hover-strong:rgba(39,39,42,.13);
|
|
28
|
+
--active-fg:#18181b; --separator:rgba(39,39,42,.16);
|
|
29
|
+
--edit-bg:#fff; --focus-bg:#fff; --spinner-track:rgba(39,39,42,.22);
|
|
18
30
|
}
|
|
19
31
|
*{box-sizing:border-box;margin:0;padding:0}
|
|
20
32
|
html,body{height:100%;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:var(--bg);color:var(--fg);overflow:hidden;-webkit-user-select:none;user-select:none}
|
|
@@ -39,7 +51,7 @@
|
|
|
39
51
|
.tab,#newtab{-webkit-app-region:no-drag}
|
|
40
52
|
#tabs::-webkit-scrollbar{height:0}
|
|
41
53
|
.tab{position:relative;display:flex;align-items:center;gap:9px;height:32px;min-width:60px;max-width:240px;padding:0 16px;border-radius:10px 10px 0 0;font-size:12.5px;color:var(--muted);background:transparent;cursor:default;flex:1 1 0;transition:background .12s,color .12s}
|
|
42
|
-
.tab:hover{background:
|
|
54
|
+
.tab:hover{background:var(--hover);color:var(--fg)}
|
|
43
55
|
/* home = the resident homepage tab: pinned first, fixed width, user icon, no
|
|
44
56
|
title text, no close — like Chrome's profile/avatar button. */
|
|
45
57
|
/* start-page tab: pinned, sizes to its "我的团队" label (not a flex-grow tab) */
|
|
@@ -52,7 +64,7 @@
|
|
|
52
64
|
so drop the outward ears — they'd otherwise meet the homepage top with a seam. */
|
|
53
65
|
.tab.home.active::before,.tab.home.active::after{display:none}
|
|
54
66
|
/* active tab = toolbar/content color, merged into the page, with outward bottom curves */
|
|
55
|
-
.tab.active{background:var(--toolbar);color
|
|
67
|
+
.tab.active{background:var(--toolbar);color:var(--active-fg);z-index:1}
|
|
56
68
|
/* outward bottom "ears" carved from the strip into the toolbar. gradient goes
|
|
57
69
|
strip→toolbar (NOT transparent→toolbar): a transparent→opaque ramp passes
|
|
58
70
|
through semi-transparent black and leaves a dark fringe on the lighter strip;
|
|
@@ -67,21 +79,21 @@
|
|
|
67
79
|
first tab and around the active/hovered tab (Chrome hides the seam that would
|
|
68
80
|
touch an active or hovered tab on either side). Inactive tabs don't use
|
|
69
81
|
::before (that's the active tab's ear), so it's free here. */
|
|
70
|
-
.tab:not(.active)::before{content:"";position:absolute;left:-3.5px;top:50%;transform:translateY(-50%);width:1px;height:16px;background:
|
|
82
|
+
.tab:not(.active)::before{content:"";position:absolute;left:-3.5px;top:50%;transform:translateY(-50%);width:1px;height:16px;background:var(--separator);pointer-events:none}
|
|
71
83
|
#tabs>.tab:first-child::before{display:none} /* nothing left of the first tab */
|
|
72
84
|
.tab:hover::before{display:none} /* no line on the hovered tab's left */
|
|
73
85
|
.tab.active+.tab::before,.tab:hover+.tab::before{display:none} /* none on the active/hovered tab's right */
|
|
74
86
|
.fav{width:16px;height:16px;flex:0 0 16px;border-radius:4px;display:flex;align-items:center;justify-content:center;overflow:hidden}
|
|
75
87
|
.fav img{width:16px;height:16px;object-fit:contain}
|
|
76
|
-
.spin{width:14px;height:14px;border:2px solid
|
|
88
|
+
.spin{width:14px;height:14px;border:2px solid var(--spinner-track);border-top-color:var(--accent);border-radius:50%;animation:sp .7s linear infinite}
|
|
77
89
|
@keyframes sp{to{transform:rotate(360deg)}}
|
|
78
90
|
.ttl{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
79
|
-
.ttl-edit{flex:1;min-width:0;height:22px;padding:0 5px;border:1px solid var(--accent);border-radius:5px;outline:none;background
|
|
91
|
+
.ttl-edit{flex:1;min-width:0;height:22px;padding:0 5px;border:1px solid var(--accent);border-radius:5px;outline:none;background:var(--edit-bg);color:var(--fg);font-size:12.5px;-webkit-user-select:text;user-select:text}
|
|
80
92
|
.cls{flex:0 0 16px;width:16px;height:16px;border-radius:50%;display:flex;align-items:center;justify-content:center;opacity:0;color:var(--muted);transition:opacity .1s,background .1s}
|
|
81
93
|
.tab:hover .cls,.tab.active .cls{opacity:.85}
|
|
82
|
-
.cls:hover{background:
|
|
94
|
+
.cls:hover{background:var(--hover-strong);color:var(--fg);opacity:1}
|
|
83
95
|
#newtab{flex:0 0 30px;width:30px;height:30px;margin:2px 0 2px 2px;border-radius:50%;display:flex;align-items:center;justify-content:center;color:var(--muted);cursor:default}
|
|
84
|
-
#newtab:hover{background:
|
|
96
|
+
#newtab:hover{background:var(--hover);color:var(--fg)}
|
|
85
97
|
/* ── drag-to-reorder (Chrome-style) ── */
|
|
86
98
|
.tab.dragging{opacity:.4}
|
|
87
99
|
/* vertical insertion bar shown between tabs while dragging */
|
|
@@ -93,7 +105,7 @@
|
|
|
93
105
|
button sits left of it (fullscreen hides the overlay → reclaim). */
|
|
94
106
|
#panelbtn{position:fixed;top:5px;right:8px;z-index:30;width:30px;height:30px;border-radius:8px;display:none;align-items:center;justify-content:center;color:var(--muted);-webkit-app-region:no-drag;cursor:default}
|
|
95
107
|
body.has-panel #panelbtn{display:flex}
|
|
96
|
-
#panelbtn:hover{background:
|
|
108
|
+
#panelbtn:hover{background:var(--hover);color:var(--fg)}
|
|
97
109
|
body.is-win #panelbtn{right:148px}
|
|
98
110
|
body.is-win.is-fullscreen #panelbtn{right:8px}
|
|
99
111
|
/* keep tabs from sliding under the pinned button */
|
|
@@ -107,10 +119,10 @@
|
|
|
107
119
|
The main process also moves its BrowserView content up to STRIP_H. */
|
|
108
120
|
body.profile-0 #bar{display:none}
|
|
109
121
|
.nav{width:32px;height:32px;flex:0 0 32px;border-radius:50%;display:flex;align-items:center;justify-content:center;color:var(--fg);cursor:default}
|
|
110
|
-
.nav:hover{background:
|
|
122
|
+
.nav:hover{background:var(--hover)}
|
|
111
123
|
.nav.off{color:var(--muted);opacity:.35;pointer-events:none}
|
|
112
124
|
#omni{flex:1;height:30px;display:flex;align-items:center;gap:8px;margin-left:6px;padding:0 12px;background:var(--omni);border:1px solid var(--line);border-radius:16px;transition:border-color .12s,background .12s}
|
|
113
|
-
#omni.focus{background
|
|
125
|
+
#omni.focus{background:var(--focus-bg);border-color:var(--accent)}
|
|
114
126
|
#lead{flex:0 0 16px;width:16px;height:16px;color:var(--muted);display:flex;align-items:center;justify-content:center}
|
|
115
127
|
#url{flex:1;height:100%;background:transparent;border:none;outline:none;color:var(--fg);font-size:13px;-webkit-user-select:text;user-select:text}
|
|
116
128
|
#url::placeholder{color:var(--muted)}
|
|
@@ -149,7 +161,7 @@
|
|
|
149
161
|
window.addEventListener('contextmenu', (e) => {
|
|
150
162
|
if (!(e.target instanceof HTMLInputElement && e.target.id === 'url')) e.preventDefault();
|
|
151
163
|
});
|
|
152
|
-
const ICON_GLOBE ='<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="
|
|
164
|
+
const ICON_GLOBE ='<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>';
|
|
153
165
|
// 「我的团队」标签的品牌标识 = 与首页 brand-mark 同款 BrandGlyph(白色六芒星)在蓝→紫渐变方块上。
|
|
154
166
|
const CICY_LOGO = '<span class="cicy-logo"><svg width="12" height="12" viewBox="0 0 96 96" fill="none"><path d="M48 11L39.5 33.3L16 29.5L31 48L16 66.5L39.5 62.7L48 85L56.5 62.7L80 66.5L65 48L80 29.5L56.5 33.3Z" fill="white" stroke="white" stroke-width="8" stroke-linejoin="round" stroke-linecap="round"/></svg></span>';
|
|
155
167
|
const ICON_LOCK = '<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>';
|
|
@@ -347,7 +359,7 @@
|
|
|
347
359
|
const isHome = !!(activeTab && activeTab.home);
|
|
348
360
|
const u = nav.url || '';
|
|
349
361
|
const secure = /^https:\/\//i.test(u);
|
|
350
|
-
leadEl.style.color = secure ? '#
|
|
362
|
+
leadEl.style.color = secure ? '#3f8f5b' : 'var(--muted)';
|
|
351
363
|
leadEl.innerHTML = secure ? ICON_LOCK : ICON_GLOBE;
|
|
352
364
|
// homepage tab → empty omnibox (like Chrome's new-tab page), don't expose the file:// path
|
|
353
365
|
if (!urlFocused) urlEl.value = isHome ? '' : displayUrl(u);
|
|
@@ -362,7 +374,12 @@
|
|
|
362
374
|
// Skip re-render while a drag is in flight — rebuilding tabsEl.innerHTML mid-drag
|
|
363
375
|
// would destroy the dragged element and abort the gesture. State is still stored;
|
|
364
376
|
// dragend triggers a render to sync.
|
|
365
|
-
window.tabAPI.onState((s) => {
|
|
377
|
+
window.tabAPI.onState((s) => {
|
|
378
|
+
state = s || { tabs: [], nav: {} };
|
|
379
|
+
document.documentElement.dataset.theme = state.theme === 'light' ? 'light' : 'dark';
|
|
380
|
+
if (dragId != null) return;
|
|
381
|
+
render();
|
|
382
|
+
});
|
|
366
383
|
|
|
367
384
|
function go() {
|
|
368
385
|
let v = urlEl.value.trim(); if (!v) return;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Copyright 2026 CiCy AI
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
function normalizeCicyTheme(theme) {
|
|
5
|
+
return theme === "light" || theme === "dark" ? theme : null;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function resolveReportedCicyTheme(documentTheme, savedTheme) {
|
|
9
|
+
return normalizeCicyTheme(documentTheme) || normalizeCicyTheme(savedTheme) || "light";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function resolveTabChromeTheme(tabs) {
|
|
13
|
+
const active = Array.isArray(tabs) ? tabs.find((tab) => tab && tab.active) : null;
|
|
14
|
+
if (!active || !active.team) return "dark";
|
|
15
|
+
return normalizeCicyTheme(active.cicyTheme) || "dark";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
module.exports = { normalizeCicyTheme, resolveReportedCicyTheme, resolveTabChromeTheme };
|
|
@@ -16,6 +16,7 @@ const {
|
|
|
16
16
|
launchChrome,
|
|
17
17
|
closeChromeProcess,
|
|
18
18
|
bringChromeAppToForeground,
|
|
19
|
+
resolveLiveProfileAction,
|
|
19
20
|
} = require("../chrome/chrome-launcher");
|
|
20
21
|
const { isPortOpen } = require("../utils/process-utils");
|
|
21
22
|
const { getVersion, getTargets, createTarget, activateTarget, callCdp } = require("../chrome/chrome-cdp-client");
|
|
@@ -404,6 +405,7 @@ async function launchOrActivateProfile({
|
|
|
404
405
|
|
|
405
406
|
// Script parity: if /json/version reachable => activate first page target and return reused
|
|
406
407
|
const liveStatus = await probeChromeDebugger(effectivePort);
|
|
408
|
+
let restoreLastSession = false;
|
|
407
409
|
if (liveStatus.isRunning) {
|
|
408
410
|
// Detect a windowless Chrome — debugger up but 0 page targets. It lingers on
|
|
409
411
|
// macOS after its last window is closed; reusing it is unreliable because a
|
|
@@ -419,13 +421,19 @@ async function launchOrActivateProfile({
|
|
|
419
421
|
windowless = false;
|
|
420
422
|
}
|
|
421
423
|
|
|
422
|
-
|
|
424
|
+
const liveAction = resolveLiveProfileAction({
|
|
425
|
+
liveStatus,
|
|
426
|
+
runtime: registry.get(accountIdx),
|
|
427
|
+
windowless,
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
if (liveAction === "reuse") {
|
|
423
431
|
const { targets, activatedTargetId } = await ensurePageTargets({
|
|
424
432
|
debuggerPort: effectivePort,
|
|
425
433
|
url,
|
|
426
434
|
activateIfRunning,
|
|
427
435
|
});
|
|
428
|
-
if (activateIfRunning) bringChromeAppToForeground();
|
|
436
|
+
if (activateIfRunning) bringChromeAppToForeground(undefined, { debuggerPort: effectivePort });
|
|
429
437
|
|
|
430
438
|
const nextRuntime = registry.upsert(accountIdx, {
|
|
431
439
|
status: "running",
|
|
@@ -454,13 +462,8 @@ async function launchOrActivateProfile({
|
|
|
454
462
|
};
|
|
455
463
|
}
|
|
456
464
|
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
// spawns Chrome with a real, persistent window.
|
|
460
|
-
const lingering = registry.get(accountIdx);
|
|
461
|
-
if (lingering?.pid) {
|
|
462
|
-
closeChromeProcess(lingering.pid);
|
|
463
|
-
} else if (liveStatus.webSocketDebuggerUrl) {
|
|
465
|
+
if (liveAction === "restart-unmanaged") {
|
|
466
|
+
restoreLastSession = true;
|
|
464
467
|
try {
|
|
465
468
|
await callCdp({
|
|
466
469
|
debuggerPort: effectivePort,
|
|
@@ -469,8 +472,28 @@ async function launchOrActivateProfile({
|
|
|
469
472
|
params: {},
|
|
470
473
|
});
|
|
471
474
|
} catch (_) {}
|
|
475
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Windowless: tear the lingering process down (release its SingletonLock on
|
|
479
|
+
// the user-data-dir) before falling through to the fresh-launch path, which
|
|
480
|
+
// spawns Chrome with a real, persistent window.
|
|
481
|
+
if (liveAction === "restart-windowless") {
|
|
482
|
+
const lingering = registry.get(accountIdx);
|
|
483
|
+
if (lingering?.pid) {
|
|
484
|
+
closeChromeProcess(lingering.pid);
|
|
485
|
+
} else if (liveStatus.webSocketDebuggerUrl) {
|
|
486
|
+
try {
|
|
487
|
+
await callCdp({
|
|
488
|
+
debuggerPort: effectivePort,
|
|
489
|
+
target: liveStatus.webSocketDebuggerUrl,
|
|
490
|
+
method: "Browser.close",
|
|
491
|
+
params: {},
|
|
492
|
+
});
|
|
493
|
+
} catch (_) {}
|
|
494
|
+
}
|
|
495
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
472
496
|
}
|
|
473
|
-
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
474
497
|
}
|
|
475
498
|
|
|
476
499
|
ensureRpaProfileInitialized({
|
|
@@ -503,6 +526,7 @@ async function launchOrActivateProfile({
|
|
|
503
526
|
chromeBinary: config.chromeBinary,
|
|
504
527
|
url,
|
|
505
528
|
userDataDirRoot: effectiveUserDataDirRoot,
|
|
529
|
+
restoreLastSession,
|
|
506
530
|
});
|
|
507
531
|
|
|
508
532
|
const nextRuntime = registry.upsert(accountIdx, {
|
|
@@ -52,13 +52,11 @@ module.exports = (registerTool) => {
|
|
|
52
52
|
return { content: [{ type: "text", text: fresh.b64 }] };
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
// Stale/missing.
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
// mac/linux:进程内即时抓屏(honor snapshotEnabled — mac 默认关避免授权弹窗)。
|
|
61
|
-
if (process.platform !== "win32" && snap.snapshotEnabled()) {
|
|
55
|
+
// Stale/missing. A desktop_snapshot RPC is an explicit one-shot request,
|
|
56
|
+
// so mac/linux capture immediately even when the periodic daemon is off.
|
|
57
|
+
// CICY_DESKTOP_SNAPSHOT only controls the background capture loop in
|
|
58
|
+
// main.js; it must not make the user's “立即截图” button unusable.
|
|
59
|
+
if (process.platform !== "win32") {
|
|
62
60
|
try {
|
|
63
61
|
const r = await snap.captureB64(maxWidth);
|
|
64
62
|
return { content: [{ type: "text", text: r.b64 }] };
|
|
@@ -81,10 +79,6 @@ module.exports = (registerTool) => {
|
|
|
81
79
|
}
|
|
82
80
|
|
|
83
81
|
if (fresh) return { content: [{ type: "text", text: fresh.b64 }] }; // stale is better than nothing
|
|
84
|
-
// macOS 默认关(避免反复弹屏幕录制授权)。
|
|
85
|
-
if (process.platform === "darwin" && !snap.snapshotEnabled()) {
|
|
86
|
-
throw new Error("桌面截图在 macOS 默认关闭(避免反复弹屏幕录制授权)。需要 agent 看屏幕时,启动 app 前设环境变量 CICY_DESKTOP_SNAPSHOT=1。");
|
|
87
|
-
}
|
|
88
82
|
throw new Error("no desktop snapshot yet");
|
|
89
83
|
} catch (error) {
|
|
90
84
|
return { content: [{ type: "text", text: `Error: ${error.message}` }], isError: true };
|
|
@@ -27,13 +27,30 @@ const SHELL_HTML = path.join(__dirname, "..", "tabbrowser", "tab-shell.html");
|
|
|
27
27
|
const SHELL_PRELOAD = path.join(__dirname, "..", "tabbrowser", "tab-shell-preload.js");
|
|
28
28
|
const PANEL_PRELOAD = path.join(__dirname, "..", "tabbrowser", "panel-preload.js");
|
|
29
29
|
const panelCells = require("../tabbrowser/panel-cells");
|
|
30
|
+
const { normalizeCicyTheme, resolveTabChromeTheme } = require("../tabbrowser/tab-theme");
|
|
30
31
|
const HOMEPAGE_PRELOAD = path.join(__dirname, "..", "backends", "homepage-preload.js");
|
|
31
32
|
const WEBVIEW_PRELOAD = path.join(__dirname, "..", "backends", "webview-preload.js");
|
|
32
33
|
const CHROME_H = 80; // tab strip (40) + toolbar (40) — must match tab-shell.html
|
|
33
34
|
const STRIP_H = 40; // tab strip only; the homepage tab hides the toolbar (no address bar)
|
|
35
|
+
const TAB_CHROME_COLORS = {
|
|
36
|
+
dark: { background: "#1c1c20", symbols: "#e8eaed" },
|
|
37
|
+
light: { background: "#e4e4e7", symbols: "#27272a" },
|
|
38
|
+
};
|
|
34
39
|
|
|
35
|
-
|
|
36
|
-
|
|
40
|
+
// `r-reset` deliberately evicts tool modules from require.cache. BrowserWindows
|
|
41
|
+
// and BrowserViews outlive that reload, so their manager registry must outlive
|
|
42
|
+
// it too. A module-local Map made ensureManager(0) think no profile-0 window
|
|
43
|
+
// existed after a tool refresh; the next homepage "Open" click then created a
|
|
44
|
+
// second BrowserWindow instead of adding a tab to the current one.
|
|
45
|
+
const sharedState = globalThis.__cicyTabBrowserState || (globalThis.__cicyTabBrowserState = {
|
|
46
|
+
managers: new Map(),
|
|
47
|
+
managerByHost: new Map(),
|
|
48
|
+
openedWc: new Map(),
|
|
49
|
+
ipcInstalled: false,
|
|
50
|
+
themeIpcInstalled: false,
|
|
51
|
+
});
|
|
52
|
+
const managers = sharedState.managers; // accountIdx -> TabManager
|
|
53
|
+
const managerByHost = sharedState.managerByHost; // shell webContents.id -> TabManager
|
|
37
54
|
|
|
38
55
|
function stripVol(u) { try { const x = new URL(u); return x.origin + x.pathname; } catch (e) { return u || ""; } }
|
|
39
56
|
|
|
@@ -68,7 +85,7 @@ function isInstanceAuthNavigation(currentUrl, targetUrl) {
|
|
|
68
85
|
// (打开/关闭都更新)。刷新窗口据此按 wcId 强制 reload(reloadIgnoringCache),避开
|
|
69
86
|
// "tab 打开后 URL 漂移(登录跳转 / token / 重定向)→ 按 URL 匹配失败"的 bug。
|
|
70
87
|
// 键 = stripVol(打开时传入的 url);open 与 reload 两端 stripVol 一致(token 被剥掉)。
|
|
71
|
-
const openedWc =
|
|
88
|
+
const openedWc = sharedState.openedWc; // stripVol(url) -> webContentsId
|
|
72
89
|
|
|
73
90
|
const { NEWTAB_URL, PANEL_URL_BASE, ensureForPartition } = require("../tabbrowser/newtab-protocol");
|
|
74
91
|
const PANEL_STATE_PATH = path.join(os.homedir(), "cicy-ai", "db", "last-panel.json");
|
|
@@ -206,7 +223,7 @@ class TabManager {
|
|
|
206
223
|
this.activeId = null;
|
|
207
224
|
// profile 0 is the primary window (its first tab is the resident homepage),
|
|
208
225
|
// so give it the app title/icon; other profiles are sandbox browsers.
|
|
209
|
-
const STRIP_BG =
|
|
226
|
+
const STRIP_BG = TAB_CHROME_COLORS.dark.background; // matches tab-shell.html --strip
|
|
210
227
|
const winOpts = {
|
|
211
228
|
width: 1180,
|
|
212
229
|
height: 820,
|
|
@@ -285,21 +302,25 @@ class TabManager {
|
|
|
285
302
|
const active = this.tabs.find((t) => t.id === this.activeId);
|
|
286
303
|
let wc = null;
|
|
287
304
|
try { wc = active ? active.view.webContents : null; } catch (e) {}
|
|
305
|
+
const tabs = this.tabs.map((t) => ({
|
|
306
|
+
id: t.id,
|
|
307
|
+
// fixedTitle (team title / homepage) wins over the page's own document.title
|
|
308
|
+
title: t.fixedTitle || t.title || "新标签页",
|
|
309
|
+
url: t.url || "",
|
|
310
|
+
active: t.id === this.activeId,
|
|
311
|
+
loading: !!t.loading,
|
|
312
|
+
favicon: t.favicon || "",
|
|
313
|
+
avatar: t.avatar || "", // team 自定义头像 → tab icon 用它(优先于页面 favicon)
|
|
314
|
+
team: !!t.team, // 团队 tab:icon 只用 avatar,禁用页面 favicon
|
|
315
|
+
colorKey: t.colorKey || "", // 无头像时首字母色块的底色种子 = teamId(和卡片一致)
|
|
316
|
+
home: !!t.home,
|
|
317
|
+
panel: typeof t.url === "string" && t.url.startsWith(PANEL_URL_BASE),
|
|
318
|
+
cicyTheme: t.cicyTheme || "",
|
|
319
|
+
}));
|
|
320
|
+
const chromeTheme = resolveTabChromeTheme(tabs);
|
|
288
321
|
const s = {
|
|
289
|
-
tabs
|
|
290
|
-
|
|
291
|
-
// fixedTitle (team title / homepage) wins over the page's own document.title
|
|
292
|
-
title: t.fixedTitle || t.title || "新标签页",
|
|
293
|
-
url: t.url || "",
|
|
294
|
-
active: t.id === this.activeId,
|
|
295
|
-
loading: !!t.loading,
|
|
296
|
-
favicon: t.favicon || "",
|
|
297
|
-
avatar: t.avatar || "", // team 自定义头像 → tab icon 用它(优先于页面 favicon)
|
|
298
|
-
team: !!t.team, // 团队 tab:icon 只用 avatar,禁用页面 favicon
|
|
299
|
-
colorKey: t.colorKey || "", // 无头像时首字母色块的底色种子 = teamId(和卡片一致)
|
|
300
|
-
home: !!t.home,
|
|
301
|
-
panel: typeof t.url === "string" && t.url.startsWith(PANEL_URL_BASE),
|
|
302
|
-
})),
|
|
322
|
+
tabs,
|
|
323
|
+
theme: chromeTheme,
|
|
303
324
|
nav: {
|
|
304
325
|
canBack: wc ? wc.canGoBack() : false,
|
|
305
326
|
canFwd: wc ? wc.canGoForward() : false,
|
|
@@ -307,6 +328,20 @@ class TabManager {
|
|
|
307
328
|
url: active ? active.url || "" : "",
|
|
308
329
|
},
|
|
309
330
|
};
|
|
331
|
+
if (this.chromeTheme !== chromeTheme) {
|
|
332
|
+
this.chromeTheme = chromeTheme;
|
|
333
|
+
const colors = TAB_CHROME_COLORS[chromeTheme];
|
|
334
|
+
try { this.win.setBackgroundColor(colors.background); } catch (e) {}
|
|
335
|
+
if (process.platform === "win32") {
|
|
336
|
+
try {
|
|
337
|
+
this.win.setTitleBarOverlay({
|
|
338
|
+
color: colors.background,
|
|
339
|
+
symbolColor: colors.symbols,
|
|
340
|
+
height: STRIP_H,
|
|
341
|
+
});
|
|
342
|
+
} catch (e) {}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
310
345
|
try { this.win.webContents.send("tabwin:state", s); } catch (e) {}
|
|
311
346
|
}
|
|
312
347
|
|
|
@@ -387,7 +422,7 @@ class TabManager {
|
|
|
387
422
|
// home = the resident homepage tab (pinned, first, user-icon, no close).
|
|
388
423
|
// fixedTitle = a caller-supplied tab name (e.g. the team title) that the
|
|
389
424
|
// page's own document.title must NOT override.
|
|
390
|
-
const tab = { id, view, title: "", url: target, home: !!opts.home, fixedTitle: opts.title || "", avatar: opts.avatar || "", team: !!opts.team, colorKey: opts.colorKey || "" };
|
|
425
|
+
const tab = { id, view, title: "", url: target, home: !!opts.home, fixedTitle: opts.title || "", avatar: opts.avatar || "", team: !!opts.team, colorKey: opts.colorKey || "", cicyTheme: "" };
|
|
391
426
|
// team tab 用团队自定义头像做 icon:调用方没给就按 URL 反查 teams.json(覆盖所有打开路径)。
|
|
392
427
|
if (!tab.avatar && !opts.home) { try { tab.avatar = require("../backends/local-teams").avatarForUrl(target) || ""; } catch (e) {} }
|
|
393
428
|
if (opts.home) this.tabs.unshift(tab); else this.tabs.push(tab);
|
|
@@ -651,10 +686,9 @@ function openHomeWindow(accountIdx, homeUrl, opts = {}) {
|
|
|
651
686
|
}
|
|
652
687
|
|
|
653
688
|
// ── shell IPC (registered once) ──────────────────────────────────────────────
|
|
654
|
-
let ipcInstalled = false;
|
|
655
689
|
function installIpc() {
|
|
656
|
-
if (ipcInstalled) return;
|
|
657
|
-
ipcInstalled = true;
|
|
690
|
+
if (sharedState.ipcInstalled) return;
|
|
691
|
+
sharedState.ipcInstalled = true;
|
|
658
692
|
const mgr = (e) => managerByHost.get(e.sender.id);
|
|
659
693
|
ipcMain.on("tabwin:ready", (e) => {
|
|
660
694
|
const m = mgr(e);
|
|
@@ -693,7 +727,25 @@ function installIpc() {
|
|
|
693
727
|
ipcMain.on("tabwin:fwd", (e) => { const m = mgr(e); const wc = m && m.activeWc(); if (wc && wc.canGoForward()) wc.goForward(); });
|
|
694
728
|
ipcMain.on("tabwin:reload", (e) => { const m = mgr(e); const wc = m && m.activeWc(); if (wc) wc.reload(); });
|
|
695
729
|
}
|
|
730
|
+
|
|
731
|
+
function installThemeIpc() {
|
|
732
|
+
if (sharedState.themeIpcInstalled) return;
|
|
733
|
+
sharedState.themeIpcInstalled = true;
|
|
734
|
+
ipcMain.on("tab-content:cicy-theme", (e, payload) => {
|
|
735
|
+
const theme = normalizeCicyTheme(payload && payload.theme);
|
|
736
|
+
if (!theme) return;
|
|
737
|
+
const m = findManagerByTab(e.sender.id);
|
|
738
|
+
if (!m) return;
|
|
739
|
+
const tab = m.tabs.find((candidate) => candidate.id === e.sender.id);
|
|
740
|
+
// Only cicy-code/team tabs own the Desktop chrome theme. A normal page can
|
|
741
|
+
// carry the guarded preload, but it must never be able to repaint the shell.
|
|
742
|
+
if (!tab || !tab.team || tab.cicyTheme === theme) return;
|
|
743
|
+
tab.cicyTheme = theme;
|
|
744
|
+
m.pushState();
|
|
745
|
+
});
|
|
746
|
+
}
|
|
696
747
|
installIpc();
|
|
748
|
+
installThemeIpc();
|
|
697
749
|
|
|
698
750
|
// CDP captureScreenshot — works for background tabs (capturePage blanks when the
|
|
699
751
|
// BrowserView isn't the attached/visible one).
|
|
@@ -37,14 +37,15 @@ function snapDir() {
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
// Is desktop screen capture allowed here? Single source of truth shared by the
|
|
40
|
-
// periodic daemon (main.js)
|
|
40
|
+
// periodic daemon (main.js). One-shot `desktop_snapshot` RPC requests bypass this
|
|
41
|
+
// switch: clicking “立即截图” must work without enabling background capture.
|
|
41
42
|
// OFF by default on macOS AND Windows (opt in with CICY_DESKTOP_SNAPSHOT=1):
|
|
42
43
|
// - macOS: any capture trips the Screen-Recording prompt that won't persist for a
|
|
43
44
|
// non-Apple-Team-ID signature (re-prompts forever).
|
|
44
45
|
// - Windows: the periodic whole-desktop capture spawns an always-on --disable-gpu
|
|
45
46
|
// child + writes every 30s — pure overhead for users who only manage docker/teams
|
|
46
47
|
// and aren't being driven by a screen-watching cloud agent (资源占用). The on-demand
|
|
47
|
-
// `desktop_snapshot`
|
|
48
|
+
// `desktop_snapshot` still captures once when explicitly requested.
|
|
48
49
|
// linux stays ON by default (no prompt, cheap scrot); opt out with =0.
|
|
49
50
|
function snapshotEnabled() {
|
|
50
51
|
return (process.platform === "darwin" || process.platform === "win32")
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Copyright 2026 CiCy AI
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
const assert = require("node:assert/strict");
|
|
5
|
+
const childProcess = require("node:child_process");
|
|
6
|
+
const net = require("node:net");
|
|
7
|
+
const test = require("node:test");
|
|
8
|
+
|
|
9
|
+
test("macOS restarts a live Chrome profile not launched by the current desktop runtime", () => {
|
|
10
|
+
const { resolveLiveProfileAction } = require("../src/chrome/chrome-launcher");
|
|
11
|
+
|
|
12
|
+
assert.equal(
|
|
13
|
+
resolveLiveProfileAction({
|
|
14
|
+
platform: "darwin",
|
|
15
|
+
liveStatus: { isRunning: true },
|
|
16
|
+
runtime: null,
|
|
17
|
+
windowless: false,
|
|
18
|
+
}),
|
|
19
|
+
"restart-unmanaged"
|
|
20
|
+
);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("an unmanaged Chrome restart restores the previous browser session", () => {
|
|
24
|
+
const { buildChromeArgs } = require("../src/chrome/chrome-launcher");
|
|
25
|
+
const args = buildChromeArgs({
|
|
26
|
+
userDataDirRoot: "/tmp/cicy-profile-1",
|
|
27
|
+
profileDirectory: "Default",
|
|
28
|
+
debuggerPort: 11001,
|
|
29
|
+
restoreLastSession: true,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
assert.ok(args.includes("--restore-last-session"));
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("macOS activates a reused Chrome profile by the debugger listener PID", async (t) => {
|
|
36
|
+
if (process.platform !== "darwin") {
|
|
37
|
+
t.skip("macOS-specific foreground behavior");
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const server = net.createServer();
|
|
42
|
+
await new Promise((resolve, reject) => {
|
|
43
|
+
server.once("error", reject);
|
|
44
|
+
server.listen(0, "127.0.0.1", resolve);
|
|
45
|
+
});
|
|
46
|
+
t.after(() => new Promise((resolve) => server.close(resolve)));
|
|
47
|
+
|
|
48
|
+
const calls = [];
|
|
49
|
+
const originalSpawn = childProcess.spawn;
|
|
50
|
+
childProcess.spawn = (command, args, options) => {
|
|
51
|
+
calls.push({ command, args, options });
|
|
52
|
+
return { unref() {} };
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const launcherPath = require.resolve("../src/chrome/chrome-launcher");
|
|
56
|
+
delete require.cache[launcherPath];
|
|
57
|
+
const { bringChromeAppToForeground } = require(launcherPath);
|
|
58
|
+
childProcess.spawn = originalSpawn;
|
|
59
|
+
|
|
60
|
+
bringChromeAppToForeground("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", {
|
|
61
|
+
debuggerPort: server.address().port,
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
assert.equal(calls.length, 1);
|
|
65
|
+
assert.equal(calls[0].command, "/usr/bin/osascript");
|
|
66
|
+
assert.deepEqual(calls[0].args.slice(0, 3), ["-l", "JavaScript", "-e"]);
|
|
67
|
+
assert.match(
|
|
68
|
+
calls[0].args[3],
|
|
69
|
+
new RegExp(`runningApplicationWithProcessIdentifier\\(${process.pid}\\)`)
|
|
70
|
+
);
|
|
71
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// Copyright 2026 CiCy AI
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
const assert = require("node:assert/strict");
|
|
5
|
+
const test = require("node:test");
|
|
6
|
+
|
|
7
|
+
let resolveReportedCicyTheme;
|
|
8
|
+
let resolveTabChromeTheme;
|
|
9
|
+
try {
|
|
10
|
+
({ resolveReportedCicyTheme, resolveTabChromeTheme } = require("../src/tabbrowser/tab-theme"));
|
|
11
|
+
} catch (_) {}
|
|
12
|
+
|
|
13
|
+
test("the active cicy-code team tab controls the desktop tab chrome theme", () => {
|
|
14
|
+
assert.equal(typeof resolveTabChromeTheme, "function");
|
|
15
|
+
assert.equal(
|
|
16
|
+
resolveTabChromeTheme([
|
|
17
|
+
{ id: 1, active: false, team: true, cicyTheme: "dark" },
|
|
18
|
+
{ id: 2, active: true, team: true, cicyTheme: "light" },
|
|
19
|
+
]),
|
|
20
|
+
"light"
|
|
21
|
+
);
|
|
22
|
+
assert.equal(
|
|
23
|
+
resolveTabChromeTheme([{ id: 2, active: true, team: true, cicyTheme: "dark" }]),
|
|
24
|
+
"dark"
|
|
25
|
+
);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("inactive cicy-code tabs and ordinary pages cannot recolor the desktop chrome", () => {
|
|
29
|
+
assert.equal(
|
|
30
|
+
resolveTabChromeTheme([
|
|
31
|
+
{ id: 1, active: false, team: true, cicyTheme: "light" },
|
|
32
|
+
{ id: 2, active: true, team: false, cicyTheme: "light" },
|
|
33
|
+
]),
|
|
34
|
+
"dark"
|
|
35
|
+
);
|
|
36
|
+
assert.equal(resolveTabChromeTheme([]), "dark");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("cicy-code reports its document theme, then its saved preference, then light by default", () => {
|
|
40
|
+
assert.equal(typeof resolveReportedCicyTheme, "function");
|
|
41
|
+
assert.equal(resolveReportedCicyTheme("light", "dark"), "light");
|
|
42
|
+
assert.equal(resolveReportedCicyTheme("", "dark"), "dark");
|
|
43
|
+
assert.equal(resolveReportedCicyTheme("system", "sepia"), "light");
|
|
44
|
+
});
|