cicy-desktop 2.1.85 → 2.1.86
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/bin/cicy-desktop +39 -0
- package/package.json +6 -6
- package/src/backends/homepage-preload.js +1 -1
- package/src/backends/homepage-react/assets/index-B7UhCeq6.js +365 -0
- package/src/backends/homepage-react/index.html +1 -1
- package/src/backends/local-teams.js +16 -3
- package/src/backends/sidecar-ipc.js +1 -1
- package/src/backends/window-manager.js +1 -1
- package/src/main.js +2 -2
- package/src/sidecar/cicy-code.js +16 -7
- package/src/sidecar/native.js +2 -2
- package/src/sidecar/version.js +2 -1
- package/src/tabbrowser/tab-shell-preload.js +3 -0
- package/src/tabbrowser/tab-shell.html +56 -1
- package/src/tools/tab-browser-tools.js +79 -0
- package/src/utils/window-monitor.js +43 -0
- package/workers/render/src/App.jsx +19 -21
- package/src/backends/homepage-react/assets/index-xHkT3-tl.js +0 -365
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
<link rel="icon" type="image/svg+xml" href="./favicon.svg" />
|
|
7
7
|
<link rel="icon" type="image/png" sizes="256x256" href="./favicon-256.png" />
|
|
8
8
|
<title>CiCy Desktop</title>
|
|
9
|
-
<script type="module" crossorigin src="./assets/index-
|
|
9
|
+
<script type="module" crossorigin src="./assets/index-B7UhCeq6.js"></script>
|
|
10
10
|
<link rel="stylesheet" crossorigin href="./assets/index-CKpaMBKz.css">
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
@@ -331,13 +331,25 @@ function stripVolatile(u) {
|
|
|
331
331
|
// Reload the web content of this team's already-open window (the homepage's
|
|
332
332
|
// 刷新 action). Matches the window the same way openTeam reuses one — by
|
|
333
333
|
// origin+pathname. No-op-with-error if no window is open for the team.
|
|
334
|
-
function reloadTeam(id) {
|
|
334
|
+
function reloadTeam(id, opts = {}) {
|
|
335
|
+
const { ignoreCache = false } = opts || {};
|
|
335
336
|
const node = readNodes()[id];
|
|
336
337
|
if (!node) return { ok: false, error: "team not found" };
|
|
337
338
|
const baseUrl = (node.base_url || "").replace(/\/$/, "");
|
|
338
339
|
if (!baseUrl) return { ok: false, error: "no base_url" };
|
|
339
340
|
const token = node.api_token || "";
|
|
340
341
|
const url = token ? `${baseUrl}/?token=${encodeURIComponent(token)}` : baseUrl;
|
|
342
|
+
// 本地团队都开在 **profile 0** 的标签窗口里(BrowserView tab),不是顶层
|
|
343
|
+
// BrowserWindow。所以先走 account-0 的标签管理器按 URL 找那个标签 IN-PLACE 刷
|
|
344
|
+
// (ignoreCache 绕缓存,cicy-code 升级后才能拿到新资源而非缓存的旧 index.html)。
|
|
345
|
+
// 找不到 = 标签没开 → no_open_window,绝不偷偷开新标签。
|
|
346
|
+
// (旧版 reloadTeam 在顶层 BrowserWindow 里找,永远找不到 BrowserView 标签 →
|
|
347
|
+
// 永远 no_open_window:刷新窗口从没真刷、更新后自动刷静默失效,都是这个 bug。)
|
|
348
|
+
try {
|
|
349
|
+
const r = require("../tools/tab-browser-tools").reloadTabIfOpen(0, url, { ignoreCache });
|
|
350
|
+
if (r && r.ok) { log.info(`[local-teams] reload ${id} → tab in win.id=${r.winId} ignoreCache=${ignoreCache}`); return r; }
|
|
351
|
+
} catch (e) { log.warn(`[local-teams] reload ${id} tab path failed: ${e.message}`); }
|
|
352
|
+
// 兜底:极少数情况下 openTeam 退化成真窗口(openTab 抛错时),按老方式找顶层窗口。
|
|
341
353
|
const targetKey = stripVolatile(url);
|
|
342
354
|
const win = BrowserWindow.getAllWindows().find((w) => {
|
|
343
355
|
if (!w || w.isDestroyed()) return false;
|
|
@@ -346,10 +358,11 @@ function reloadTeam(id) {
|
|
|
346
358
|
});
|
|
347
359
|
if (!win) return { ok: false, error: "no_open_window" };
|
|
348
360
|
try {
|
|
349
|
-
win.webContents.
|
|
361
|
+
if (ignoreCache) win.webContents.reloadIgnoringCache();
|
|
362
|
+
else win.webContents.reload();
|
|
350
363
|
if (win.isMinimized()) win.restore();
|
|
351
364
|
win.show(); win.focus();
|
|
352
|
-
log.info(`[local-teams] reload ${id} → win.id=${win.id}`);
|
|
365
|
+
log.info(`[local-teams] reload ${id} → win.id=${win.id} ignoreCache=${ignoreCache}`);
|
|
353
366
|
return { ok: true, windowId: win.id };
|
|
354
367
|
} catch (e) {
|
|
355
368
|
return { ok: false, error: e.message };
|
|
@@ -16,7 +16,7 @@ const { ipcMain } = require("electron");
|
|
|
16
16
|
const sidecar = require("../sidecar/cicy-code");
|
|
17
17
|
const docker = require("../sidecar/docker");
|
|
18
18
|
|
|
19
|
-
const PORT = Number(process.env.CICY_CODE_PORT || 8008);
|
|
19
|
+
const PORT = Number(process.env.CICY_CODE_PORT || (process.platform === "win32" ? 8007 : 8008));
|
|
20
20
|
let registered = false;
|
|
21
21
|
|
|
22
22
|
function register({ sidecarLogPath } = {}) {
|
|
@@ -11,7 +11,7 @@ const sidecar = require("../sidecar/cicy-code");
|
|
|
11
11
|
const { createWindow } = require("../utils/window-utils");
|
|
12
12
|
const registry = require("./registry");
|
|
13
13
|
|
|
14
|
-
const LOCAL_PORT = Number(process.env.CICY_CODE_PORT || 8008);
|
|
14
|
+
const LOCAL_PORT = Number(process.env.CICY_CODE_PORT || (process.platform === "win32" ? 8007 : 8008));
|
|
15
15
|
const LOCAL_HOST = "127.0.0.1";
|
|
16
16
|
|
|
17
17
|
// On a typical install cicy-code runs as the same user as cicy-desktop,
|
package/src/main.js
CHANGED
|
@@ -862,7 +862,7 @@ electronApp.whenReady().then(async () => {
|
|
|
862
862
|
// cloud team register + gateway-key injection when logged in. A fresh boot
|
|
863
863
|
// may npm-seed the runtime first, so probe for up to ~90s before giving up.
|
|
864
864
|
(async () => {
|
|
865
|
-
const sidecarPort = Number(process.env.CICY_CODE_PORT || 8008);
|
|
865
|
+
const sidecarPort = Number(process.env.CICY_CODE_PORT || (process.platform === "win32" ? 8007 : 8008));
|
|
866
866
|
const lt = require("./backends/local-teams");
|
|
867
867
|
for (let i = 0; i < 30; i++) {
|
|
868
868
|
try {
|
|
@@ -1071,7 +1071,7 @@ electronApp.whenReady().then(async () => {
|
|
|
1071
1071
|
const { ipcMain: __ipcLT } = require("electron");
|
|
1072
1072
|
__ipcLT.handle("localTeams:list", (_e, opts) => lt.list(opts || {}));
|
|
1073
1073
|
__ipcLT.handle("localTeams:open", (_e, id) => lt.openTeam(id));
|
|
1074
|
-
__ipcLT.handle("localTeams:reload", (_e, id)
|
|
1074
|
+
__ipcLT.handle("localTeams:reload", (_e, id, opts) => lt.reloadTeam(id, opts));
|
|
1075
1075
|
__ipcLT.handle("localTeams:add", (_e, spec) => lt.addTeam(spec || {}));
|
|
1076
1076
|
__ipcLT.handle("localTeams:remove", (_e, id) => lt.removeTeam(id));
|
|
1077
1077
|
__ipcLT.handle("localTeams:update", (_e, payload) => lt.updateTeam(payload?.id, payload?.patch || {}));
|
package/src/sidecar/cicy-code.js
CHANGED
|
@@ -20,7 +20,9 @@ const net = require("net");
|
|
|
20
20
|
const path = require("path");
|
|
21
21
|
const { spawn, execFileSync } = require("child_process");
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
// Default cicy-code port: 8007 on Windows, 8008 elsewhere (主人令). CICY_CODE_PORT
|
|
24
|
+
// overrides on every platform.
|
|
25
|
+
const DEFAULT_PORT = Number(process.env.CICY_CODE_PORT || (process.platform === "win32" ? 8007 : 8008));
|
|
24
26
|
|
|
25
27
|
// Liveness = "is something LISTENING on :port", via a raw TCP connect — NOT an
|
|
26
28
|
// HTTP GET. /health can block (mid-boot, busy, hung) and time out even while the
|
|
@@ -88,8 +90,8 @@ async function startFromRuntime({ logPath, port }) {
|
|
|
88
90
|
if (msys) env.CICY_MSYS_ROOT = msys; // w-10084 exe 探测约定
|
|
89
91
|
} catch {}
|
|
90
92
|
}
|
|
91
|
-
const c = spawn(exe, [], { stdio, detached: false, windowsHide: true, env });
|
|
92
|
-
console.log(`[cicy-code-sidecar] spawned runtime ${exe} (v${runtime.currentVersion("cicy-code")}) pid=${c.pid} port=${port}`);
|
|
93
|
+
const c = spawn(exe, ["--desktop"], { stdio, detached: false, windowsHide: true, env });
|
|
94
|
+
console.log(`[cicy-code-sidecar] spawned runtime ${exe} --desktop (v${runtime.currentVersion("cicy-code")}) pid=${c.pid} port=${port}`);
|
|
93
95
|
c.on("exit", (code, signal) => {
|
|
94
96
|
console.log(`[cicy-code-sidecar] exited code=${code} signal=${signal}`);
|
|
95
97
|
child = null;
|
|
@@ -145,7 +147,7 @@ async function start({ logPath, port = DEFAULT_PORT, force = false, version = nu
|
|
|
145
147
|
// --helper removed (主人指令): Windows now runs cicy-code in normal mode (full
|
|
146
148
|
// tmux-based multi-agent via the bundled MSYS2 runtime), same as mac/linux —
|
|
147
149
|
// no longer the single headless 团队助手.
|
|
148
|
-
const args = [];
|
|
150
|
+
const args = ["--desktop"];
|
|
149
151
|
child = spawn(exe, args, { stdio, detached: false, windowsHide: true, env });
|
|
150
152
|
console.log(`[cicy-code-sidecar] spawned ${exe} ${args.join(" ")} pid=${child.pid} port=${port} log=${logPath || "(none)"}`);
|
|
151
153
|
|
|
@@ -314,13 +316,20 @@ async function update({ logPath, port = DEFAULT_PORT, emit } = {}) {
|
|
|
314
316
|
e({ phase: "swap", status: "running", message: "启动 cicy-code…" });
|
|
315
317
|
const c = await start({ logPath, port, force: true });
|
|
316
318
|
|
|
317
|
-
// 4) 探活:等 TCP
|
|
319
|
+
// 4) 探活:等 TCP 监听起来。注意:cicy-code 启动会先恢复团队的 agent 面板
|
|
320
|
+
// (w-1xx,可能十几个),:8008 在这些 REPL 拉起之后才 bind —— 繁忙团队这一步
|
|
321
|
+
// 可能要 1~2 分钟。所以探活窗口放到 180s(原 60s 太短,会把"还在恢复 agent"
|
|
322
|
+
// 误判成"启动失败",抽屉卡在「启动 cicy-code…」)。子进程一旦真退出(崩了)
|
|
323
|
+
// 立即停手,不空等满 180s。
|
|
324
|
+
const PROBE_TRIES = 360; // 360 * 500ms = 180s
|
|
318
325
|
let up = false;
|
|
319
|
-
for (let i = 0; i <
|
|
326
|
+
for (let i = 0; i < PROBE_TRIES; i++) {
|
|
320
327
|
if (await probeExisting(port)) { up = true; break; }
|
|
328
|
+
if (c && c.exitCode != null) break; // 进程已退出 = 真失败,别空等
|
|
329
|
+
if (i === 30) e({ phase: "swap", status: "running", message: "启动 cicy-code…(正在恢复 agent 面板,稍候)" });
|
|
321
330
|
await new Promise(r => setTimeout(r, 500));
|
|
322
331
|
}
|
|
323
|
-
if (!up) { e({ phase: "done", status: "error", message:
|
|
332
|
+
if (!up) { e({ phase: "done", status: "error", message: `cicy-code 未在 ${PROBE_TRIES / 2}s 内启动` }); return c; }
|
|
324
333
|
|
|
325
334
|
// 5) 拿运行中真实 version(唯一来源 version.running();可能略慢于 TCP,重试几次)
|
|
326
335
|
const version = require("./version");
|
package/src/sidecar/native.js
CHANGED
|
@@ -142,10 +142,10 @@ async function start({ port = 8008, logPath = null, emit, version = null } = {})
|
|
|
142
142
|
};
|
|
143
143
|
// --helper removed (主人指令): boot cicy-code in normal mode (full tmux-based
|
|
144
144
|
// multi-agent), not the single headless 团队助手.
|
|
145
|
-
const child = spawn(exe, [], { stdio, detached: true, windowsHide: true, env });
|
|
145
|
+
const child = spawn(exe, ["--desktop"], { stdio, detached: true, windowsHide: true, env });
|
|
146
146
|
child.unref();
|
|
147
147
|
try { fs.writeFileSync(PID_FILE, String(child.pid)); } catch {}
|
|
148
|
-
console.log(`[native-sidecar] spawned ${exe} pid=${child.pid} port=${port} log=${logPath || "(none)"}`);
|
|
148
|
+
console.log(`[native-sidecar] spawned ${exe} --desktop pid=${child.pid} port=${port} log=${logPath || "(none)"}`);
|
|
149
149
|
|
|
150
150
|
const up = await docker.waitUntil(() => probeHealth(port), { totalMs: 60000, everyMs: 2000 });
|
|
151
151
|
if (!up) {
|
package/src/sidecar/version.js
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
// installed() → 磁盘 binary 版本(localbin manifest,诊断用)
|
|
6
6
|
const http = require("http");
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
// Match the sidecar default: 8007 on Windows, 8008 elsewhere (CICY_CODE_PORT wins).
|
|
9
|
+
const DEFAULT_PORT = Number(process.env.CICY_CODE_PORT || (process.platform === "win32" ? 8007 : 8008));
|
|
9
10
|
|
|
10
11
|
// The ONE running-version reader. GET /api/health → version. Returns the version
|
|
11
12
|
// string, or null on any failure / missing field. Used by the update flow's
|
|
@@ -8,6 +8,9 @@ contextBridge.exposeInMainWorld("tabAPI", {
|
|
|
8
8
|
newTab: (url) => ipcRenderer.send("tabwin:new", { url: url || "" }),
|
|
9
9
|
activate: (id) => ipcRenderer.send("tabwin:activate", { id }),
|
|
10
10
|
close: (id) => ipcRenderer.send("tabwin:close", { id }),
|
|
11
|
+
// Reorder tabs (Chrome-style drag). `ids` = the new order of NON-home tab ids;
|
|
12
|
+
// main keeps the resident homepage tab pinned first.
|
|
13
|
+
reorder: (ids) => ipcRenderer.send("tabwin:reorder", { ids }),
|
|
11
14
|
navigate: (url) => ipcRenderer.send("tabwin:navigate", { url }),
|
|
12
15
|
back: () => ipcRenderer.send("tabwin:back"),
|
|
13
16
|
fwd: () => ipcRenderer.send("tabwin:fwd"),
|
|
@@ -81,6 +81,10 @@
|
|
|
81
81
|
.cls:hover{background:rgba(255,255,255,.16);color:#fff;opacity:1}
|
|
82
82
|
#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}
|
|
83
83
|
#newtab:hover{background:rgba(255,255,255,.08);color:#fff}
|
|
84
|
+
/* ── drag-to-reorder (Chrome-style) ── */
|
|
85
|
+
.tab.dragging{opacity:.4}
|
|
86
|
+
/* vertical insertion bar shown between tabs while dragging */
|
|
87
|
+
#dropmarker{position:fixed;width:2px;background:var(--accent);border-radius:1px;pointer-events:none;z-index:20;display:none;box-shadow:0 0 4px var(--accent)}
|
|
84
88
|
|
|
85
89
|
/* ── toolbar ── */
|
|
86
90
|
#bar{position:relative;height:40px;display:flex;align-items:center;gap:4px;padding:0 8px;background:var(--toolbar)}
|
|
@@ -137,6 +141,8 @@
|
|
|
137
141
|
try { window.tabAPI.onFullscreen((fs) => document.body.classList.toggle('is-fullscreen', !!fs)); } catch (e) {}
|
|
138
142
|
let state = { tabs: [], nav: {} };
|
|
139
143
|
let urlFocused = false;
|
|
144
|
+
let dragId = null; // id of the tab being dragged (Chrome-style reorder)
|
|
145
|
+
let dropMarker = null; // the vertical insertion bar element
|
|
140
146
|
|
|
141
147
|
function faviconNode(t) {
|
|
142
148
|
const w = document.createElement('span');
|
|
@@ -151,6 +157,51 @@
|
|
|
151
157
|
return w;
|
|
152
158
|
}
|
|
153
159
|
|
|
160
|
+
// ── drag-to-reorder (Chrome-style) ──────────────────────────────────────────
|
|
161
|
+
function clearDropMarker() { if (dropMarker) dropMarker.style.display = 'none'; }
|
|
162
|
+
function showDropMarker(tabEl, after) {
|
|
163
|
+
if (!dropMarker) { dropMarker = document.createElement('div'); dropMarker.id = 'dropmarker'; document.body.appendChild(dropMarker); }
|
|
164
|
+
const r = tabEl.getBoundingClientRect();
|
|
165
|
+
dropMarker.style.left = ((after ? r.right : r.left) - 1) + 'px';
|
|
166
|
+
dropMarker.style.top = r.top + 'px';
|
|
167
|
+
dropMarker.style.height = r.height + 'px';
|
|
168
|
+
dropMarker.style.display = 'block';
|
|
169
|
+
}
|
|
170
|
+
// Compute the new order of NON-home tab ids and hand it to main.
|
|
171
|
+
function reorderTabs(srcId, targetId, after) {
|
|
172
|
+
if (srcId === targetId) return;
|
|
173
|
+
const ids = state.tabs.filter((x) => !x.home).map((x) => x.id);
|
|
174
|
+
const from = ids.indexOf(srcId); if (from < 0) return;
|
|
175
|
+
ids.splice(from, 1);
|
|
176
|
+
let to = ids.indexOf(targetId); if (to < 0) return;
|
|
177
|
+
if (after) to += 1;
|
|
178
|
+
ids.splice(to, 0, srcId);
|
|
179
|
+
window.tabAPI.reorder(ids);
|
|
180
|
+
}
|
|
181
|
+
function attachDrag(d, id) {
|
|
182
|
+
d.draggable = true;
|
|
183
|
+
d.addEventListener('dragstart', (e) => {
|
|
184
|
+
dragId = id;
|
|
185
|
+
try { e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', String(id)); } catch (_) {}
|
|
186
|
+
d.classList.add('dragging');
|
|
187
|
+
});
|
|
188
|
+
d.addEventListener('dragend', () => { dragId = null; clearDropMarker(); render(); });
|
|
189
|
+
d.addEventListener('dragover', (e) => {
|
|
190
|
+
if (dragId == null || dragId === id) return;
|
|
191
|
+
e.preventDefault();
|
|
192
|
+
try { e.dataTransfer.dropEffect = 'move'; } catch (_) {}
|
|
193
|
+
const r = d.getBoundingClientRect();
|
|
194
|
+
showDropMarker(d, (e.clientX - r.left) > r.width / 2);
|
|
195
|
+
});
|
|
196
|
+
d.addEventListener('drop', (e) => {
|
|
197
|
+
if (dragId == null) return;
|
|
198
|
+
e.preventDefault();
|
|
199
|
+
const r = d.getBoundingClientRect();
|
|
200
|
+
reorderTabs(dragId, id, (e.clientX - r.left) > r.width / 2);
|
|
201
|
+
dragId = null; clearDropMarker();
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
154
205
|
function render() {
|
|
155
206
|
tabsEl.innerHTML = '';
|
|
156
207
|
const hasHome = state.tabs.some((x) => x.home);
|
|
@@ -184,6 +235,7 @@
|
|
|
184
235
|
cls.onclick = (e) => { e.stopPropagation(); window.tabAPI.close(t.id); };
|
|
185
236
|
d.appendChild(cls);
|
|
186
237
|
}
|
|
238
|
+
attachDrag(d, t.id); // Chrome-style drag-to-reorder (home tab stays pinned)
|
|
187
239
|
tabsEl.appendChild(d);
|
|
188
240
|
});
|
|
189
241
|
if (!NO_NEW) {
|
|
@@ -214,7 +266,10 @@
|
|
|
214
266
|
return u;
|
|
215
267
|
}
|
|
216
268
|
|
|
217
|
-
|
|
269
|
+
// Skip re-render while a drag is in flight — rebuilding tabsEl.innerHTML mid-drag
|
|
270
|
+
// would destroy the dragged element and abort the gesture. State is still stored;
|
|
271
|
+
// dragend triggers a render to sync.
|
|
272
|
+
window.tabAPI.onState((s) => { state = s || { tabs: [], nav: {} }; if (dragId != null) return; render(); });
|
|
218
273
|
|
|
219
274
|
function go() {
|
|
220
275
|
let v = urlEl.value.trim(); if (!v) return;
|
|
@@ -183,6 +183,10 @@ class TabManager {
|
|
|
183
183
|
// attaches to BrowserWindows + webviews), so give each tab exactly one
|
|
184
184
|
// right-click menu (copy/paste/inspect) — without this, right-click did nothing.
|
|
185
185
|
try { attachContextMenu(wc); } catch (e) {}
|
|
186
|
+
// Buffer this tab's console (keyed by its webContents.id) so
|
|
187
|
+
// get_tab_console_logs(<wcId>) can read it — window-monitor only listens on
|
|
188
|
+
// BrowserWindow main webContents, never BrowserView tabs.
|
|
189
|
+
try { require("../utils/window-monitor").attachTabConsole(wc); } catch (e) {}
|
|
186
190
|
// home = the resident homepage tab (pinned, first, user-icon, no close).
|
|
187
191
|
// fixedTitle = a caller-supplied tab name (e.g. the team title) that the
|
|
188
192
|
// page's own document.title must NOT override.
|
|
@@ -245,6 +249,38 @@ class TabManager {
|
|
|
245
249
|
return true;
|
|
246
250
|
}
|
|
247
251
|
|
|
252
|
+
// Chrome-style drag reorder. `orderedIds` is the desired order of the NON-home
|
|
253
|
+
// tabs (the resident homepage tab stays pinned first regardless). Unknown/missing
|
|
254
|
+
// ids are ignored; any movable tab not named keeps its relative order at the end.
|
|
255
|
+
reorder(orderedIds) {
|
|
256
|
+
if (!Array.isArray(orderedIds)) return false;
|
|
257
|
+
const home = this.tabs.filter((t) => t.home);
|
|
258
|
+
const movable = this.tabs.filter((t) => !t.home);
|
|
259
|
+
const byId = new Map(movable.map((t) => [t.id, t]));
|
|
260
|
+
const next = [];
|
|
261
|
+
for (const id of orderedIds) { const t = byId.get(id); if (t && !next.includes(t)) next.push(t); }
|
|
262
|
+
for (const t of movable) { if (!next.includes(t)) next.push(t); } // keep any unnamed tabs
|
|
263
|
+
this.tabs = [...home, ...next];
|
|
264
|
+
this.pushState();
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Reload the tab whose URL matches (origin+pathname) IN PLACE — used by the
|
|
269
|
+
// homepage team card's 刷新窗口 / 更新后自动刷. ignoreCache → reloadIgnoringCache
|
|
270
|
+
// (re-fetch new assets after a cicy-code update, not the cached index.html).
|
|
271
|
+
// Returns true iff a matching tab was found+reloaded; NEVER opens a new tab.
|
|
272
|
+
reloadTabByUrlInPlace(url, { ignoreCache = false } = {}) {
|
|
273
|
+
const key = stripVol(url);
|
|
274
|
+
const tab = this.tabs.find((t) => stripVol(t.url) === key);
|
|
275
|
+
if (!tab) return false;
|
|
276
|
+
try {
|
|
277
|
+
if (ignoreCache) tab.view.webContents.reloadIgnoringCache();
|
|
278
|
+
else tab.view.webContents.reload();
|
|
279
|
+
} catch (e) {}
|
|
280
|
+
try { this.activate(tab.id); this.win.show(); this.win.focus(); } catch (e) {}
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
283
|
+
|
|
248
284
|
list() { return this.tabs.map((t) => ({ webContentsId: t.id, title: t.title, url: t.url, active: t.id === this.activeId })); }
|
|
249
285
|
|
|
250
286
|
activeWc() { const t = this.tabs.find((x) => x.id === this.activeId); return t ? t.view.webContents : null; }
|
|
@@ -299,6 +335,7 @@ function installIpc() {
|
|
|
299
335
|
ipcMain.on("tabwin:new", (e, { url }) => { const m = mgr(e); if (m) m.addTab(url || ""); });
|
|
300
336
|
ipcMain.on("tabwin:activate", (e, { id }) => { const m = mgr(e); if (m) m.activate(id); });
|
|
301
337
|
ipcMain.on("tabwin:close", (e, { id }) => { const m = mgr(e); if (m) m.close(id); });
|
|
338
|
+
ipcMain.on("tabwin:reorder", (e, { ids }) => { const m = mgr(e); if (m) m.reorder(ids); });
|
|
302
339
|
ipcMain.on("tabwin:navigate", (e, { url }) => { const m = mgr(e); const wc = m && m.activeWc(); if (wc && url) wc.loadURL(String(url)); });
|
|
303
340
|
ipcMain.on("tabwin:back", (e) => { const m = mgr(e); const wc = m && m.activeWc(); if (wc && wc.canGoBack()) wc.goBack(); });
|
|
304
341
|
ipcMain.on("tabwin:fwd", (e) => { const m = mgr(e); const wc = m && m.activeWc(); if (wc && wc.canGoForward()) wc.goForward(); });
|
|
@@ -443,6 +480,36 @@ function registerTabBrowserTools(registerTool) {
|
|
|
443
480
|
},
|
|
444
481
|
{ tag: "TabBrowser" }
|
|
445
482
|
);
|
|
483
|
+
|
|
484
|
+
registerTool(
|
|
485
|
+
"get_tab_console_logs",
|
|
486
|
+
"获取某个标签(按 webContentsId)的控制台日志:自该标签创建以来捕获的所有 console 输出(log/info/warning/error)。支持关键词/级别过滤、分页;最新在前。",
|
|
487
|
+
z.object({
|
|
488
|
+
webContentsId: z.number().describe("标签的 webContentsId"),
|
|
489
|
+
page: z.number().optional().default(1).describe("页码,从 1 开始"),
|
|
490
|
+
page_size: z.number().optional().default(50).describe("每页数量"),
|
|
491
|
+
keyword: z.string().optional().describe("关键词过滤,匹配日志消息"),
|
|
492
|
+
level: z.enum(["verbose", "info", "warning", "error"]).optional().describe("日志级别过滤"),
|
|
493
|
+
}),
|
|
494
|
+
async ({ webContentsId, page, page_size, keyword, level }) => {
|
|
495
|
+
try {
|
|
496
|
+
let logs = require("../utils/window-monitor").getTabConsoleLogs(webContentsId);
|
|
497
|
+
if (keyword) logs = logs.filter((l) => l.message.includes(keyword));
|
|
498
|
+
if (level) logs = logs.filter((l) => l.level === level);
|
|
499
|
+
logs = [...logs].sort((a, b) => b.timestamp - a.timestamp);
|
|
500
|
+
const start = (page - 1) * page_size;
|
|
501
|
+
const paginated = logs.slice(start, start + page_size);
|
|
502
|
+
const header = `Tab Console Logs (wc=${webContentsId}, ${logs.length} total, page ${page}/${Math.ceil(logs.length / page_size) || 1}):\n`;
|
|
503
|
+
const lines = paginated.map((l) => {
|
|
504
|
+
const time = new Date(l.timestamp).toISOString().replace("T", " ").substring(0, 23);
|
|
505
|
+
const src = l.source ? ` (${String(l.source).split("/").pop()}:${l.line})` : "";
|
|
506
|
+
return `${time} ${l.level.toUpperCase().padEnd(7)} ${String(l.message).replace(/\n/g, " ").substring(0, 200)}${src}`;
|
|
507
|
+
});
|
|
508
|
+
return { content: [{ type: "text", text: header + (lines.join("\n") || "(no console output)") }] };
|
|
509
|
+
} catch (e) { return { content: [{ type: "text", text: `Error: ${e.message}` }], isError: true }; }
|
|
510
|
+
},
|
|
511
|
+
{ tag: "TabBrowser" }
|
|
512
|
+
);
|
|
446
513
|
}
|
|
447
514
|
|
|
448
515
|
// Reload the profile-N tab whose URL matches (origin+pathname); if none is open,
|
|
@@ -462,8 +529,20 @@ async function reloadTabByUrl(accountIdx, url, opts = {}) {
|
|
|
462
529
|
return { ok: true, winId: r.winId, opened: true };
|
|
463
530
|
}
|
|
464
531
|
|
|
532
|
+
// Reload an OPEN team tab in `accountIdx`'s window, in place (no open-if-missing).
|
|
533
|
+
// Returns { ok:true, reloaded:true } if a matching tab was found, else
|
|
534
|
+
// { ok:false, error:"no_open_window" }. Used by local-teams.reloadTeam (profile 0).
|
|
535
|
+
function reloadTabIfOpen(accountIdx, url, opts = {}) {
|
|
536
|
+
const m = managers.get(accountIdx);
|
|
537
|
+
if (!m || m.win.isDestroyed()) return { ok: false, error: "no_open_window" };
|
|
538
|
+
return m.reloadTabByUrlInPlace(url, opts)
|
|
539
|
+
? { ok: true, winId: m.win.id, reloaded: true }
|
|
540
|
+
: { ok: false, error: "no_open_window" };
|
|
541
|
+
}
|
|
542
|
+
|
|
465
543
|
registerTabBrowserTools.openTab = openTab;
|
|
466
544
|
registerTabBrowserTools.reloadTabByUrl = reloadTabByUrl;
|
|
545
|
+
registerTabBrowserTools.reloadTabIfOpen = reloadTabIfOpen;
|
|
467
546
|
registerTabBrowserTools.openHomeWindow = openHomeWindow;
|
|
468
547
|
registerTabBrowserTools.ensureManager = ensureManager;
|
|
469
548
|
module.exports = registerTabBrowserTools;
|
|
@@ -6,6 +6,12 @@ const log = require("electron-log");
|
|
|
6
6
|
|
|
7
7
|
// 存储每个窗口的日志和请求
|
|
8
8
|
const windowLogs = new Map();
|
|
9
|
+
// Tab (BrowserView) console logs, keyed by the TAB's webContents.id. SEPARATE
|
|
10
|
+
// from windowLogs because window-monitor keys windowLogs by BrowserWindow.id —
|
|
11
|
+
// a different counter from webContents.id, so the two id spaces overlap and must
|
|
12
|
+
// not share one Map. Read via getTabConsoleLogs(webContentsId).
|
|
13
|
+
const tabConsoleLogs = new Map();
|
|
14
|
+
const tabConsoleCounters = new Map();
|
|
9
15
|
const windowRequests = new Map(); // 已废弃,保留兼容性
|
|
10
16
|
const windowRequestDetails = new Map();
|
|
11
17
|
const windowIndexCounters = new Map();
|
|
@@ -538,6 +544,41 @@ function getConsoleLogs(winId) {
|
|
|
538
544
|
return windowLogs.get(winId) || [];
|
|
539
545
|
}
|
|
540
546
|
|
|
547
|
+
// Attach a console-message listener to ONE tab's webContents, buffering into
|
|
548
|
+
// tabConsoleLogs keyed by its webContents.id. Mirrors the windowLogs entry shape
|
|
549
|
+
// (index/timestamp/level/message/line/source) so get_tab_console_logs formats it
|
|
550
|
+
// the same way. Idempotent per webContents; auto-cleans on destroy.
|
|
551
|
+
function attachTabConsole(wc) {
|
|
552
|
+
if (!wc || wc.isDestroyed?.()) return;
|
|
553
|
+
const id = wc.id;
|
|
554
|
+
if (tabConsoleLogs.has(id)) return;
|
|
555
|
+
tabConsoleLogs.set(id, []);
|
|
556
|
+
tabConsoleCounters.set(id, { log: 0 });
|
|
557
|
+
wc.on("console-message", (_event, level, message, line, sourceId) => {
|
|
558
|
+
const logs = tabConsoleLogs.get(id);
|
|
559
|
+
const counters = tabConsoleCounters.get(id);
|
|
560
|
+
if (!logs || !counters) return;
|
|
561
|
+
logs.push({
|
|
562
|
+
index: ++counters.log,
|
|
563
|
+
timestamp: Date.now(),
|
|
564
|
+
level: ["verbose", "info", "warning", "error"][level] || "log",
|
|
565
|
+
message,
|
|
566
|
+
line,
|
|
567
|
+
source: sourceId,
|
|
568
|
+
});
|
|
569
|
+
// Cap so a chatty page can't grow it unbounded.
|
|
570
|
+
if (logs.length > 5000) logs.splice(0, logs.length - 5000);
|
|
571
|
+
});
|
|
572
|
+
wc.once("destroyed", () => {
|
|
573
|
+
tabConsoleLogs.delete(id);
|
|
574
|
+
tabConsoleCounters.delete(id);
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function getTabConsoleLogs(webContentsId) {
|
|
579
|
+
return tabConsoleLogs.get(webContentsId) || [];
|
|
580
|
+
}
|
|
581
|
+
|
|
541
582
|
function getRequests(winId) {
|
|
542
583
|
return windowRequests.get(winId) || [];
|
|
543
584
|
}
|
|
@@ -602,6 +643,8 @@ function getRequestDetail(winId, index) {
|
|
|
602
643
|
module.exports = {
|
|
603
644
|
initWindowMonitoring,
|
|
604
645
|
getConsoleLogs,
|
|
646
|
+
attachTabConsole,
|
|
647
|
+
getTabConsoleLogs,
|
|
605
648
|
getRequests,
|
|
606
649
|
getBeforeSendRequests,
|
|
607
650
|
getLoadingFinishedRequests,
|
|
@@ -737,14 +737,6 @@ export default function App() {
|
|
|
737
737
|
}}
|
|
738
738
|
/>
|
|
739
739
|
))}
|
|
740
|
-
{showLocal && (
|
|
741
|
-
<button type="button" className="add-card" onClick={() => {
|
|
742
|
-
alert("装本地 cicy-code(npx cicy-code / docker run)后会自动出现,或在云端创建团队。");
|
|
743
|
-
}}>
|
|
744
|
-
<span className="add-card__plus">+</span>
|
|
745
|
-
<span className="add-card__label">新建本地团队</span>
|
|
746
|
-
</button>
|
|
747
|
-
)}
|
|
748
740
|
</div>
|
|
749
741
|
|
|
750
742
|
{!profileLoading && !profileError && teams && teams.length === 0 && !localTeams?.length && (
|
|
@@ -1562,6 +1554,10 @@ function LocalTeamCard({ team, onOpen, onRename, onRefresh }) {
|
|
|
1562
1554
|
const errMsg = tr("sidecar.failed", "操作失败") + (r?.error ? `: ${r.error}` : "");
|
|
1563
1555
|
if (isUpdate) {
|
|
1564
1556
|
updateDrawer.finish({ ok, message: ok ? okMsg : errMsg });
|
|
1557
|
+
// 更新成功 = 新 cicy-code 已切换并启动。若该团队的 :8008 窗口正开着,
|
|
1558
|
+
// 直接刷新它,让用户立刻用上新版(没开窗口则 reload 返回 no_open_window,no-op)。
|
|
1559
|
+
// ignoreCache:绕过 HTTP 缓存重载,否则可能复用缓存的旧 index.html → 仍跑旧版。
|
|
1560
|
+
if (ok) { try { await window.cicy?.localTeams?.reload?.(team.id, { ignoreCache: true }); } catch {} }
|
|
1565
1561
|
} else {
|
|
1566
1562
|
toast.show({ id: opToastId, message: ok ? okMsg : errMsg, progress: undefined, status: ok ? "done" : "error", ttl: ok ? 4000 : 8000 });
|
|
1567
1563
|
}
|
|
@@ -1633,6 +1629,17 @@ function LocalTeamCard({ team, onOpen, onRename, onRefresh }) {
|
|
|
1633
1629
|
ref={menuRef}
|
|
1634
1630
|
style={{ position: "fixed", top: menuPos.top, left: menuPos.left, width: MENU_W }}
|
|
1635
1631
|
onClick={(e) => e.stopPropagation()}>
|
|
1632
|
+
{local && (
|
|
1633
|
+
<button
|
|
1634
|
+
type="button"
|
|
1635
|
+
data-id="LocalTeamCard-check-update"
|
|
1636
|
+
className="bcard__menu-item"
|
|
1637
|
+
disabled={checking}
|
|
1638
|
+
onClick={(e) => { e.stopPropagation(); checkUpdate(true); }}
|
|
1639
|
+
>
|
|
1640
|
+
{checking ? tr("sidecar.checking2", "检查中…") : tr("sidecar.checkUpdate", "检查更新")}
|
|
1641
|
+
</button>
|
|
1642
|
+
)}
|
|
1636
1643
|
{updateAvailable && (
|
|
1637
1644
|
<button
|
|
1638
1645
|
type="button"
|
|
@@ -1651,8 +1658,10 @@ function LocalTeamCard({ team, onOpen, onRename, onRefresh }) {
|
|
|
1651
1658
|
className="bcard__menu-item"
|
|
1652
1659
|
onClick={() => runOp("reload", async () => {
|
|
1653
1660
|
const r = await window.cicy.localTeams.reload(team.id);
|
|
1654
|
-
//
|
|
1655
|
-
|
|
1661
|
+
// 没开就不刷、也不偷偷开新标签(主人令):明确提示"窗口未打开",
|
|
1662
|
+
// 而不是替用户开一个 tab。开着才真刷(reloadTeam 走标签管理器)。
|
|
1663
|
+
if (!r?.ok && r?.error === "no_open_window") return { ok: false, error: tr("localTeams.windowNotOpen", "窗口未打开,请先点「打开」") };
|
|
1664
|
+
return r;
|
|
1656
1665
|
}, tr("localTeams.reloaded", "已刷新窗口"))}
|
|
1657
1666
|
>
|
|
1658
1667
|
{tr("localTeams.reloadWindow", "刷新窗口")}
|
|
@@ -1675,17 +1684,6 @@ function LocalTeamCard({ team, onOpen, onRename, onRefresh }) {
|
|
|
1675
1684
|
</button>
|
|
1676
1685
|
</>
|
|
1677
1686
|
)}
|
|
1678
|
-
{local && (
|
|
1679
|
-
<button
|
|
1680
|
-
type="button"
|
|
1681
|
-
data-id="LocalTeamCard-check-update"
|
|
1682
|
-
className="bcard__menu-item"
|
|
1683
|
-
disabled={checking}
|
|
1684
|
-
onClick={(e) => { e.stopPropagation(); checkUpdate(true); }}
|
|
1685
|
-
>
|
|
1686
|
-
{checking ? tr("sidecar.checking2", "检查中…") : tr("sidecar.checkUpdate", "检查更新")}
|
|
1687
|
-
</button>
|
|
1688
|
-
)}
|
|
1689
1687
|
{team.cloud_team_id && (
|
|
1690
1688
|
<button
|
|
1691
1689
|
type="button"
|