magic-agent 1.0.108 → 1.0.109

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.
@@ -191,6 +191,11 @@ textarea{font:inherit}
191
191
  padding:calc(env(safe-area-inset-top) + 20px) 20px calc(20px + env(safe-area-inset-bottom))}
192
192
  /* headlineSmall ≈ 24sp */
193
193
  .launcher__title{font-size:24px;font-weight:400;line-height:32px;margin:0;color:var(--ink)}
194
+ /* 从「切换环境 → 添加连接」进来时的返回键:已连着桌面就别把用户困在启动页 */
195
+ .launcher__back{position:absolute;top:calc(env(safe-area-inset-top) + 10px);left:12px;
196
+ display:flex;align-items:center;gap:2px;height:40px;padding:0 10px;border-radius:var(--r-full);
197
+ color:var(--primary-action);font-size:15px;font-weight:500}
198
+ .launcher__back:active{background:var(--subtle)}
194
199
  /* 说明 bodyMedium(onSurfaceVariant) */
195
200
  .launcher__desc{margin:8px 0 0;font-size:14px;line-height:20px;color:var(--muted)}
196
201
  /* OutlinedTextField:1dp 描边 · 4dp 圆角(Material3 默认) */
@@ -208,15 +213,19 @@ textarea{font:inherit}
208
213
  .btn--filled{margin-top:12px;background:var(--primary-action);color:var(--on-primary-action)}
209
214
  .btn--outlined{margin-top:8px;border:1px solid var(--border);color:var(--primary-action)}
210
215
  .btn:active{opacity:.9}
211
- /* 最近连接列表:标题 outline + 每条 OutlinedButton(标题 bodyMedium + 副标题 labelSmall) */
216
+ /* 已保存的连接(2026-10-22:由「最近连接」升级而来,可保存多台桌面并逐个删除) */
212
217
  .recent{margin-top:20px}
213
218
  .recent__head{display:flex;align-items:center}
214
219
  .recent__title{flex:1;font-size:14px;font-weight:600;color:var(--ink)}
215
220
  .recent__clear{color:var(--primary-action);font-size:13px;font-weight:600;padding:8px 12px}
216
- .recent__item{display:flex;flex-direction:column;gap:2px;width:100%;margin-top:8px;text-align:left;
221
+ .recent__row{display:flex;align-items:stretch;gap:8px;margin-top:8px}
222
+ .recent__item{display:flex;flex-direction:column;gap:2px;flex:1;min-width:0;text-align:left;
217
223
  border:1px solid var(--border);border-radius:20px;padding:10px 16px}
218
224
  .recent__t{font-size:14px;line-height:20px;color:var(--ink);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
219
225
  .recent__s{font-size:12px;line-height:16px;color:var(--muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
226
+ .recent__del{flex:0 0 auto;width:44px;border-radius:20px;border:1px solid var(--border);
227
+ display:flex;align-items:center;justify-content:center;color:var(--muted)}
228
+ .recent__del:active{background:var(--subtle);color:var(--error-text)}
220
229
 
221
230
  /* ══════════ 会话页 RemoteControlWebViewScreen + TopBar(:517-644)══════════ */
222
231
  .session{flex:1;min-height:0;display:flex;flex-direction:column}
@@ -244,6 +253,13 @@ textarea{font:inherit}
244
253
  padding:calc(env(safe-area-inset-top) + 10px) 16px 8px}
245
254
  .history__title{flex:1;min-width:0;font-size:22px;font-weight:700;color:var(--ink);
246
255
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
256
+ /* 环境切换(2026-10-22 用户:「移动端要支持环境切换 可以有多个连接」):
257
+ 任务页标题本身就是切换入口 —— 点标题弹出已保存的环境,选中即切过去。
258
+ 标题装的是**当前连接**的名字(扫码时带的 name > 桌面 health 报的 app > host)。 */
259
+ .history__title--btn{display:flex;align-items:center;gap:6px;text-align:left}
260
+ .history__title--btn>span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
261
+ .history__title-chev{flex:0 0 auto;color:var(--muted);transition:transform .15s}
262
+ .history__title--btn.is-open .history__title-chev{transform:rotate(180deg)}
247
263
  .history__disconnect{color:var(--primary-action);font-size:14px;font-weight:500;padding:8px 10px}
248
264
  .history__disconnect:disabled{color:var(--muted);opacity:.6}
249
265
  .history__list{flex:1;min-height:0;overflow-y:auto;padding:4px 10px calc(96px + env(safe-area-inset-bottom))}
@@ -369,6 +385,30 @@ textarea{font:inherit}
369
385
  .rowmenu__item:active{background:var(--subtle)}
370
386
  .rowmenu__item--danger{color:var(--error-text)}
371
387
  .rowmenu__l{flex:1;min-width:0}
388
+ /* 环境切换菜单(挂在任务页标题下):已保存的连接 + 「添加连接」。
389
+ 与会话行长按菜单同一套浮层范式(scrim 挡住下层,点空白即关)。 */
390
+ .envmenu{position:fixed;z-index:1201;width:min(320px,calc(100vw - 24px));
391
+ background:var(--raised);border:1px solid var(--border);border-radius:18px;
392
+ box-shadow:0 18px 44px -14px var(--app-shadow);padding:6px}
393
+ .envmenu__scrim{position:fixed;inset:0;z-index:1200}
394
+ .envmenu__head{padding:10px 12px 6px;font-size:12px;font-weight:700;letter-spacing:.04em;color:var(--muted)}
395
+ .envmenu__item{display:flex;align-items:center;gap:12px;width:100%;padding:11px 12px;
396
+ border-radius:12px;text-align:left}
397
+ .envmenu__item:active{background:var(--subtle)}
398
+ .envmenu__ico{flex:0 0 auto;width:38px;height:38px;border-radius:50%;background:var(--subtle);
399
+ display:flex;align-items:center;justify-content:center;color:var(--ink)}
400
+ .envmenu__col{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
401
+ .envmenu__t{display:flex;align-items:center;gap:8px;min-width:0;font-size:15px;font-weight:600;color:var(--ink)}
402
+ .envmenu__name{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
403
+ .envmenu__badge{flex:0 0 auto;padding:1px 8px;border-radius:var(--r-full);
404
+ font-size:11px;font-weight:600;line-height:17px;color:var(--primary-action);background:var(--subtle)}
405
+ .envmenu__s{font-size:12px;line-height:16px;color:var(--muted);
406
+ white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
407
+ .envmenu__check{flex:0 0 auto;color:var(--primary-action)}
408
+ .envmenu__sep{height:1px;margin:6px 4px;background:var(--border)}
409
+ .envmenu__add{display:flex;align-items:center;gap:10px;width:100%;padding:12px;border-radius:12px;
410
+ font-size:15px;font-weight:500;color:var(--primary-action);text-align:left}
411
+ .envmenu__add:active{background:var(--subtle)}
372
412
  /* toast(没有通知能力时的兜底提示) */
373
413
  .toast{position:fixed;left:50%;bottom:calc(92px + env(safe-area-inset-bottom));transform:translateX(-50%);
374
414
  max-width:82vw;padding:10px 14px;border-radius:12px;background:var(--raised);color:var(--ink);
@@ -432,6 +472,10 @@ button.setrow{font-family:inherit}
432
472
  font:13px/1.55 -apple-system,BlinkMacSystemFont,"PingFang SC",sans-serif;-webkit-appearance:none}
433
473
  .teamedit::placeholder{color:var(--faint)}
434
474
  .teamedit__acts{display:flex;gap:8px;margin-bottom:10px}
475
+ /* 团队抽屉里的分区标题(规则 / 交给谁)与行尾箭头:点开才有的配置必须看得出「这里能点」。 */
476
+ .pick__cap--team{padding:10px 4px 4px;font-size:11px;font-weight:700;letter-spacing:.04em;color:var(--muted)}
477
+ .setrow__chev{flex:0 0 auto;color:var(--faint);font-size:16px;line-height:1}
478
+ .pick__cols--team{height:min(36vh,36dvh,300px)}
435
479
  .seg{display:inline-flex;background:var(--subtle);border-radius:var(--r-full);padding:2px;gap:2px}
436
480
  .seg__b{height:28px;padding:0 12px;border-radius:var(--r-full);font-size:13px;color:var(--muted)}
437
481
  .seg__b.is-on{background:var(--raised);color:var(--ink);font-weight:600}
@@ -799,6 +843,9 @@ body.is-conv .tabfab__b{width:56px;height:56px;bottom:calc(env(safe-area-inset-b
799
843
 
800
844
  <!-- ══════════ 启动页 ══════════ -->
801
845
  <div class="launcher" id="launcher" hidden>
846
+ <button class="launcher__back" id="btnLauncherBack" type="button" hidden>
847
+ <svg width="18" height="18"><use href="#i-back"/></svg><span>返回</span>
848
+ </button>
802
849
  <h1 class="launcher__title">magic-agent · 远程控制</h1>
803
850
  <p class="launcher__desc">打开桌面配对页里的连接地址,远程继续你的会话。</p>
804
851
 
@@ -816,7 +863,7 @@ body.is-conv .tabfab__b{width:56px;height:56px;bottom:calc(env(safe-area-inset-b
816
863
 
817
864
  <div class="recent" id="recent" hidden>
818
865
  <div class="recent__head">
819
- <span class="recent__title">最近连接</span>
866
+ <span class="recent__title">已保存的连接</span>
820
867
  <button class="recent__clear" id="btnClear">清空</button>
821
868
  </div>
822
869
  <div id="recentList"></div>
@@ -826,7 +873,11 @@ body.is-conv .tabfab__b{width:56px;height:56px;bottom:calc(env(safe-area-inset-b
826
873
  <!-- ══════════ 任务列表(页头右侧:搜索 + 我的)══════════ -->
827
874
  <div class="history" id="history" hidden>
828
875
  <div class="history__top">
829
- <span class="history__title" id="historyTitle">任务</span>
876
+ <!-- 标题即环境切换入口:点开列出已保存的多台桌面(2026-10-22) -->
877
+ <button class="history__title history__title--btn" type="button" id="btnEnvSwitch" aria-label="切换环境">
878
+ <span id="historyTitle">任务</span>
879
+ <svg class="history__title-chev" width="16" height="16"><use href="#i-chev-d"/></svg>
880
+ </button>
830
881
  <span class="tasks__tools">
831
882
  <button class="topbar__icon" type="button" id="btnTaskSearch" aria-label="搜索任务"><svg width="21" height="21"><use href="#i-search"/></svg></button>
832
883
  <button class="topbar__icon" type="button" id="btnTaskMe" aria-label="我的"><svg width="21" height="21"><use href="#i-user"/></svg></button>
@@ -1300,47 +1351,211 @@ body.is-conv .tabfab__b{width:56px;height:56px;bottom:calc(env(safe-area-inset-b
1300
1351
  saveTeamPref();
1301
1352
  }
1302
1353
 
1303
- /* ══════════ 最近连接(对应 ZCode RemoteControlRecentLinksStore:最多 5 条)══════════ */
1304
- var LS_KEY = 'magic.remote.recent.v1', MAX_RECENT = 5;
1305
- function loadRecent() {
1306
- try {
1307
- var raw = localStorage.getItem(LS_KEY);
1308
- var list = raw ? JSON.parse(raw) : [];
1309
- return Array.isArray(list) ? list : [];
1310
- } catch (e) { return []; }
1354
+ /* ══════════ 环境(多连接)══════════
1355
+ 2026-10-22 用户:「移动端要支持环境切换 可以有多个连接」。
1356
+ 一台手机可以记住多台桌面(家里 / 公司 / 另一台机器),任务页标题就是切换入口。
1357
+ 键 = 地址 + 令牌:令牌是 magic-client 落盘复用、跨重启不变的机器身份(与排队队列的
1358
+ devKey 同一口径),地址会随 IP / 热点变化,所以不能只用地址。 */
1359
+ var CONN_KEY = 'magic.remote.conns.v1', CONN_MAX = 8;
1360
+ var LS_KEY = 'magic.remote.recent.v1'; // 旧版「最近连接」:只在首次升级时搬一次
1361
+ var ACTIVE_ID = '';
1362
+
1363
+ function connHost(url) {
1364
+ try { return new URL(url).host; }
1365
+ catch (e) { return String(url || '').replace(/^https?:\/\//, '').replace(/\/.*$/, ''); }
1366
+ }
1367
+ function connIdOf(t) { return String(t.url || '') + '|' + String(t.token || ''); }
1368
+ function loadConns() {
1369
+ var list;
1370
+ try { list = JSON.parse(localStorage.getItem(CONN_KEY) || '[]'); } catch (e) { list = []; }
1371
+ if (!Array.isArray(list)) list = [];
1372
+ list = list.filter(function (c) { return c && c.url; });
1373
+ if (!list.length) {
1374
+ /* 老版本只记最近 5 条连接:升级后并过来,用户不必重新扫一次码。 */
1375
+ try {
1376
+ var old = JSON.parse(localStorage.getItem(LS_KEY) || '[]');
1377
+ if (Array.isArray(old) && old.length) {
1378
+ list = old.filter(function (c) { return c && c.url; }).map(function (c) {
1379
+ return { id: connIdOf(c), url: c.url, token: c.token || '', name: c.name || '',
1380
+ host: connHost(c.url), app: '', at: c.at || 0 };
1381
+ });
1382
+ saveConns(list);
1383
+ }
1384
+ } catch (e) {}
1385
+ }
1386
+ return list;
1311
1387
  }
1312
- function saveRecent(list) { try { localStorage.setItem(LS_KEY, JSON.stringify(list)); } catch (e) {} }
1313
- function recordRecent(t) {
1314
- var list = loadRecent().filter(function (x) { return x.url !== t.url; });
1315
- list.unshift({ url: t.url, token: t.token, name: t.name, at: Date.now() });
1316
- saveRecent(list.slice(0, MAX_RECENT));
1388
+ function saveConns(list) {
1389
+ try { localStorage.setItem(CONN_KEY, JSON.stringify((list || []).slice(0, CONN_MAX))); } catch (e) {}
1390
+ }
1391
+ function findConn(id) {
1392
+ var list = loadConns();
1393
+ for (var i = 0; i < list.length; i++) if (list[i].id === id) return list[i];
1394
+ return null;
1395
+ }
1396
+ /* 记一条连接(连过就记住):扫码带的 name 与 health 报的 app 都比地址更适合当名字。 */
1397
+ function upsertConn(t) {
1398
+ var id = connIdOf(t);
1399
+ var old = findConn(id) || {};
1400
+ var list = loadConns().filter(function (c) { return c.id !== id; });
1401
+ var next = {
1402
+ id: id, url: t.url || '', token: t.token || '',
1403
+ name: (t.name != null && t.name !== '') ? t.name : (old.name || ''),
1404
+ host: connHost(t.url), app: old.app || '', at: Date.now()
1405
+ };
1406
+ list.unshift(next);
1407
+ saveConns(list);
1408
+ return next;
1409
+ }
1410
+ function removeConn(id) { saveConns(loadConns().filter(function (c) { return c.id !== id; })); }
1411
+ function clearConns() { saveConns([]); }
1412
+
1413
+ /* 当前连接:先按地址 + 令牌找;App 壳注入的连接还没落盘时给一条临时记录,
1414
+ 这样标题与菜单里仍然看得见「现在连的是谁」。 */
1415
+ function activeConn() {
1416
+ var list = loadConns(), i;
1417
+ for (i = 0; i < list.length; i++) {
1418
+ if (list[i].url === BASE && String(list[i].token || '') === String(TOKEN || '')) return list[i];
1419
+ }
1420
+ for (i = 0; i < list.length; i++) if (ACTIVE_ID && list[i].id === ACTIVE_ID) return list[i];
1421
+ if (BASE || TOKEN) {
1422
+ return { id: connIdOf({ url: BASE, token: TOKEN }), url: BASE, token: TOKEN, name: '', host: connHost(BASE), app: '' };
1423
+ }
1424
+ return null;
1425
+ }
1426
+ /* 环境显示名:扫码带的 name > 桌面 health 报的 app > host。 */
1427
+ function connLabel(c) {
1428
+ if (!c) return '';
1429
+ return c.name || c.app || c.host || connHost(c.url);
1430
+ }
1431
+ function envLabel() { return connLabel(activeConn()) || histSubtitle || ''; }
1432
+
1433
+ /* 桌面 health 回来的应用名(app)补进当前连接:它比 host 更像「这台设备是谁」。 */
1434
+ function noteActiveMeta(meta) {
1435
+ var c = activeConn();
1436
+ if (!c || !c.id) return;
1437
+ var list = loadConns(), changed = false;
1438
+ for (var i = 0; i < list.length; i++) {
1439
+ if (list[i].id !== c.id) continue;
1440
+ if (meta.app && list[i].app !== meta.app) { list[i].app = meta.app; changed = true; }
1441
+ if (!list[i].host) { list[i].host = connHost(list[i].url); changed = true; }
1442
+ }
1443
+ if (!changed) return;
1444
+ saveConns(list);
1445
+ if (!historyEl.hidden) historyTitle.textContent = envLabel() || '任务';
1317
1446
  }
1318
- function clearRecent() { saveRecent([]); }
1319
1447
 
1320
1448
  function fmtTime(ms) {
1321
1449
  try { return new Date(ms).toLocaleString(); } catch (e) { return ''; }
1322
1450
  }
1323
1451
  function renderRecent() {
1324
- var list = loadRecent();
1452
+ var list = loadConns();
1325
1453
  recentListEl.innerHTML = '';
1326
1454
  if (!list.length) { recentEl.hidden = true; return; }
1327
1455
  recentEl.hidden = false;
1328
1456
  list.forEach(function (item) {
1329
- var host = '';
1330
- try { host = new URL(item.url).host; } catch (e) { host = item.url; }
1457
+ var row = document.createElement('div');
1458
+ row.className = 'recent__row';
1331
1459
  var b = document.createElement('button');
1332
1460
  b.type = 'button';
1333
1461
  b.className = 'recent__item';
1334
- var t = document.createElement('span');
1335
- t.className = 'recent__t';
1336
- t.textContent = item.name || host;
1337
- var s = document.createElement('span');
1338
- s.className = 'recent__s';
1339
- s.textContent = host + ' · ' + fmtTime(item.at);
1340
- b.appendChild(t); b.appendChild(s);
1341
- b.onclick = function () { connect({ url: item.url, token: item.token, name: item.name, host: host }); };
1342
- recentListEl.appendChild(b);
1462
+ b.appendChild(elSpan('recent__t', connLabel(item) || item.host || item.url));
1463
+ b.appendChild(elSpan('recent__s', (item.host || connHost(item.url)) + ' · ' + fmtTime(item.at)));
1464
+ b.onclick = function () { connect({ url: item.url, token: item.token, name: item.name, host: item.host || connHost(item.url) }); };
1465
+ /* 多台机器就必然要能删:删掉只是忘掉这条记录,不打断当前连接。 */
1466
+ var del = document.createElement('button');
1467
+ del.type = 'button';
1468
+ del.className = 'recent__del';
1469
+ del.setAttribute('aria-label', '删除连接');
1470
+ del.appendChild(svgUse('i-trash', 18));
1471
+ del.onclick = function () { removeConn(item.id); renderRecent(); };
1472
+ row.appendChild(b); row.appendChild(del);
1473
+ recentListEl.appendChild(row);
1474
+ });
1475
+ }
1476
+
1477
+ /* ── 环境切换菜单(任务页标题点开)── */
1478
+ var envMenuEl = null, envScrimEl = null;
1479
+ function closeEnvMenu() {
1480
+ if (envMenuEl) { envMenuEl.remove(); envMenuEl = null; }
1481
+ if (envScrimEl) { envScrimEl.remove(); envScrimEl = null; }
1482
+ var btn = document.getElementById('btnEnvSwitch');
1483
+ if (btn) btn.classList.remove('is-open');
1484
+ }
1485
+ /* 换环境 = 换一台桌面:对话、轮询、运行态都属于旧连接,必须一起收掉。
1486
+ 排队任务按令牌分别落库(见 QUEUE_KEY),旧环境那批留在原地,切回来再接着跑。 */
1487
+ function switchEnv(c) {
1488
+ if (!c) return;
1489
+ if (c.url === BASE && String(c.token || '') === String(TOKEN || '')) {
1490
+ toast('已在「' + (connLabel(c) || '当前环境') + '」');
1491
+ return;
1492
+ }
1493
+ if (conv) { try { conv.destroy(); } catch (e) {} conv = null; }
1494
+ convEl.innerHTML = '';
1495
+ stopPoll();
1496
+ PENDING = []; ASK_WAITING = []; SERIAL = {}; QUEUE_RESTORED = false;
1497
+ SERVER_VERSION = ''; CORE_FEATURES = []; TEAM_ROLES = []; ENGINES = []; ENGINES_STALE_AT = '';
1498
+ TITLES = {}; LAST_SESSIONS = []; DW = ''; DW_READY = false; ROOT_DIRS = [];
1499
+ /* 工作空间属于某台机器的目录树:沿用旧路径会落到不存在的目录,一律回到默认。 */
1500
+ WS = '';
1501
+ saveWs();
1502
+ connect({ url: c.url, token: c.token, name: c.name, host: c.host || connHost(c.url) });
1503
+ toast('已切换到「' + (connLabel(c) || connHost(c.url)) + '」');
1504
+ }
1505
+ function openEnvMenu() {
1506
+ if (envMenuEl) { closeEnvMenu(); return; }
1507
+ var cur = activeConn();
1508
+ var list = loadConns();
1509
+ if (cur && !list.some(function (c) { return c.id === cur.id; })) list = [cur].concat(list);
1510
+ envScrimEl = document.createElement('div');
1511
+ envScrimEl.className = 'envmenu__scrim';
1512
+ envScrimEl.onclick = closeEnvMenu;
1513
+ envMenuEl = document.createElement('div');
1514
+ envMenuEl.className = 'envmenu';
1515
+ envMenuEl.appendChild(elSpan('envmenu__head', '切换环境'));
1516
+ list.forEach(function (c) {
1517
+ var active = !!(cur && c.id === cur.id);
1518
+ var b = document.createElement('button');
1519
+ b.type = 'button';
1520
+ b.className = 'envmenu__item';
1521
+ var ico = document.createElement('span');
1522
+ ico.className = 'envmenu__ico';
1523
+ ico.appendChild(svgUse('i-laptop', 20));
1524
+ b.appendChild(ico);
1525
+ var col = document.createElement('span');
1526
+ col.className = 'envmenu__col';
1527
+ var tl = document.createElement('span');
1528
+ tl.className = 'envmenu__t';
1529
+ tl.appendChild(elSpan('envmenu__name', connLabel(c) || c.host || '未命名'));
1530
+ if (c.app) tl.appendChild(elSpan('envmenu__badge', c.app));
1531
+ col.appendChild(tl);
1532
+ col.appendChild(elSpan('envmenu__s', (c.host || connHost(c.url)) + (active ? ' · 当前' : '')));
1533
+ b.appendChild(col);
1534
+ if (active) {
1535
+ var ck = document.createElement('span');
1536
+ ck.className = 'envmenu__check';
1537
+ ck.appendChild(svgUse('i-check', 18));
1538
+ b.appendChild(ck);
1539
+ }
1540
+ b.onclick = function () { closeEnvMenu(); switchEnv(c); };
1541
+ envMenuEl.appendChild(b);
1343
1542
  });
1543
+ envMenuEl.appendChild(elSpan('envmenu__sep', ''));
1544
+ var add = document.createElement('button');
1545
+ add.type = 'button';
1546
+ add.className = 'envmenu__add';
1547
+ add.appendChild(svgUse('i-plus', 18));
1548
+ add.appendChild(elSpan('', '添加连接'));
1549
+ add.onclick = function () { closeEnvMenu(); showLauncher(); };
1550
+ envMenuEl.appendChild(add);
1551
+ document.body.appendChild(envScrimEl);
1552
+ document.body.appendChild(envMenuEl);
1553
+ var btn = document.getElementById('btnEnvSwitch');
1554
+ var r = btn.getBoundingClientRect();
1555
+ var mw = envMenuEl.offsetWidth;
1556
+ envMenuEl.style.top = (r.bottom + 8) + 'px';
1557
+ envMenuEl.style.left = Math.max(12, Math.min(r.left, window.innerWidth - mw - 12)) + 'px';
1558
+ btn.classList.add('is-open');
1344
1559
  }
1345
1560
 
1346
1561
  /* ══════════ 解析 + 校验连接地址 ══════════
@@ -1377,11 +1592,16 @@ body.is-conv .tabfab__b{width:56px;height:56px;bottom:calc(env(safe-area-inset-b
1377
1592
  b.classList.toggle('is-on', b.getAttribute('data-tab') === name);
1378
1593
  });
1379
1594
  }
1380
- function showLauncher() { hideAll(); launcherEl.hidden = false; tabbarEl.hidden = true; renderRecent(); }
1595
+ function showLauncher() {
1596
+ hideAll(); launcherEl.hidden = false; tabbarEl.hidden = true; renderRecent();
1597
+ /* 已连着桌面时进来(切换环境 → 添加连接)必须有路可退,否则用户被困在启动页。 */
1598
+ var back = document.getElementById('btnLauncherBack');
1599
+ if (back) back.hidden = !(BASE || TOKEN);
1600
+ }
1381
1601
  function showHistory(address) {
1382
1602
  hideAll(); historyEl.hidden = false; tabbarEl.hidden = false; setActive('tasks');
1383
1603
  if (address != null) histSubtitle = address;
1384
- historyTitle.textContent = histSubtitle || '任务';
1604
+ historyTitle.textContent = envLabel() || '任务';
1385
1605
  loadSessions();
1386
1606
  }
1387
1607
  function showSession(sub) { hideAll(); document.body.classList.add('is-conv'); sessionEl.hidden = false; tabbarEl.hidden = false; setActive('tasks'); subtitleEl.textContent = (sub != null ? sub : histSubtitle) || ''; }
@@ -1994,17 +2214,20 @@ body.is-conv .tabfab__b{width:56px;height:56px;bottom:calc(env(safe-area-inset-b
1994
2214
  function connect(target) {
1995
2215
  BASE = target.url || '';
1996
2216
  TOKEN = target.token || '';
1997
- recordRecent(target);
2217
+ var c = upsertConn(target);
2218
+ ACTIVE_ID = c.id;
1998
2219
  showError('');
1999
- showHistory(target.host || '');
2220
+ showHistory(c.host || '');
2000
2221
  startPoll();
2001
2222
  restoreQueue(); // 上次没轮到的排队任务:连上就接着排(见 restoreQueue)
2002
2223
  }
2003
2224
  function disconnect() {
2004
2225
  stopPoll();
2226
+ closeEnvMenu();
2005
2227
  if (conv) { try { conv.destroy(); } catch (e) {} conv = null; }
2006
2228
  convEl.innerHTML = '';
2007
2229
  BASE = ''; TOKEN = '';
2230
+ ACTIVE_ID = '';
2008
2231
  SERVER_VERSION = ''; // 版本号属于「那一条连接」:断开就清掉,避免下次连到别的机器还显示旧值
2009
2232
  /* 排队任务**不跟着断连消失**(它们已经落库):只清内存里的等待态,下次连上由
2010
2233
  restoreQueue 从磁盘接回来。串行门的 FIFO 也一并重置 —— 里面的 tail 是挂在一条
@@ -2087,6 +2310,8 @@ body.is-conv .tabfab__b{width:56px;height:56px;bottom:calc(env(safe-area-inset-b
2087
2310
  /* core 能力位(feature.*):团队开关要不要显示就看它。连上桌面后才有,
2088
2311
  所以「我的」那张卡片与运行设置里的那一行都要在这里刷一次。 */
2089
2312
  CORE_FEATURES = ((a[2] && a[2].features) || []);
2313
+ /* 连接的身份(app):连着哪台设备就记到哪条连接上,环境菜单里当类型标签用。 */
2314
+ if (a[2] && a[2].app) noteActiveMeta({ app: a[2].app });
2090
2315
  /* 团队角色清单(2026-10-22):core 上报什么就画什么,客户端不硬编码角色名。
2091
2316
  老 core 没有 teamRoles → 空数组 → paintTeamPref 用内置表兜底。 */
2092
2317
  TEAM_ROLES = ((a[2] && a[2].teamRoles) || []).filter(function (r) {
@@ -3871,6 +4096,7 @@ body.is-conv .tabfab__b{width:56px;height:56px;bottom:calc(env(safe-area-inset-b
3871
4096
  b.appendChild(main);
3872
4097
  b.appendChild(hEl('span', 'setrow__v',
3873
4098
  rt.engine ? (rt.engine + (rt.model ? ' · ' + qaModelText(rt.model) : '')) : '跟随主引擎'));
4099
+ b.appendChild(hEl('span', 'setrow__chev', '›'));
3874
4100
  b.onclick = function () {
3875
4101
  openTeamRoutePicker(key,
3876
4102
  String(key).indexOf('role:') === 0 ? (label + ':适合做什么、交给谁') : '计划任务:怎么拆、交给谁',
@@ -3904,7 +4130,9 @@ body.is-conv .tabfab__b{width:56px;height:56px;bottom:calc(env(safe-area-inset-b
3904
4130
  hint.style.textAlign = 'left';
3905
4131
  body.appendChild(hint);
3906
4132
  /* 规则控件放在引擎清单**之前**:规则是这一行的主内容,引擎是「交给谁」。
3907
- 它们与探测无关,引擎清单拉失败也照样能改。 */
4133
+ 它们与探测无关,引擎清单拉失败也照样能改。分区标题必须显式给出 ——
4134
+ 用户 2026-10-22:「入口在哪里 不能配置规则」:控件在,但没标题就读不出这里是配置区。 */
4135
+ body.appendChild(hEl('div', 'pick__cap pick__cap--team', '规则'));
3908
4136
  if (isRole) body.appendChild(buildTeamRuleEditor(key));
3909
4137
  else if (kind === 'plan') body.appendChild(buildTeamSplitControls());
3910
4138
  fetchEngines().then(function (list) {
@@ -3932,10 +4160,11 @@ body.is-conv .tabfab__b{width:56px;height:56px;bottom:calc(env(safe-area-inset-b
3932
4160
  } else {
3933
4161
  tipText = '计划阶段负责把任务拆成子任务,交给更擅长规划、上下文更长的引擎更稳。';
3934
4162
  }
4163
+ body.appendChild(hEl('div', 'pick__cap pick__cap--team', '交给谁'));
3935
4164
  var tip = hEl('div', 'history__hint', tipText);
3936
4165
  tip.style.textAlign = 'left';
3937
4166
  body.appendChild(tip);
3938
- var wrap = hEl('div', 'pick__cols');
4167
+ var wrap = hEl('div', 'pick__cols pick__cols--team');
3939
4168
  var left = hEl('div', 'pick__col');
3940
4169
  var right = hEl('div', 'pick__col pick__col--r');
3941
4170
  wrap.appendChild(left); wrap.appendChild(right); body.appendChild(wrap);
@@ -3980,18 +4209,25 @@ body.is-conv .tabfab__b{width:56px;height:56px;bottom:calc(env(safe-area-inset-b
3980
4209
  });
3981
4210
  });
3982
4211
  }
3983
- /* 角色行的「规则」:一句话写清这类任务适合什么。默认值 = core 内置职责(placeholder),
3984
- 保存后覆盖它(core 的 --team-rule role:<name>=…),清空则恢复内置描述。 */
4212
+ /* 角色行的「规则」:一句话写清这类任务适合什么。**默认值直接填内置职责**(用户
4213
+ 2026-10-22:「也没看到默认规则啊」—— placeholder 在手机上几乎看不见),保存后覆盖它
4214
+ (core 的 --team-rule role:<name>=…);改回内置值等于没改,不落覆盖。 */
3985
4215
  function buildTeamRuleEditor(key) {
3986
4216
  var box = hEl('div', 'teamedit__box');
3987
4217
  var lab = hEl('div', 'history__hint', '这类任务适合什么(写进拆解提示词)');
3988
4218
  lab.style.textAlign = 'left';
3989
4219
  box.appendChild(lab);
4220
+ var sub = hEl('div', 'history__hint', '下面填的就是内置默认规则,可直接改;改完点保存才生效。');
4221
+ sub.style.textAlign = 'left';
4222
+ sub.style.padding = '0 12px 2px';
4223
+ sub.style.fontSize = '12px';
4224
+ box.appendChild(sub);
3990
4225
  var ta = hEl('textarea', 'teamedit');
3991
4226
  var name = String(key).slice(5);
3992
4227
  var info = teamRoleList().filter(function (r) { return r.name === name; })[0] || {};
3993
- ta.placeholder = info.duty || '用一句话写清这类任务承接什么';
3994
- ta.value = (TEAM.rules || {})[key] || '';
4228
+ var builtin = String(info.duty || '').trim();
4229
+ ta.placeholder = builtin || '用一句话写清这类任务承接什么';
4230
+ ta.value = String((TEAM.rules || {})[key] || '').trim() || builtin;
3995
4231
  box.appendChild(ta);
3996
4232
  var acts = hEl('div', 'teamedit__acts');
3997
4233
  var save = hEl('button', 'btn btn--filled', '保存规则');
@@ -4001,18 +4237,19 @@ body.is-conv .tabfab__b{width:56px;height:56px;bottom:calc(env(safe-area-inset-b
4001
4237
  /* 500 字上限与 core 同口径(core 会再判一次,这里是提前拦,省一次往返)。 */
4002
4238
  if (v.length > 500) { toast('规则最多 500 字'); return; }
4003
4239
  TEAM.rules = TEAM.rules || {};
4004
- if (v) TEAM.rules[key] = v; else delete TEAM.rules[key];
4240
+ if (!v || v === builtin) delete TEAM.rules[key];
4241
+ else TEAM.rules[key] = v;
4005
4242
  saveTeamPref();
4006
- toast(v ? '规则已保存' : '已恢复内置描述');
4243
+ toast((v && v !== builtin) ? '规则已保存' : '已使用内置规则');
4007
4244
  };
4008
4245
  acts.appendChild(save);
4009
4246
  var reset = hEl('button', 'btn btn--outlined', '恢复默认');
4010
4247
  reset.type = 'button';
4011
4248
  reset.onclick = function () {
4012
4249
  if (TEAM.rules) delete TEAM.rules[key];
4013
- ta.value = '';
4250
+ ta.value = builtin;
4014
4251
  saveTeamPref();
4015
- toast('已恢复内置描述');
4252
+ toast('已恢复内置规则');
4016
4253
  };
4017
4254
  acts.appendChild(reset);
4018
4255
  box.appendChild(acts);
@@ -4299,7 +4536,10 @@ body.is-conv .tabfab__b{width:56px;height:56px;bottom:calc(env(safe-area-inset-b
4299
4536
  connect(target);
4300
4537
  };
4301
4538
  addrEl.addEventListener('input', function () { showError(''); });
4302
- on('btnClear', function () { clearRecent(); renderRecent(); });
4539
+ on('btnClear', function () { clearConns(); renderRecent(); });
4540
+ /* 任务页标题 = 环境切换菜单(多连接)。已连着桌面时,从启动页进来也能退回去。 */
4541
+ on('btnEnvSwitch', openEnvMenu);
4542
+ on('btnLauncherBack', function () { if (BASE || TOKEN) showHistory(); });
4303
4543
  /* 别处起的任务要能自己冒出来(列表页可见时静默轮询;见 startListPoll 的注释) */
4304
4544
  startListPoll();
4305
4545
  on('btnList', function () { showHistory(); });
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "magic-agent",
3
- "version": "1.0.108",
3
+ "version": "1.0.109",
4
4
  "description": "专业的 agent CLI 代理工具 - 统一 claude / codebuddy / trae / llm / codex / openclaw 引擎",
5
5
  "license": "MIT",
6
6
  "bin": {