cicy-desktop 2.1.46 → 2.1.48

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.
@@ -4,8 +4,8 @@
4
4
  <meta charset="utf-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
6
  <title>CiCy Desktop</title>
7
- <script type="module" crossorigin src="./assets/index-CoLaJ3o7.js"></script>
8
- <link rel="stylesheet" crossorigin href="./assets/index-UOwEuHCy.css">
7
+ <script type="module" crossorigin src="./assets/index-DAzN_7As.js"></script>
8
+ <link rel="stylesheet" crossorigin href="./assets/index-BowhPJHl.css">
9
9
  </head>
10
10
  <body>
11
11
  <div id="root"></div>
@@ -530,7 +530,10 @@ body {
530
530
 
531
531
  .bcard__top {
532
532
  display: flex; align-items: center; justify-content: space-between;
533
- position: relative; z-index: 1;
533
+ /* Above .bcard__body (which is also position:relative z-index:1) so the ⋯
534
+ dropdown menu, which lives in here, paints OVER the card body instead of
535
+ being covered by it. */
536
+ position: relative; z-index: 5;
534
537
  }
535
538
  .bcard__pill {
536
539
  display: inline-flex; align-items: center; gap: 6px;
@@ -630,31 +633,53 @@ body {
630
633
  border-radius: 10px;
631
634
  }
632
635
 
633
- /* ---- 本地团队卡片上的 cicy-code 守护进程控制 (重启/更新/停止) ---- */
634
- .bcard__ops {
635
- display: flex;
636
- gap: 6px;
637
- margin-top: 10px;
638
- }
639
- .bcard__op {
640
- display: inline-flex; align-items: center; gap: 5px;
641
- flex: 1;
642
- justify-content: center;
643
- border: 1px solid rgba(255,255,255,.12);
644
- background: rgba(255,255,255,.03); color: #d1d5db;
645
- border-radius: 7px; padding: 5px 8px; font-size: 11.5px;
636
+ /* ---- 本地团队卡片:cicy-code 守护进程维护 (⋯ 菜单 + 新版提示) ---- */
637
+ .bcard__menuwrap { position: relative; }
638
+ .bcard__kebab {
639
+ display: inline-flex; align-items: center; justify-content: center;
640
+ width: 28px; height: 28px;
641
+ border: 1px solid transparent; border-radius: 8px;
642
+ background: transparent; color: #8b949e;
646
643
  cursor: pointer; transition: .15s;
647
644
  }
648
- .bcard__op:hover:not(:disabled) {
649
- color: #fff; background: rgba(255,255,255,.06); border-color: rgba(255,255,255,.2);
645
+ .bcard__kebab:hover:not(:disabled) {
646
+ color: #e6edf3; background: rgba(255,255,255,.06); border-color: rgba(255,255,255,.12);
650
647
  }
651
- .bcard__op:disabled { opacity: .4; cursor: default; }
652
- .bcard__op--danger { color: #f7a3a3; border-color: rgba(239,68,68,.25); }
653
- .bcard__op--danger:hover:not(:disabled) {
654
- color: #fff; background: rgba(239,68,68,.18); border-color: rgba(239,68,68,.5);
648
+ .bcard__kebab:disabled { opacity: .5; cursor: default; }
649
+ /* amber dot on the when an update is available */
650
+ .bcard__kebab.has-dot { color: #e6edf3; }
651
+ .bcard__kebab.has-dot::after {
652
+ content: ""; position: absolute; top: 3px; right: 3px;
653
+ width: 7px; height: 7px; border-radius: 50%;
654
+ background: #f59e0b; box-shadow: 0 0 0 2px #0f1115;
655
+ }
656
+ .bcard__menu {
657
+ position: absolute; top: 32px; right: 0; z-index: 20;
658
+ min-width: 150px; padding: 5px;
659
+ background: #1b2027; border: 1px solid rgba(255,255,255,.12);
660
+ border-radius: 10px; box-shadow: 0 10px 30px rgba(0,0,0,.45);
661
+ display: flex; flex-direction: column; gap: 2px;
662
+ }
663
+ .bcard__menu-item {
664
+ text-align: left; width: 100%;
665
+ border: none; background: transparent; color: #d1d5db;
666
+ border-radius: 7px; padding: 7px 10px; font-size: 12.5px;
667
+ cursor: pointer; transition: .12s;
668
+ }
669
+ .bcard__menu-item:hover { background: rgba(255,255,255,.07); color: #fff; }
670
+ .bcard__menu-item.is-accent { color: #fbbf24; font-weight: 600; }
671
+ .bcard__menu-item.is-accent:hover { background: rgba(245,158,11,.15); color: #fcd34d; }
672
+ .bcard__menu-item.is-danger { color: #f7a3a3; }
673
+ .bcard__menu-item.is-danger:hover { background: rgba(239,68,68,.16); color: #fff; }
674
+ /* "新版 vX.Y.Z" chip on the card face */
675
+ .bcard__chip--new {
676
+ color: #fbbf24;
677
+ background: rgba(245,158,11,.12);
678
+ border-color: rgba(245,158,11,.3);
655
679
  }
656
680
  .bcard__opmsg {
657
- margin-top: 8px;
658
- font-size: 11px; color: #9ca3af;
681
+ display: flex; align-items: center; gap: 6px;
682
+ margin-top: 9px;
683
+ font-size: 11.5px; color: #9ca3af;
659
684
  word-break: break-word;
660
685
  }
@@ -439,28 +439,58 @@ function LocalTeamCard({ team, onOpen, onRename, onRefresh }) {
439
439
  if (onRename && next && next !== team.name) await onRename(team.id, next);
440
440
  };
441
441
 
442
- // Lifecycle of the local cicy-code daemon (the :8008 sidecar this card
443
- // represents): 重启 / 更新 / 停止, inline on the card itself. Only shown when
444
- // the bridge exists (desktop build that owns the daemon).
445
- const [busy, setBusy] = useState(""); // "" | "restart" | "update" | "stop"
446
- const [opMsg, setOpMsg] = useState("");
442
+ // The local cicy-code daemon (the :8008 sidecar) that backs this team:
443
+ // 启动 / 重启 / 更新 / 停止. 打开 stays the one primary action daemon
444
+ // maintenance lives in a menu so it never competes for attention. Only on
445
+ // a desktop build whose bridge owns the daemon.
447
446
  const hasOps = !!window.cicy?.sidecar?.restart;
448
- const runOp = async (e, kind, fn, doneText) => {
449
- e.stopPropagation();
447
+ const running = team.status === "running";
448
+ const [busy, setBusy] = useState(""); // "" | start | restart | update | stop
449
+ const [opMsg, setOpMsg] = useState("");
450
+ const [menuOpen, setMenuOpen] = useState(false);
451
+ const [latest, setLatest] = useState(null); // newest cicy-code on the registry
452
+ const menuWrap = useRef(null);
453
+
454
+ // Look up the newest cicy-code once so we surface 更新 only when one actually
455
+ // exists (no nagging when current). Renderer-side via cloud.fetch — main
456
+ // proxies it, dodging CORS; no extra IPC needed.
457
+ useEffect(() => {
458
+ if (!hasOps || !window.cicy?.cloud?.fetch) return;
459
+ let alive = true;
460
+ window.cicy.cloud
461
+ .fetch("https://registry.npmmirror.com/cicy-code/latest")
462
+ .then((r) => { if (alive && r?.ok) { try { setLatest(JSON.parse(r.body)?.version || null); } catch {} } })
463
+ .catch(() => {});
464
+ return () => { alive = false; };
465
+ }, [hasOps]);
466
+
467
+ const updateAvailable = !!(latest && team.version && cmpVer(latest, team.version) > 0);
468
+ const showMenu = hasOps && (running || updateAvailable);
469
+
470
+ useEffect(() => {
471
+ if (!menuOpen) return;
472
+ const onDoc = (e) => { if (menuWrap.current && !menuWrap.current.contains(e.target)) setMenuOpen(false); };
473
+ document.addEventListener("mousedown", onDoc);
474
+ return () => document.removeEventListener("mousedown", onDoc);
475
+ }, [menuOpen]);
476
+
477
+ const runOp = async (kind, fn, doneText) => {
478
+ setMenuOpen(false);
450
479
  if (busy) return;
451
480
  setBusy(kind); setOpMsg("");
452
481
  try {
453
482
  const r = await fn();
454
483
  setOpMsg(r?.ok
455
484
  ? (r.warning ? `${doneText}(${r.warning})` : doneText)
456
- : tr("sidecar.failed", "失败") + (r?.error ? `: ${r.error}` : ""));
485
+ : (tr("sidecar.failed", "操作失败") + (r?.error ? `: ${r.error}` : "")));
457
486
  } catch (err) {
458
- setOpMsg(tr("sidecar.failed", "失败") + `: ${err?.message || err}`);
487
+ setOpMsg(tr("sidecar.failed", "操作失败") + `: ${err?.message || err}`);
459
488
  } finally {
460
489
  setBusy("");
461
490
  onRefresh?.(); // re-probe so the status dot/chip catches up
462
491
  }
463
492
  };
493
+ const BUSY_LABEL = { start: "启动中…", restart: "重启中…", update: "更新中…", stop: "停止中…" };
464
494
  return (
465
495
  <div data-id="LocalTeamCard" className={`bcard bcard--local${tone === "ok" ? " bcard--online" : ""}`}>
466
496
  <div className="bcard__accent" />
@@ -469,6 +499,54 @@ function LocalTeamCard({ team, onOpen, onRename, onRefresh }) {
469
499
  <span className="bcard__dot" data-tone={tone} />
470
500
  <LaptopIcon />
471
501
  </div>
502
+ {showMenu && (
503
+ <div className="bcard__menuwrap" ref={menuWrap} onClick={(e) => e.stopPropagation()}>
504
+ <button
505
+ type="button"
506
+ data-id="LocalTeamCard-menu-btn"
507
+ className={`bcard__kebab${updateAvailable ? " has-dot" : ""}`}
508
+ title={tr("localTeams.manage", "管理本地 cicy-code")}
509
+ disabled={!!busy}
510
+ onClick={() => setMenuOpen((v) => !v)}
511
+ >
512
+ {busy ? <Spinner /> : <KebabIcon />}
513
+ </button>
514
+ {menuOpen && (
515
+ <div className="bcard__menu" data-id="LocalTeamCard-menu" role="menu">
516
+ {updateAvailable && (
517
+ <button
518
+ type="button"
519
+ data-id="LocalTeamCard-update"
520
+ className="bcard__menu-item is-accent"
521
+ onClick={() => runOp("update", () => window.cicy.sidecar.update(), tr("sidecar.updated", "已更新到最新"))}
522
+ >
523
+ {tr("sidecar.updateTo", "更新到")} v{latest}
524
+ </button>
525
+ )}
526
+ {running && (
527
+ <>
528
+ <button
529
+ type="button"
530
+ data-id="LocalTeamCard-restart"
531
+ className="bcard__menu-item"
532
+ onClick={() => runOp("restart", () => window.cicy.sidecar.restart(), tr("sidecar.restarted", "已重启"))}
533
+ >
534
+ {tr("sidecar.restart", "重启")}
535
+ </button>
536
+ <button
537
+ type="button"
538
+ data-id="LocalTeamCard-stop"
539
+ className="bcard__menu-item is-danger"
540
+ onClick={() => runOp("stop", () => window.cicy.sidecar.stop(), tr("sidecar.stoppedDone", "已停止"))}
541
+ >
542
+ {tr("sidecar.stop", "停止")}
543
+ </button>
544
+ </>
545
+ )}
546
+ </div>
547
+ )}
548
+ </div>
549
+ )}
472
550
  </div>
473
551
  <div className="bcard__body">
474
552
  {editing ? (
@@ -500,56 +578,58 @@ function LocalTeamCard({ team, onOpen, onRename, onRefresh }) {
500
578
  <div className="bcard__meta">
501
579
  <span className="bcard__chip">{statusInfo.label}</span>
502
580
  {team.version && <span className="bcard__chip">v{team.version}</span>}
503
- </div>
504
- {hasOps && (
505
- <div className="bcard__ops" data-id="LocalTeamCard-ops" onClick={(e) => e.stopPropagation()}>
506
- <button
507
- type="button"
508
- data-id="LocalTeamCard-restart"
509
- className="bcard__op"
510
- disabled={!!busy}
511
- title={tr("sidecar.restartHint", "重启本地 cicy-code")}
512
- onClick={(e) => runOp(e, "restart", () => window.cicy.sidecar.restart(), tr("sidecar.restarted", "已重启"))}
513
- >
514
- {busy === "restart" ? <Spinner /> : null}{tr("sidecar.restart", "重启")}
515
- </button>
516
- <button
517
- type="button"
518
- data-id="LocalTeamCard-update"
519
- className="bcard__op"
520
- disabled={!!busy}
521
- title={tr("sidecar.updateHint", "更新到最新版并重启")}
522
- onClick={(e) => runOp(e, "update", () => window.cicy.sidecar.update(), tr("sidecar.updated", "已更新到最新"))}
523
- >
524
- {busy === "update" ? <Spinner /> : null}{tr("sidecar.update", "更新")}
525
- </button>
526
- <button
527
- type="button"
528
- data-id="LocalTeamCard-stop"
529
- className="bcard__op bcard__op--danger"
530
- disabled={!!busy || team.status !== "running"}
531
- title={tr("sidecar.stopHint", "停止本地 cicy-code")}
532
- onClick={(e) => runOp(e, "stop", () => window.cicy.sidecar.stop(), tr("sidecar.stoppedDone", "已停止"))}
581
+ {updateAvailable && (
582
+ <span
583
+ className="bcard__chip bcard__chip--new"
584
+ data-id="LocalTeamCard-newbadge"
585
+ title={`${tr("sidecar.updateTo", "更新到")} v${latest}`}
533
586
  >
534
- {busy === "stop" ? <Spinner /> : null}{tr("sidecar.stop", "停止")}
535
- </button>
587
+ {tr("sidecar.newVersion", "新版")} v{latest}
588
+ </span>
589
+ )}
590
+ </div>
591
+ {(busy || opMsg) && (
592
+ <div className="bcard__opmsg" data-id="LocalTeamCard-opmsg">
593
+ {busy ? <><Spinner />{BUSY_LABEL[busy] || tr("sidecar.working", "处理中…")}</> : opMsg}
536
594
  </div>
537
595
  )}
538
- {opMsg && <div className="bcard__opmsg" data-id="LocalTeamCard-opmsg">{opMsg}</div>}
539
596
  </div>
540
- <button
541
- type="button"
542
- className="bcard__cta"
543
- onClick={onOpen}
544
- disabled={team.status !== "running"}
545
- >
546
- <ArrowIcon />
547
- <span>{team.status === "running" ? "打开" : statusInfo.cta}</span>
548
- </button>
597
+ {running ? (
598
+ <button type="button" className="bcard__cta" onClick={onOpen}>
599
+ <ArrowIcon />
600
+ <span>{tr("localTeams.open", "打开")}</span>
601
+ </button>
602
+ ) : hasOps ? (
603
+ <button
604
+ type="button"
605
+ className="bcard__cta"
606
+ data-id="LocalTeamCard-start"
607
+ disabled={!!busy}
608
+ onClick={() => runOp("start", () => window.cicy.sidecar.start(), tr("sidecar.started", "已启动"))}
609
+ >
610
+ {busy === "start" ? <Spinner /> : <ArrowIcon />}
611
+ <span>{tr("sidecar.start", "启动")}</span>
612
+ </button>
613
+ ) : (
614
+ <button type="button" className="bcard__cta" onClick={onOpen} disabled>
615
+ <ArrowIcon />
616
+ <span>{statusInfo.cta}</span>
617
+ </button>
618
+ )}
549
619
  </div>
550
620
  );
551
621
  }
552
622
 
623
+ // Compare dotted versions: >0 if a newer than b, <0 older, 0 equal.
624
+ function cmpVer(a, b) {
625
+ const pa = String(a).split("."), pb = String(b).split(".");
626
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
627
+ const d = (parseInt(pa[i], 10) || 0) - (parseInt(pb[i], 10) || 0);
628
+ if (d) return d > 0 ? 1 : -1;
629
+ }
630
+ return 0;
631
+ }
632
+
553
633
  const LOCAL_STATUS = {
554
634
  running: { tone: "ok", label: "running", cta: "打开" },
555
635
  stopped: { tone: "off", label: "stopped", cta: "未运行" },
@@ -648,6 +728,15 @@ function LaptopIcon() {
648
728
  </svg>
649
729
  );
650
730
  }
731
+ function KebabIcon() {
732
+ return (
733
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden>
734
+ <circle cx="12" cy="5" r="1.7" />
735
+ <circle cx="12" cy="12" r="1.7" />
736
+ <circle cx="12" cy="19" r="1.7" />
737
+ </svg>
738
+ );
739
+ }
651
740
  function GlobeIcon() {
652
741
  return (
653
742
  <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">