cicy-desktop 2.1.85 → 2.1.87

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.
@@ -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-xHkT3-tl.js"></script>
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.reload();
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) => lt.reloadTeam(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 || {}));
@@ -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
- const DEFAULT_PORT = Number(process.env.CICY_CODE_PORT || 8008);
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 < 120; 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: "cicy-code 未在 60s 内启动" }); return c; }
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");
@@ -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) {
@@ -5,7 +5,8 @@
5
5
  // installed() → 磁盘 binary 版本(localbin manifest,诊断用)
6
6
  const http = require("http");
7
7
 
8
- const DEFAULT_PORT = 8008;
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
- window.tabAPI.onState((s) => { state = s || { tabs: [], nav: {} }; render(); });
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
- // not open yet → open it (still a "refresh" of the team)
1655
- return (!r?.ok && r?.error === "no_open_window") ? window.cicy.localTeams.open(team.id) : r;
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"