cicy-desktop 2.1.78 → 2.1.80

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.
Files changed (54) hide show
  1. package/bin/cicy-desktop +7 -7
  2. package/package.json +6 -6
  3. package/src/backends/homepage-preload.js +22 -0
  4. package/src/backends/homepage-react/assets/index-CKpaMBKz.css +1 -0
  5. package/src/backends/homepage-react/assets/index-CSsNZgC5.js +365 -0
  6. package/src/backends/homepage-react/index.html +2 -2
  7. package/src/backends/homepage-window.js +52 -7
  8. package/src/backends/ipc.js +57 -0
  9. package/src/backends/local-teams.js +73 -26
  10. package/src/backends/sidecar-ipc.js +11 -0
  11. package/src/backends/webview-preload.js +5 -3
  12. package/src/backends/window-manager.js +13 -3
  13. package/src/chrome/chrome-launcher.js +5 -4
  14. package/src/chrome/debugger-port-resolver.js +1 -1
  15. package/src/cloud/cloud-client.js +237 -41
  16. package/src/cluster/types.js +0 -5
  17. package/src/extension/inject.js +1 -1
  18. package/src/main.js +282 -88
  19. package/src/master/chrome-config.js +2 -2
  20. package/src/preload-rpc.js +1 -1
  21. package/src/profiles/profile-store.js +321 -0
  22. package/src/profiles/trusted-origins-store.js +95 -0
  23. package/src/server/worker-observability-routes.js +0 -2
  24. package/src/sidecar/cicy-code.js +84 -23
  25. package/src/sidecar/localbin.js +20 -3
  26. package/src/sidecar/native.js +3 -3
  27. package/src/sidecar/version.js +45 -0
  28. package/src/tabbrowser/newtab-protocol.js +54 -0
  29. package/src/tabbrowser/tab-browser.html +151 -0
  30. package/src/tabbrowser/tab-shell-preload.js +28 -0
  31. package/src/tabbrowser/tab-shell.html +227 -0
  32. package/src/tools/account-tools.js +191 -25
  33. package/src/tools/chrome-tools.js +173 -37
  34. package/src/tools/device-tools.js +25 -0
  35. package/src/tools/index.js +2 -0
  36. package/src/tools/tab-browser-tools.js +453 -0
  37. package/src/tools/window-tools.js +64 -7
  38. package/src/utils/brand-host-electron.js +25 -0
  39. package/src/utils/context-menu-options.js +80 -0
  40. package/src/utils/cookie-logins.js +58 -0
  41. package/src/utils/ip-probe.js +50 -0
  42. package/src/utils/rpc-audit.js +53 -0
  43. package/src/utils/rpc-guard.js +189 -0
  44. package/src/utils/window-monitor.js +5 -15
  45. package/src/utils/window-registry.js +210 -0
  46. package/src/utils/window-thumbnails.js +126 -0
  47. package/src/utils/window-utils.js +146 -109
  48. package/workers/render/package-lock.json +6 -6
  49. package/workers/render/src/App.css +36 -2
  50. package/workers/render/src/App.jsx +587 -103
  51. package/src/backends/artifact-ipc.js +0 -142
  52. package/src/backends/homepage-react/assets/index-DE9m6JTn.css +0 -1
  53. package/src/backends/homepage-react/assets/index-DLYMzgf5.js +0 -365
  54. package/src/cluster/artifact-registry.js +0 -61
@@ -0,0 +1,45 @@
1
+ // cicy-code 版本的唯一读法。三个概念一个模块,别处不准再自己解析 /api/health、
2
+ // 也不准再直连 npm 拿版本(主人令:"拿版本就一个方法")。
3
+ // running(port) → 活着的 daemon 在 /api/health 报的版本(“正在跑什么”的唯一真相)
4
+ // latest() → npm 上最新版(和 update() 升级到的同一个号)
5
+ // installed() → 磁盘 binary 版本(localbin manifest,诊断用)
6
+ const http = require("http");
7
+
8
+ const DEFAULT_PORT = 8008;
9
+
10
+ // The ONE running-version reader. GET /api/health → version. Returns the version
11
+ // string, or null on any failure / missing field. Used by the update flow's
12
+ // post-restart verification AND surfaced to the UI via the sidecar:versions IPC.
13
+ //
14
+ // timeout=4000 (not 1500): on Windows Node's http.get to 127.0.0.1:8008 routinely
15
+ // takes 1.5–5s (localhost goes through 360/Defender socket inspection) even though
16
+ // curl is instant — a 1500ms cap returned null while the daemon was fine. Connection:
17
+ // close makes the Go server end the response promptly instead of holding keep-alive.
18
+ function running(port = DEFAULT_PORT, timeoutMs = 4000) {
19
+ return new Promise((resolve) => {
20
+ const req = http.get({ host: "127.0.0.1", port, path: "/api/health", timeout: timeoutMs, headers: { Connection: "close" } }, (res) => {
21
+ let body = "";
22
+ res.on("data", (d) => { body += d; if (body.length > 8192) body = body.slice(0, 8192); });
23
+ res.on("end", () => { resolve(parseHealthVersion(body)); });
24
+ });
25
+ req.on("error", () => resolve(null));
26
+ req.on("timeout", () => { req.destroy(); resolve(null); });
27
+ });
28
+ }
29
+
30
+ // The ONE way to pull the version out of a /api/health body — shared so every
31
+ // caller (this module + local-teams' liveness probe) parses identically.
32
+ function parseHealthVersion(body) {
33
+ try { const j = JSON.parse(body); return String(j?.version || j?.data?.version || "").trim() || null; }
34
+ catch { return null; }
35
+ }
36
+
37
+ async function latest() {
38
+ try { return await require("./localbin").latestVersion(); } catch { return null; }
39
+ }
40
+
41
+ function installed() {
42
+ try { return require("./localbin").currentVersion(); } catch { return null; }
43
+ }
44
+
45
+ module.exports = { running, latest, installed, parseHealthVersion, DEFAULT_PORT };
@@ -0,0 +1,54 @@
1
+ // cicy://newtab — the tab browser's start page, served via a custom scheme so a
2
+ // new tab's URL is a clean "cicy://newtab" instead of a giant inline data: URL
3
+ // (主人令: url 不要那串 data: 天书). Chrome's chrome://newtab analog.
4
+ const { protocol, session } = require("electron");
5
+ const _handled = new WeakSet(); // sessions that already have the cicyui handler
6
+
7
+ // NOTE: scheme is "cicyui", NOT "cicy" — "cicy" is already an OS deep-link
8
+ // protocol client (setAsDefaultProtocolClient), so navigating a webContents to
9
+ // cicy://… gets dispatched externally (open-url) and the page never renders.
10
+ // A dedicated scheme avoids that collision.
11
+ const NEWTAB_URL = "cicyui://newtab";
12
+
13
+ function startPageHtml() {
14
+ // <title> → document.title = 起始页 (so the tab isn't titled by its URL).
15
+ return `<!doctype html><meta charset=utf-8><title>起始页</title><style>html,body{height:100%;margin:0;display:flex;align-items:center;justify-content:center;background:#202124;color:#e8eaed;font-family:-apple-system,sans-serif}.w{text-align:center}.l{width:54px;height:54px;border-radius:16px;margin:0 auto 16px;background:linear-gradient(135deg,#3b82f6,#8b5cf6);display:flex;align-items:center;justify-content:center;color:#fff;font-size:26px}h1{font-size:16px;margin-bottom:6px}p{color:#9aa0a6;font-size:13px}</style><div class=w><div class=l>&#10022;</div><h1>CiCy Browser</h1><p>新标签页</p></div>`;
16
+ }
17
+
18
+ // MUST be called BEFORE app 'ready' (registerSchemesAsPrivileged requirement).
19
+ function registerScheme() {
20
+ try {
21
+ protocol.registerSchemesAsPrivileged([
22
+ { scheme: "cicyui", privileges: { standard: true, secure: true, supportFetchAPI: true } },
23
+ ]);
24
+ } catch (e) {}
25
+ }
26
+
27
+ // Register the cicyui handler on a given session. protocol.handle on the global
28
+ // (default) session does NOT cover persist:sandbox-N partition sessions — the
29
+ // tab BrowserViews run in those — so each partition session needs its own.
30
+ function handlerFor(ses) {
31
+ if (!ses || _handled.has(ses)) return;
32
+ try {
33
+ ses.protocol.handle("cicyui", (request) => {
34
+ let host = "";
35
+ try { host = new URL(request.url).hostname; } catch (e) {}
36
+ if (host === "newtab") {
37
+ return new Response(startPageHtml(), { headers: { "content-type": "text/html; charset=utf-8" } });
38
+ }
39
+ return new Response("not found", { status: 404 });
40
+ });
41
+ _handled.add(ses);
42
+ } catch (e) {}
43
+ }
44
+
45
+ // MUST be called AFTER app 'ready' — default session (homepage etc.).
46
+ function installHandler() { handlerFor(session.defaultSession); }
47
+
48
+ // Per-partition session for the tab-browser sandboxes — call before a tab in
49
+ // that partition loads cicyui://newtab.
50
+ function ensureForPartition(partition) {
51
+ try { handlerFor(session.fromPartition(partition)); } catch (e) {}
52
+ }
53
+
54
+ module.exports = { NEWTAB_URL, registerScheme, installHandler, ensureForPartition, startPageHtml };
@@ -0,0 +1,151 @@
1
+ <!doctype html>
2
+ <html lang="zh">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>CiCy Browser</title>
6
+ <style>
7
+ *{box-sizing:border-box;margin:0;padding:0}
8
+ html,body{height:100%;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif}
9
+ body{display:flex;flex-direction:column;background:#202124;color:#e8eaed}
10
+ #tabs{display:flex;align-items:center;gap:2px;padding:6px 6px 0;overflow-x:auto;flex-wrap:nowrap}
11
+ #tabs::-webkit-scrollbar{height:0}
12
+ .tab{display:flex;align-items:center;gap:6px;max-width:220px;min-width:110px;padding:8px 10px;background:#35363a;border-radius:9px 9px 0 0;font-size:12px;white-space:nowrap;cursor:default}
13
+ .tab.active{background:#5f6368}
14
+ .tab .t{flex:1;overflow:hidden;text-overflow:ellipsis}
15
+ .tab .x{opacity:.55;cursor:pointer;padding:0 3px;border-radius:3px}
16
+ .tab .x:hover{background:#0004;opacity:1}
17
+ #newtab{padding:4px 11px;cursor:pointer;font-size:18px;color:#9aa0a6}
18
+ #newtab:hover{color:#fff}
19
+ #bar{display:flex;align-items:center;gap:8px;padding:8px 10px;background:#35363a}
20
+ #bar button{background:none;border:none;color:#9aa0a6;font-size:17px;cursor:pointer;padding:4px 9px;border-radius:6px}
21
+ #bar button:hover{background:#ffffff18;color:#fff}
22
+ #url{flex:1;background:#202124;border:1px solid #5f6368;border-radius:18px;padding:8px 14px;color:#e8eaed;font-size:13px;outline:none}
23
+ #url:focus{border-color:#8ab4f8}
24
+ #views{flex:1;position:relative;background:#fff}
25
+ webview{position:absolute;inset:0;width:100%;height:100%}
26
+ webview.hidden{display:none}
27
+ </style>
28
+ </head>
29
+ <body>
30
+ <div id="tabs"></div>
31
+ <div id="bar">
32
+ <button id="back" title="后退">&#8249;</button>
33
+ <button id="fwd" title="前进">&#8250;</button>
34
+ <button id="reload" title="刷新">&#8635;</button>
35
+ <input id="url" placeholder="输入网址或搜索">
36
+ </div>
37
+ <div id="views"></div>
38
+ <script>
39
+ // partition (profile) comes from the query: ?p=<accountIdx>
40
+ const Q = new URLSearchParams(location.search);
41
+ const ACCT = Q.get('p') || '0';
42
+ const PARTITION = 'persist:sandbox-' + ACCT;
43
+ // cicy's webview preload → trusted tabs (cicy-code team app) get
44
+ // window.electronRPC + cicy bridges so they keep working as a tab.
45
+ const PRELOAD = new URL('../backends/webview-preload.js', location.href).href;
46
+ // homepage uses its OWN preload (window.cicy.localTeams/auth/...). When ?home=
47
+ // is set, the first tab loads the homepage with that preload.
48
+ const HOMEPAGE_PRELOAD = new URL('../backends/homepage-preload.js', location.href).href;
49
+ const HOME = Q.get('home') || '';
50
+ // Built-in start page (a real page, not about:blank) for new tabs.
51
+ const START = 'data:text/html;charset=utf-8,' + encodeURIComponent(
52
+ `<!doctype html><meta charset=utf-8><style>html,body{height:100%;margin:0;display:flex;align-items:center;justify-content:center;background:#202124;color:#e8eaed;font-family:-apple-system,sans-serif}.w{text-align:center}.l{width:54px;height:54px;border-radius:16px;margin:0 auto 16px;background:linear-gradient(135deg,#3b82f6,#8b5cf6);display:flex;align-items:center;justify-content:center;color:#fff;font-size:26px}h1{font-size:16px;margin-bottom:6px}p{color:#9aa0a6;font-size:13px}</style><div class=w><div class=l>&#10022;</div><h1>CiCy Browser</h1><p>profile sandbox-${ACCT}</p></div>`);
53
+
54
+ const tabsEl = document.getElementById('tabs');
55
+ const viewsEl = document.getElementById('views');
56
+ const urlEl = document.getElementById('url');
57
+ let tabs = []; // { wv, title, url }
58
+ let active = -1;
59
+
60
+ function render() {
61
+ tabsEl.innerHTML = '';
62
+ tabs.forEach((t, i) => {
63
+ const d = document.createElement('div');
64
+ d.className = 'tab' + (i === active ? ' active' : '');
65
+ d.innerHTML = '<span class="t"></span><span class="x">&#10005;</span>';
66
+ d.querySelector('.t').textContent = t.title || '新标签页';
67
+ d.title = t.url || '';
68
+ d.onclick = () => select(i);
69
+ d.querySelector('.x').onclick = (e) => { e.stopPropagation(); closeAt(i); };
70
+ tabsEl.appendChild(d);
71
+ });
72
+ const add = document.createElement('div');
73
+ add.id = 'newtab'; add.textContent = '+';
74
+ add.onclick = () => newTab();
75
+ tabsEl.appendChild(add);
76
+ }
77
+
78
+ function select(i) {
79
+ active = i;
80
+ tabs.forEach((t, j) => t.wv.classList.toggle('hidden', j !== i));
81
+ const t = tabs[i];
82
+ if (t) { try { urlEl.value = t.wv.getURL() || t.url || ''; } catch (e) { urlEl.value = t.url || ''; } }
83
+ render();
84
+ }
85
+
86
+ function closeAt(i) {
87
+ const t = tabs[i]; if (!t) return;
88
+ t.wv.remove(); tabs.splice(i, 1);
89
+ if (tabs.length === 0) { newTab(); return; }
90
+ if (active >= tabs.length) active = tabs.length - 1;
91
+ select(active);
92
+ }
93
+
94
+ // Public: open a new tab. opts.trusted=true → inject the electronRPC bridge
95
+ // (ONLY for trusted pages like the cicy-code team app — never random sites).
96
+ window.newTab = function (u, opts) {
97
+ u = (u && String(u).trim()) || START;
98
+ const wv = document.createElement('webview');
99
+ wv.setAttribute('partition', PARTITION);
100
+ wv.setAttribute('allowpopups', '');
101
+ if (opts && opts.trusted) { try { wv.setAttribute('preload', PRELOAD); } catch (e) {} }
102
+ wv.setAttribute('src', u);
103
+ viewsEl.appendChild(wv);
104
+ const t = { wv, title: '', url: u };
105
+ tabs.push(t);
106
+ wv.addEventListener('page-title-updated', (e) => { t.title = e.title; render(); });
107
+ wv.addEventListener('did-navigate', (e) => { t.url = e.url; if (tabs[active] === t) urlEl.value = e.url; });
108
+ wv.addEventListener('did-navigate-in-page', (e) => { if (tabs[active] === t) urlEl.value = e.url; });
109
+ // popups / window.open → open as a new tab instead of a new window
110
+ wv.addEventListener('new-window', (e) => { try { newTab(e.url); } catch (x) {} });
111
+ select(tabs.length - 1);
112
+ };
113
+
114
+ // Public: close the tab whose webview has this webContentsId (used by main).
115
+ window.closeTabByWcId = function (id) {
116
+ const i = tabs.findIndex((t) => { try { return t.wv.getWebContentsId() === id; } catch (e) { return false; } });
117
+ if (i >= 0) closeAt(i);
118
+ return i >= 0;
119
+ };
120
+ // Public: activate (foreground) a tab by webContentsId.
121
+ window.activateTabByWcId = function (id) {
122
+ const i = tabs.findIndex((t) => { try { return t.wv.getWebContentsId() === id; } catch (e) { return false; } });
123
+ if (i >= 0) select(i);
124
+ return i >= 0;
125
+ };
126
+ // origin+pathname only (token/hash vary per session) — for tab reuse.
127
+ function stripVol(u) { try { const x = new URL(u); return x.origin + x.pathname; } catch (e) { return u || ''; } }
128
+ // Public: focus an existing tab whose url matches (origin+pathname); else false.
129
+ window.activateTabByUrl = function (key) {
130
+ const i = tabs.findIndex((t) => { try { return stripVol(t.wv.getURL() || t.url) === key; } catch (e) { return false; } });
131
+ if (i >= 0) { select(i); return true; }
132
+ return false;
133
+ };
134
+
135
+ function go() {
136
+ let v = urlEl.value.trim(); if (!v) return;
137
+ if (!/^[a-z]+:\/\//i.test(v)) {
138
+ v = (/\.\w/.test(v) && !/\s/.test(v)) ? 'https://' + v : 'https://www.google.com/search?q=' + encodeURIComponent(v);
139
+ }
140
+ if (tabs[active]) tabs[active].wv.loadURL(v);
141
+ }
142
+ urlEl.addEventListener('keydown', (e) => { if (e.key === 'Enter') go(); });
143
+ document.getElementById('back').onclick = () => { const w = tabs[active] && tabs[active].wv; if (w && w.canGoBack()) w.goBack(); };
144
+ document.getElementById('fwd').onclick = () => { const w = tabs[active] && tabs[active].wv; if (w && w.canGoForward()) w.goForward(); };
145
+ document.getElementById('reload').onclick = () => { tabs[active] && tabs[active].wv.reload(); };
146
+
147
+ // first tab = start page
148
+ newTab();
149
+ </script>
150
+ </body>
151
+ </html>
@@ -0,0 +1,28 @@
1
+ // Preload for the BrowserView tab-browser shell (tab strip + toolbar).
2
+ // Exposes window.tabAPI so the chrome UI drives the main-process TabManager,
3
+ // and receives tab state pushes. Tabs themselves are BrowserViews (managed in
4
+ // main) — this preload is ONLY for the thin chrome UI, never the tab content.
5
+ const { contextBridge, ipcRenderer } = require("electron");
6
+
7
+ contextBridge.exposeInMainWorld("tabAPI", {
8
+ newTab: (url) => ipcRenderer.send("tabwin:new", { url: url || "" }),
9
+ activate: (id) => ipcRenderer.send("tabwin:activate", { id }),
10
+ close: (id) => ipcRenderer.send("tabwin:close", { id }),
11
+ navigate: (url) => ipcRenderer.send("tabwin:navigate", { url }),
12
+ back: () => ipcRenderer.send("tabwin:back"),
13
+ fwd: () => ipcRenderer.send("tabwin:fwd"),
14
+ reload: () => ipcRenderer.send("tabwin:reload"),
15
+ ready: () => ipcRenderer.send("tabwin:ready"),
16
+ onState: (cb) => {
17
+ const h = (_e, s) => { try { cb(s); } catch (e) {} };
18
+ ipcRenderer.on("tabwin:state", h);
19
+ return () => ipcRenderer.removeListener("tabwin:state", h);
20
+ },
21
+ // mac native fullscreen toggles the traffic lights → the strip reclaims the
22
+ // reserved left gutter. cb(isFullScreen:boolean).
23
+ onFullscreen: (cb) => {
24
+ const h = (_e, fs) => { try { cb(!!fs); } catch (e) {} };
25
+ ipcRenderer.on("window:fullscreen", h);
26
+ return () => ipcRenderer.removeListener("window:fullscreen", h);
27
+ },
28
+ });
@@ -0,0 +1,227 @@
1
+ <!doctype html>
2
+ <html lang="zh">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>CiCy Browser</title>
6
+ <style>
7
+ /* Chrome MUST total CHROME_H (80) in tab-browser-tools.js: strip 40 + bar 40. */
8
+ :root{
9
+ /* match cicy-code's #0A0A0A main bg so the tab strip blends into the
10
+ content below — no color seam (断层). */
11
+ /* The top tab-strip is a clearly different (lighter) bar; the ACTIVE tab is
12
+ the same color as the toolbar/content (#0A0A0A) so it merges into the
13
+ page, carved out of the lighter strip with outward bottom curves.
14
+ (content is near-black, so a darker strip would be invisible → go lighter.) */
15
+ --bg:#0A0A0A; --strip:#1c1c20; --toolbar:#0A0A0A;
16
+ --fg:#e6e6e9; --muted:#8b8b92; --line:rgba(255,255,255,.06);
17
+ --accent:#3b82f6; --omni:#1a1a1c;
18
+ }
19
+ *{box-sizing:border-box;margin:0;padding:0}
20
+ 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}
21
+ svg{display:block}
22
+
23
+ /* ── tab strip ── */
24
+ /* Chrome Refresh tabs: floating rounded pills, gaps between; active = a filled
25
+ lighter surface (NO bottom border — Chrome doesn't use one). */
26
+ /* gap >= the active tab's ear width so the ear sits in the inter-tab gap
27
+ (over the strip) instead of spilling onto / occluding the neighbor tab. */
28
+ #tabs{height:40px;display:flex;align-items:flex-end;gap:6px;padding:8px 8px 0;background:var(--strip);overflow-x:auto;white-space:nowrap;-webkit-app-region:drag}
29
+ /* titleBarStyle:hidden — the strip IS the top of the window: make empty areas
30
+ draggable, and reserve room for the native window controls (mac traffic
31
+ lights on the left, win min/max/close overlay on the right). Tabs/buttons
32
+ stay clickable. */
33
+ body.is-mac #tabs{padding-left:78px}
34
+ body.is-win #tabs{padding-right:140px}
35
+ /* fullscreen → OS window controls hidden → reclaim the reserved gutter
36
+ (mac: traffic lights on the left, win: min/max/close overlay on the right) */
37
+ body.is-mac.is-fullscreen #tabs{padding-left:8px}
38
+ body.is-win.is-fullscreen #tabs{padding-right:8px}
39
+ .tab,#newtab{-webkit-app-region:no-drag}
40
+ #tabs::-webkit-scrollbar{height:0}
41
+ .tab{position:relative;display:flex;align-items:center;gap:9px;height:32px;min-width:54px;max-width:240px;padding:0 12px;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:rgba(255,255,255,.06);color:var(--fg)}
43
+ /* home = the resident homepage tab: pinned first, fixed width, user icon, no
44
+ title text, no close — like Chrome's profile/avatar button. */
45
+ /* start-page tab: pinned, sizes to its "我的团队" label (not a flex-grow tab) */
46
+ .tab.home{flex:0 0 auto;min-width:0;max-width:none;padding:0 12px 0 10px}
47
+ .tab.home .ttl{flex:0 0 auto}
48
+ .tab.home .fav{overflow:visible}
49
+ /* CiCy brand mark — gradient rounded square + ✦, matches the start page logo */
50
+ .cicy-logo{width:17px;height:17px;border-radius:5px;background:linear-gradient(135deg,#3b82f6,#8b5cf6);display:flex;align-items:center;justify-content:center;color:#fff;font-size:11px;line-height:1;box-shadow:0 1px 2px rgba(0,0,0,.3)}
51
+ /* the homepage tab has no toolbar beneath it (its content covers y=40 down),
52
+ so drop the outward ears — they'd otherwise meet the homepage top with a seam. */
53
+ .tab.home.active::before,.tab.home.active::after{display:none}
54
+ /* active tab = toolbar/content color, merged into the page, with outward bottom curves */
55
+ .tab.active{background:var(--toolbar);color:#fff;z-index:1}
56
+ /* outward bottom "ears" carved from the strip into the toolbar. gradient goes
57
+ strip→toolbar (NOT transparent→toolbar): a transparent→opaque ramp passes
58
+ through semi-transparent black and leaves a dark fringe on the lighter strip;
59
+ using the solid strip color makes the inner quarter blend seamlessly. */
60
+ /* ear width (5px) < the inter-tab gap (6px) so the ear never reaches the
61
+ neighbor tab — no overlap/occlusion on hover. */
62
+ .tab.active::before,.tab.active::after{content:"";position:absolute;bottom:0;width:5px;height:5px}
63
+ .tab.active::before{left:-5px;background:radial-gradient(circle at top left,var(--strip) 4.5px,var(--toolbar) 5px)}
64
+ .tab.active::after{right:-5px;background:radial-gradient(circle at top right,var(--strip) 4.5px,var(--toolbar) 5px)}
65
+ .fav{width:16px;height:16px;flex:0 0 16px;border-radius:4px;display:flex;align-items:center;justify-content:center;overflow:hidden}
66
+ .fav img{width:16px;height:16px;object-fit:contain}
67
+ .spin{width:14px;height:14px;border:2px solid rgba(255,255,255,.25);border-top-color:var(--accent);border-radius:50%;animation:sp .7s linear infinite}
68
+ @keyframes sp{to{transform:rotate(360deg)}}
69
+ .ttl{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
70
+ .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}
71
+ .tab:hover .cls,.tab.active .cls{opacity:.85}
72
+ .cls:hover{background:rgba(255,255,255,.16);color:#fff;opacity:1}
73
+ #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}
74
+ #newtab:hover{background:rgba(255,255,255,.08);color:#fff}
75
+
76
+ /* ── toolbar ── */
77
+ #bar{position:relative;height:40px;display:flex;align-items:center;gap:4px;padding:0 8px;background:var(--toolbar)}
78
+ .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}
79
+ .nav:hover{background:rgba(255,255,255,.10)}
80
+ .nav.off{color:var(--muted);opacity:.35;pointer-events:none}
81
+ #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}
82
+ #omni.focus{background:#111113;border-color:var(--accent)}
83
+ #lead{flex:0 0 16px;width:16px;height:16px;color:var(--muted);display:flex;align-items:center;justify-content:center}
84
+ #url{flex:1;height:100%;background:transparent;border:none;outline:none;color:var(--fg);font-size:13px;-webkit-user-select:text;user-select:text}
85
+ #url::placeholder{color:var(--muted)}
86
+
87
+ /* ── top load progress ── */
88
+ #prog{position:absolute;left:0;right:0;bottom:-1px;height:2px;overflow:hidden;pointer-events:none;opacity:0;transition:opacity .2s}
89
+ #prog.on{opacity:1}
90
+ #prog::before{content:"";position:absolute;left:0;top:0;height:100%;width:35%;background:linear-gradient(90deg,transparent,var(--accent),transparent);animation:load 1.05s ease-in-out infinite}
91
+ @keyframes load{0%{left:-35%}100%{left:100%}}
92
+ </style>
93
+ </head>
94
+ <body>
95
+ <div id="tabs"></div>
96
+ <div id="bar">
97
+ <div class="nav off" id="back" title="后退">
98
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg>
99
+ </div>
100
+ <div class="nav off" id="fwd" title="前进">
101
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"/></svg>
102
+ </div>
103
+ <div class="nav" id="reload" title="刷新">
104
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>
105
+ </div>
106
+ <div id="omni">
107
+ <span id="lead"></span>
108
+ <input id="url" placeholder="搜索或输入网址" spellcheck="false" autocomplete="off">
109
+ </div>
110
+ <div id="prog"></div>
111
+ </div>
112
+ <script>
113
+ const ICON_GLOBE = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#9aa0a6" 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>';
114
+ const CICY_LOGO = '<span class="cicy-logo">✦</span>';
115
+ 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>';
116
+
117
+ const tabsEl = document.getElementById('tabs');
118
+ const urlEl = document.getElementById('url');
119
+ const omniEl = document.getElementById('omni');
120
+ const leadEl = document.getElementById('lead');
121
+ const backEl = document.getElementById('back');
122
+ const fwdEl = document.getElementById('fwd');
123
+ const progEl = document.getElementById('prog');
124
+ const NO_NEW = new URLSearchParams(location.search).get('noNew') === '1'; // profile 0
125
+ const PLAT = new URLSearchParams(location.search).get('plat') || ''; // darwin|win32|linux
126
+ if (PLAT === 'darwin') document.body.classList.add('is-mac');
127
+ else if (PLAT === 'win32') document.body.classList.add('is-win');
128
+ try { window.tabAPI.onFullscreen((fs) => document.body.classList.toggle('is-fullscreen', !!fs)); } catch (e) {}
129
+ let state = { tabs: [], nav: {} };
130
+ let urlFocused = false;
131
+
132
+ function faviconNode(t) {
133
+ const w = document.createElement('span');
134
+ w.className = 'fav';
135
+ if (t.loading) { w.innerHTML = '<span class="spin"></span>'; return w; }
136
+ if (t.favicon) {
137
+ const img = document.createElement('img');
138
+ img.src = t.favicon;
139
+ img.onerror = () => { w.innerHTML = ICON_GLOBE; };
140
+ w.appendChild(img);
141
+ } else { w.innerHTML = ICON_GLOBE; }
142
+ return w;
143
+ }
144
+
145
+ function render() {
146
+ tabsEl.innerHTML = '';
147
+ const hasHome = state.tabs.some((x) => x.home);
148
+ const nonHome = state.tabs.filter((x) => !x.home).length;
149
+ state.tabs.forEach((t) => {
150
+ const d = document.createElement('div');
151
+ d.className = 'tab' + (t.home ? ' home' : '') + (t.active ? ' active' : '');
152
+ d.title = t.home ? '我的团队' : (t.url || '');
153
+ d.onclick = () => window.tabAPI.activate(t.id);
154
+ // resident start-page tab: pinned first, user icon only, no title, no close
155
+ if (t.home) {
156
+ const ic = document.createElement('span');
157
+ ic.className = 'fav'; ic.innerHTML = CICY_LOGO;
158
+ d.appendChild(ic);
159
+ const ttl = document.createElement('span');
160
+ ttl.className = 'ttl'; ttl.textContent = '我的团队';
161
+ d.appendChild(ttl);
162
+ tabsEl.appendChild(d);
163
+ return;
164
+ }
165
+ d.appendChild(faviconNode(t));
166
+ const ttl = document.createElement('span');
167
+ ttl.className = 'ttl'; ttl.textContent = t.title || '新标签页';
168
+ d.appendChild(ttl);
169
+ // closable unless it's the only tab and there's no resident homepage to
170
+ // keep the window alive (don't let the user empty a plain browser window).
171
+ if (hasHome || nonHome > 1) {
172
+ const cls = document.createElement('span');
173
+ cls.className = 'cls';
174
+ cls.innerHTML = '<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round"><line x1="5" y1="5" x2="19" y2="19"/><line x1="19" y1="5" x2="5" y2="19"/></svg>';
175
+ cls.onclick = (e) => { e.stopPropagation(); window.tabAPI.close(t.id); };
176
+ d.appendChild(cls);
177
+ }
178
+ tabsEl.appendChild(d);
179
+ });
180
+ if (!NO_NEW) {
181
+ const add = document.createElement('div');
182
+ add.id = 'newtab'; add.title = '新建标签';
183
+ add.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>';
184
+ add.onclick = () => window.tabAPI.newTab();
185
+ tabsEl.appendChild(add);
186
+ }
187
+
188
+ const nav = state.nav || {};
189
+ backEl.classList.toggle('off', !nav.canBack);
190
+ fwdEl.classList.toggle('off', !nav.canFwd);
191
+ progEl.classList.toggle('on', !!nav.loading);
192
+ const activeTab = state.tabs.find((x) => x.active);
193
+ const isHome = !!(activeTab && activeTab.home);
194
+ const u = nav.url || '';
195
+ const secure = /^https:\/\//i.test(u);
196
+ leadEl.style.color = secure ? '#81c995' : '#9aa0a6';
197
+ leadEl.innerHTML = secure ? ICON_LOCK : ICON_GLOBE;
198
+ // homepage tab → empty omnibox (like Chrome's new-tab page), don't expose the file:// path
199
+ if (!urlFocused) urlEl.value = isHome ? '' : displayUrl(u);
200
+ }
201
+
202
+ function displayUrl(u) {
203
+ // start pages (cicy://newtab) and data: URLs → empty omnibox (like Chrome NTP)
204
+ if (!u || u.startsWith('data:') || u.startsWith('cicyui://')) return '';
205
+ return u;
206
+ }
207
+
208
+ window.tabAPI.onState((s) => { state = s || { tabs: [], nav: {} }; render(); });
209
+
210
+ function go() {
211
+ let v = urlEl.value.trim(); if (!v) return;
212
+ if (!/^[a-z]+:\/\//i.test(v)) {
213
+ v = (/\.\w/.test(v) && !/\s/.test(v)) ? 'https://' + v : 'https://www.google.com/search?q=' + encodeURIComponent(v);
214
+ }
215
+ window.tabAPI.navigate(v);
216
+ }
217
+ urlEl.addEventListener('focus', () => { urlFocused = true; omniEl.classList.add('focus'); urlEl.select(); });
218
+ urlEl.addEventListener('blur', () => { urlFocused = false; omniEl.classList.remove('focus'); render(); });
219
+ urlEl.addEventListener('keydown', (e) => { if (e.key === 'Enter') { go(); urlEl.blur(); } });
220
+ backEl.onclick = () => window.tabAPI.back();
221
+ fwdEl.onclick = () => window.tabAPI.fwd();
222
+ document.getElementById('reload').onclick = () => window.tabAPI.reload();
223
+
224
+ window.tabAPI.ready();
225
+ </script>
226
+ </body>
227
+ </html>