cicy-desktop 2.1.45 → 2.1.47

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.
@@ -1 +1 @@
1
- *{box-sizing:border-box}html,body,#root{margin:0;height:100%}body{font-family:-apple-system,BlinkMacSystemFont,PingFang SC,Segoe UI,Roboto,Helvetica Neue,sans-serif;background:#07080c;color:#e5e7eb;-webkit-font-smoothing:antialiased}.shell{position:relative;min-height:100vh;display:flex;align-items:center;justify-content:center;overflow:hidden;-webkit-app-region:drag}.shell--app{align-items:stretch;justify-content:stretch;flex-direction:row;background:linear-gradient(180deg,#0b0d13,#07080c);overflow:hidden;-webkit-app-region:no-drag}.shell__left{flex:1 1 auto;min-width:0;display:flex;flex-direction:column;overflow:auto}.helper-aside{flex:0 0 auto;position:relative;background:#0f1115;border-left:1px solid rgba(255,255,255,.06);display:flex;flex-direction:column;min-width:320px;z-index:1}.helper-aside,.helper-aside *,.shell--app .main{-webkit-app-region:no-drag}.shell--app .topbar{-webkit-app-region:drag}.shell--app .topbar .user-chip,.shell--app .topbar .user-chip *,.shell--app .topbar .btn-ghost{-webkit-app-region:no-drag}.glow{position:absolute;top:-40%;right:-40%;bottom:-40%;left:-40%;z-index:0;pointer-events:none;background:radial-gradient(closest-side,rgba(91,141,247,.2),transparent 60%),radial-gradient(closest-side at 30% 70%,rgba(167,139,250,.13),transparent 65%);filter:blur(30px)}.card{-webkit-app-region:no-drag;position:relative;z-index:1;width:380px;padding:36px 36px 28px;background:linear-gradient(180deg,#141820d9,#0d1016d9);border:1px solid rgba(255,255,255,.06);border-radius:16px;box-shadow:0 1px #ffffff0a inset,0 30px 60px #0006;-webkit-backdrop-filter:blur(18px);backdrop-filter:blur(18px);display:flex;flex-direction:column;align-items:center;gap:18px}.brand{display:flex;align-items:center;gap:12px;align-self:stretch}.brand-mark{width:40px;height:40px;border-radius:12px;display:grid;place-items:center;background:linear-gradient(135deg,#5b8df7,#a78bfa);box-shadow:0 8px 20px #5b8df759}.brand-mark.sm{width:28px;height:28px;border-radius:8px;box-shadow:none}.brand-mark.sm svg{width:16px;height:16px}.brand-text{line-height:1.2}.brand-name{font-weight:600;font-size:15px}.brand-sub{font-size:12px;color:#9ca3af;margin-top:2px}.tagline{margin:4px 0 0;color:#c7cdd6;font-size:13.5px;text-align:center;line-height:1.55}.btn-primary{-webkit-app-region:no-drag;margin-top:4px;-webkit-appearance:none;-moz-appearance:none;appearance:none;width:100%;height:42px;display:inline-flex;align-items:center;justify-content:center;gap:8px;background:linear-gradient(180deg,#5b8df7,#4570d8);color:#fff;border:0;border-radius:10px;font-size:14px;font-weight:500;letter-spacing:.2px;cursor:pointer;box-shadow:0 1px #ffffff2e inset,0 -1px #0000002e inset,0 10px 22px #5b8df747;transition:transform 80ms ease,filter .12s ease}.btn-primary:hover{filter:brightness(1.08)}.btn-primary:active{transform:translateY(1px)}.btn-primary:disabled{filter:grayscale(.4) brightness(.7);cursor:default}.btn-ghost{-webkit-app-region:no-drag;-webkit-appearance:none;-moz-appearance:none;appearance:none;background:transparent;color:#9ca3af;border:1px solid rgba(255,255,255,.08);border-radius:8px;padding:6px 14px;font-size:12.5px;cursor:pointer;transition:color .12s ease,border-color .12s ease,background .12s ease}.btn-ghost.sm{padding:4px 10px;font-size:11.5px}.btn-ghost:hover{color:#e5e7eb;background:#ffffff0a;border-color:#ffffff24}.btn-ghost:disabled{opacity:.4;cursor:default}.hint{margin:0;font-size:11.5px;color:#6b7280}.spinner-row{display:inline-flex;align-items:center;gap:8px;color:#c7cdd6;font-size:13px}.spin{animation:spin 1s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.error{width:100%;font-size:12px;color:#fca5a5;background:#ef444414;border:1px solid rgba(239,68,68,.18);padding:8px 12px;border-radius:8px;text-align:center;line-height:1.5}.topbar{-webkit-app-region:drag;position:sticky;top:0;z-index:10;display:flex;align-items:center;justify-content:space-between;padding:14px 24px 12px;background:#08090e99;border-bottom:1px solid rgba(255,255,255,.05);-webkit-backdrop-filter:blur(14px);backdrop-filter:blur(14px)}[data-platform=darwin][data-fullscreen="0"] .topbar{padding-left:84px}.brand-mini{display:inline-flex;align-items:center;gap:10px}.brand-mini .brand-name{font-size:14px}.user-chip{-webkit-app-region:no-drag;display:inline-flex;align-items:center;gap:10px}.welcome{font-size:12px;color:#34d399;padding:4px 10px;border-radius:999px;background:#10b9811a;border:1px solid rgba(16,185,129,.25);animation:fadein .2s ease}@keyframes fadein{0%{opacity:0;transform:translateY(-2px)}to{opacity:1;transform:none}}.avatar{width:26px;height:26px;border-radius:50%;display:grid;place-items:center;background:linear-gradient(135deg,#5b8df7,#a78bfa);font-size:12px;font-weight:600;color:#fff}.user-name{font-size:13px;color:#c7cdd6}.glow--app{inset:-10% -10% auto -10%;height:50vh;background:radial-gradient(closest-side at 75% 0%,rgba(91,141,247,.18),transparent 65%),radial-gradient(closest-side at 20% 10%,rgba(167,139,250,.1),transparent 60%);filter:blur(40px)}.main{position:relative;z-index:1;padding:22px 32px 48px;width:100%;display:flex;flex-direction:column;gap:16px}.app__tabs{display:inline-flex;align-items:center;gap:4px;padding:4px;background:#1418208c;border:1px solid rgba(255,255,255,.06);border-radius:10px;align-self:flex-start;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}.app__tab{-webkit-app-region:no-drag;-webkit-appearance:none;-moz-appearance:none;appearance:none;background:transparent;color:#9ca3af;border:0;border-radius:7px;padding:6px 14px;font-size:12.5px;font-weight:500;letter-spacing:.15px;cursor:pointer;display:inline-flex;align-items:center;gap:6px;transition:color .12s ease,background .12s ease}.app__tab:hover{color:#e5e7eb;background:#ffffff0a}.app__tab.is-active{color:#fff;background:#5b8df72e;box-shadow:0 0 0 1px #5b8df74d inset}.app__tab-count{font-size:10.5px;color:#9ca3af;background:#ffffff0f;padding:1px 6px;border-radius:999px;min-width:18px;text-align:center}.app__tab.is-active .app__tab-count{background:#5b8df74d;color:#fff}.app__grid{display:grid;grid-template-columns:repeat(auto-fill,200px);gap:14px}.add-card{-webkit-app-region:no-drag;-webkit-appearance:none;-moz-appearance:none;appearance:none;width:200px;height:200px;background:transparent;border:1.5px dashed rgba(255,255,255,.12);border-radius:14px;color:#9ca3af;cursor:pointer;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;transition:border-color .16s ease,color .16s ease,background .16s ease}.add-card:hover{border-color:#5b8df780;color:#e5e7eb;background:#5b8df70d}.add-card__plus{width:36px;height:36px;border-radius:50%;display:grid;place-items:center;background:#ffffff0a;font-size:22px;font-weight:300;color:inherit}.add-card__label{font-size:13px;font-weight:500}.bcard--helper{background:linear-gradient(160deg,#5b8df729,#a78bfa24 60%,#1418208c);border-color:#a78bfa40}.bcard--helper .bcard__accent{background:linear-gradient(90deg,#5b8df7,#a78bfa);opacity:1}.bcard--helper:hover{border-color:#a78bfa73}.bcard__pill--helper{background:#a78bfa2e;border-color:#a78bfa4d;color:#c4b5fd;font-size:11px;font-weight:600;letter-spacing:.2px}.bcard__helper-icon{font-size:12px;filter:grayscale(.2)}.bcard__badge--free{background:#34d39926;color:#34d399;border:1px solid rgba(52,211,153,.3)}.bcard__badge--trial{background:#fbbf2426;color:#fcd34d;border:1px solid rgba(251,191,36,.3);font-size:9.5px;letter-spacing:.3px;padding:2px 7px}.bcard__badge--local{background:#5b8df72e;color:#a5c4ff;border:1px solid rgba(91,141,247,.35);font-size:9.5px;letter-spacing:.3px;padding:2px 7px}.bcard--helper .bcard__name{color:#f3f4f6}.bcard__desc{margin:4px 0 0;font-size:11.5px;line-height:1.5;color:#c7cdd6;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.bcard__fineprint{margin:4px 0 0;font-size:10.5px;color:#9ca3af;opacity:.8}.bcard__cta--helper{background:linear-gradient(90deg,#5b8df7,#a78bfa);box-shadow:0 6px 18px -4px #a78bfa73}.bcard__cta--helper:hover{filter:brightness(1.1)}.helper-aside__top{display:flex;align-items:center;justify-content:space-between;padding:12px 14px 10px;border-bottom:1px solid rgba(255,255,255,.05)}.helper-aside__title{font-size:12.5px;font-weight:600;color:#c7cdd6}.helper-aside__close{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:transparent;border:0;color:#6b7280;font-size:20px;line-height:1;width:26px;height:26px;border-radius:6px;cursor:pointer;transition:color .12s ease,background .12s ease}.helper-aside__close:hover{color:#e5e7eb;background:#ffffff0f}.helper-modal__backdrop{position:absolute;top:0;right:0;bottom:0;left:0;display:flex;align-items:center;justify-content:center;background:transparent;z-index:20;animation:fadein .14s ease;padding:16px;pointer-events:auto}.helper-modal{width:min(360px,100%);background:linear-gradient(180deg,#1f2733,#1a2029);border:1px solid rgba(255,255,255,.1);border-radius:12px;padding:20px 22px 16px;color:#e5e7eb;box-shadow:0 24px 48px -12px #0009,0 1px #ffffff14 inset;animation:fadein .18s ease}.helper-modal__title{font-size:14px;font-weight:600;color:#f3f4f6;margin-bottom:8px}.helper-modal__desc{font-size:12.5px;line-height:1.6;color:#b6bcc6;margin-bottom:18px}.helper-modal__desc code{font-family:ui-monospace,SFMono-Regular,SF Mono,Menlo,monospace;font-size:11.5px;padding:1px 6px;background:#ffffff14;border:1px solid rgba(255,255,255,.08);border-radius:4px;color:#e7eaef}.helper-modal__actions{display:flex;align-items:center;justify-content:flex-end;gap:8px}.helper-modal__btn{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:#ffffff0f;color:#d2d6dd;border:1px solid rgba(255,255,255,.1);border-radius:7px;padding:7px 14px;font-size:12.5px;font-weight:500;cursor:pointer;transition:background .12s ease,color .12s ease,transform 80ms ease}.helper-modal__btn:hover{background:#ffffff1a;color:#fff}.helper-modal__btn:active{transform:translateY(1px)}.helper-modal__btn--ghost{background:transparent;border-color:transparent;color:#ffffff8c;margin-right:auto;padding-left:4px;padding-right:4px}.helper-modal__btn--ghost:hover{background:transparent;color:#ffffffd9}.helper-modal__btn--primary{background:linear-gradient(180deg,#5b8df7,#4570d8);border-color:#ffffff2e;color:#fff}.helper-modal__btn--primary:hover{background:linear-gradient(180deg,#6c9af9,#5a82e0)}.helper-modal__btn:disabled{filter:grayscale(.4) brightness(.7);cursor:default}.helper-placeholder{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;padding:32px 40px;text-align:center;color:#9ca3af}.helper-placeholder__mark{font-size:48px;line-height:1;filter:grayscale(.2);opacity:.85}.helper-placeholder__title{margin:0;font-size:16px;font-weight:600;color:#e5e7eb}.helper-placeholder__sub{margin:0;font-size:12.5px;line-height:1.6;color:#9ca3af;max-width:320px}.helper-placeholder__note{margin-top:8px;font-size:11px;color:#6b7280;padding:4px 10px;background:#ffffff08;border:1px solid rgba(255,255,255,.06);border-radius:999px}.helper-placeholder__note code{font-family:ui-monospace,SFMono-Regular,SF Mono,Menlo,monospace;font-size:11px;color:#c7cdd6}.section{background:linear-gradient(180deg,#141820b8,#0d1016b8);border:1px solid rgba(255,255,255,.06);border-radius:14px;box-shadow:0 1px #ffffff0a inset,0 14px 30px #00000052;-webkit-backdrop-filter:blur(14px);backdrop-filter:blur(14px);padding:18px 18px 16px}.section-head{display:flex;align-items:center;gap:8px;padding:0 4px 14px;border-bottom:1px solid rgba(255,255,255,.04);margin-bottom:14px}.section-icon{font-size:14px;opacity:.8}.section-head h2{margin:0;font-size:13.5px;font-weight:600;color:#e5e7eb;letter-spacing:.2px}.section-sub{font-size:12px;color:#6b7280}.section-body{display:flex;flex-direction:column;gap:10px}.grid{display:grid;grid-template-columns:repeat(auto-fill,200px);gap:14px}.bcard{position:relative;width:200px;height:200px;background:#1418208c;border:1px solid rgba(255,255,255,.07);border-radius:14px;padding:16px;display:flex;flex-direction:column;overflow:hidden;transition:transform .18s ease,border-color .18s ease,box-shadow .18s ease;--brand: #5b8df7;--accent-cloud: #f59e0b}.bcard:hover{transform:translateY(-2px);border-color:#ffffff24;box-shadow:0 12px 32px -10px #00000073,0 1px #ffffff0a inset}.bcard__accent{position:absolute;top:0;left:0;right:0;height:3px;background:var(--brand);opacity:.9}.bcard--cloud .bcard__accent{background:var(--accent-cloud)}.bcard--online:before{content:"";position:absolute;bottom:-40%;right:-20%;width:140%;height:100%;background:radial-gradient(circle at center,rgba(91,141,247,.22) 0%,transparent 60%);pointer-events:none;opacity:.55}.bcard--cloud.bcard--online:before{background:radial-gradient(circle at center,rgba(245,158,11,.22) 0%,transparent 60%)}.bcard__top{display:flex;align-items:center;justify-content:space-between;position:relative;z-index:1}.bcard__pill{display:inline-flex;align-items:center;gap:6px;padding:4px 8px;background:#08090eb3;border:1px solid rgba(255,255,255,.07);border-radius:999px;color:#9ca3af}.bcard__pill svg{display:block}.bcard__dot{width:7px;height:7px;border-radius:50%;background:#6b7280;flex-shrink:0}.bcard__dot[data-tone=ok]{background:#34d399;box-shadow:0 0 0 2px #34d39940}.bcard__dot[data-tone=off]{background:#6b7280}.bcard__dot[data-tone=warn]{background:#fbbf24;box-shadow:0 0 0 2px #fbbf2440}.bcard__dot[data-tone=err]{background:#f87171;box-shadow:0 0 0 2px #f8717140}.bcard__badge{display:inline-flex;align-items:center;padding:2px 8px;border-radius:999px;background:#fbbf241f;color:#fbbf24;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.5px}.bcard__body{flex:1;display:flex;flex-direction:column;gap:4px;padding-top:12px;padding-bottom:12px;position:relative;z-index:1;min-width:0}.bcard__name{margin:0;font-size:17px;font-weight:700;color:#e5e7eb;letter-spacing:-.015em;line-height:1.25;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.bcard__host{font-family:ui-monospace,SFMono-Regular,SF Mono,Menlo,monospace;font-size:11px;color:#9ca3af;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.bcard__meta{display:flex;flex-wrap:wrap;gap:4px;margin-top:4px}.bcard__chip{font-size:10.5px;color:#9ca3af;background:#ffffff08;border:1px solid rgba(255,255,255,.06);padding:2px 7px;border-radius:999px}.bcard__cta{position:relative;z-index:1;display:inline-flex;align-items:center;justify-content:center;gap:8px;width:100%;height:38px;border:0;border-radius:9px;background:var(--brand);color:#fff;font-size:13px;font-weight:600;letter-spacing:.1px;cursor:pointer;transition:transform 80ms ease,background .16s ease,box-shadow .16s ease,filter .16s ease;box-shadow:0 4px 12px -2px #5b8df773}.bcard--cloud .bcard__cta{background:var(--accent-cloud);box-shadow:0 4px 12px -2px #f59e0b73}.bcard__cta:hover{filter:brightness(1.08)}.bcard__cta:active{transform:translateY(1px)}.bcard__cta:disabled{background:#ffffff08;color:#6b7280;cursor:not-allowed;box-shadow:none;border:1px solid rgba(255,255,255,.06)}.empty{font-size:12.5px;color:#6b7280;padding:14px 16px;background:#ffffff05;border:1px dashed rgba(255,255,255,.08);border-radius:10px}.sidecar-bar{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-bottom:12px;padding:8px 12px;background:#1418208c;border:1px solid rgba(255,255,255,.08);border-radius:10px}.sidecar-bar__label{display:flex;align-items:center;gap:8px;font-size:12.5px;color:#c7cdd6}.sidecar-bar__title{font-weight:600}.sidecar-bar__state{color:#9ca3af;font-size:11.5px}.sidecar-dot{width:8px;height:8px;border-radius:50%;background:#6b7280;flex:none}.sidecar-dot.is-on{background:#10b981;box-shadow:0 0 0 3px #10b98126}.sidecar-dot.is-off{background:#ef4444;box-shadow:0 0 0 3px #ef44441f}.sidecar-dot.is-unknown{background:#6b7280}.sidecar-bar__actions{display:flex;gap:6px;margin-left:auto}.sidecar-btn{display:inline-flex;align-items:center;gap:6px;border:1px solid rgba(255,255,255,.12);background:#ffffff08;color:#d1d5db;border-radius:8px;padding:5px 12px;font-size:11.5px;cursor:pointer;transition:.15s}.sidecar-btn:hover:not(:disabled){color:#fff;background:#ffffff0f;border-color:#fff3}.sidecar-btn:disabled{opacity:.4;cursor:default}.sidecar-btn--danger{color:#f7a3a3;border-color:#ef444440}.sidecar-btn--danger:hover:not(:disabled){color:#fff;background:#ef44442e;border-color:#ef444480}.sidecar-bar__msg{flex-basis:100%;font-size:11.5px;color:#9ca3af}
1
+ *{box-sizing:border-box}html,body,#root{margin:0;height:100%}body{font-family:-apple-system,BlinkMacSystemFont,PingFang SC,Segoe UI,Roboto,Helvetica Neue,sans-serif;background:#07080c;color:#e5e7eb;-webkit-font-smoothing:antialiased}.shell{position:relative;min-height:100vh;display:flex;align-items:center;justify-content:center;overflow:hidden;-webkit-app-region:drag}.shell--app{align-items:stretch;justify-content:stretch;flex-direction:row;background:linear-gradient(180deg,#0b0d13,#07080c);overflow:hidden;-webkit-app-region:no-drag}.shell__left{flex:1 1 auto;min-width:0;display:flex;flex-direction:column;overflow:auto}.helper-aside{flex:0 0 auto;position:relative;background:#0f1115;border-left:1px solid rgba(255,255,255,.06);display:flex;flex-direction:column;min-width:320px;z-index:1}.helper-aside,.helper-aside *,.shell--app .main{-webkit-app-region:no-drag}.shell--app .topbar{-webkit-app-region:drag}.shell--app .topbar .user-chip,.shell--app .topbar .user-chip *,.shell--app .topbar .btn-ghost{-webkit-app-region:no-drag}.glow{position:absolute;top:-40%;right:-40%;bottom:-40%;left:-40%;z-index:0;pointer-events:none;background:radial-gradient(closest-side,rgba(91,141,247,.2),transparent 60%),radial-gradient(closest-side at 30% 70%,rgba(167,139,250,.13),transparent 65%);filter:blur(30px)}.card{-webkit-app-region:no-drag;position:relative;z-index:1;width:380px;padding:36px 36px 28px;background:linear-gradient(180deg,#141820d9,#0d1016d9);border:1px solid rgba(255,255,255,.06);border-radius:16px;box-shadow:0 1px #ffffff0a inset,0 30px 60px #0006;-webkit-backdrop-filter:blur(18px);backdrop-filter:blur(18px);display:flex;flex-direction:column;align-items:center;gap:18px}.brand{display:flex;align-items:center;gap:12px;align-self:stretch}.brand-mark{width:40px;height:40px;border-radius:12px;display:grid;place-items:center;background:linear-gradient(135deg,#5b8df7,#a78bfa);box-shadow:0 8px 20px #5b8df759}.brand-mark.sm{width:28px;height:28px;border-radius:8px;box-shadow:none}.brand-mark.sm svg{width:16px;height:16px}.brand-text{line-height:1.2}.brand-name{font-weight:600;font-size:15px}.brand-sub{font-size:12px;color:#9ca3af;margin-top:2px}.tagline{margin:4px 0 0;color:#c7cdd6;font-size:13.5px;text-align:center;line-height:1.55}.btn-primary{-webkit-app-region:no-drag;margin-top:4px;-webkit-appearance:none;-moz-appearance:none;appearance:none;width:100%;height:42px;display:inline-flex;align-items:center;justify-content:center;gap:8px;background:linear-gradient(180deg,#5b8df7,#4570d8);color:#fff;border:0;border-radius:10px;font-size:14px;font-weight:500;letter-spacing:.2px;cursor:pointer;box-shadow:0 1px #ffffff2e inset,0 -1px #0000002e inset,0 10px 22px #5b8df747;transition:transform 80ms ease,filter .12s ease}.btn-primary:hover{filter:brightness(1.08)}.btn-primary:active{transform:translateY(1px)}.btn-primary:disabled{filter:grayscale(.4) brightness(.7);cursor:default}.btn-ghost{-webkit-app-region:no-drag;-webkit-appearance:none;-moz-appearance:none;appearance:none;background:transparent;color:#9ca3af;border:1px solid rgba(255,255,255,.08);border-radius:8px;padding:6px 14px;font-size:12.5px;cursor:pointer;transition:color .12s ease,border-color .12s ease,background .12s ease}.btn-ghost.sm{padding:4px 10px;font-size:11.5px}.btn-ghost:hover{color:#e5e7eb;background:#ffffff0a;border-color:#ffffff24}.btn-ghost:disabled{opacity:.4;cursor:default}.hint{margin:0;font-size:11.5px;color:#6b7280}.spinner-row{display:inline-flex;align-items:center;gap:8px;color:#c7cdd6;font-size:13px}.spin{animation:spin 1s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.error{width:100%;font-size:12px;color:#fca5a5;background:#ef444414;border:1px solid rgba(239,68,68,.18);padding:8px 12px;border-radius:8px;text-align:center;line-height:1.5}.topbar{-webkit-app-region:drag;position:sticky;top:0;z-index:10;display:flex;align-items:center;justify-content:space-between;padding:14px 24px 12px;background:#08090e99;border-bottom:1px solid rgba(255,255,255,.05);-webkit-backdrop-filter:blur(14px);backdrop-filter:blur(14px)}[data-platform=darwin][data-fullscreen="0"] .topbar{padding-left:84px}.brand-mini{display:inline-flex;align-items:center;gap:10px}.brand-mini .brand-name{font-size:14px}.user-chip{-webkit-app-region:no-drag;display:inline-flex;align-items:center;gap:10px}.welcome{font-size:12px;color:#34d399;padding:4px 10px;border-radius:999px;background:#10b9811a;border:1px solid rgba(16,185,129,.25);animation:fadein .2s ease}@keyframes fadein{0%{opacity:0;transform:translateY(-2px)}to{opacity:1;transform:none}}.avatar{width:26px;height:26px;border-radius:50%;display:grid;place-items:center;background:linear-gradient(135deg,#5b8df7,#a78bfa);font-size:12px;font-weight:600;color:#fff}.user-name{font-size:13px;color:#c7cdd6}.glow--app{inset:-10% -10% auto -10%;height:50vh;background:radial-gradient(closest-side at 75% 0%,rgba(91,141,247,.18),transparent 65%),radial-gradient(closest-side at 20% 10%,rgba(167,139,250,.1),transparent 60%);filter:blur(40px)}.main{position:relative;z-index:1;padding:22px 32px 48px;width:100%;display:flex;flex-direction:column;gap:16px}.app__tabs{display:inline-flex;align-items:center;gap:4px;padding:4px;background:#1418208c;border:1px solid rgba(255,255,255,.06);border-radius:10px;align-self:flex-start;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}.app__tab{-webkit-app-region:no-drag;-webkit-appearance:none;-moz-appearance:none;appearance:none;background:transparent;color:#9ca3af;border:0;border-radius:7px;padding:6px 14px;font-size:12.5px;font-weight:500;letter-spacing:.15px;cursor:pointer;display:inline-flex;align-items:center;gap:6px;transition:color .12s ease,background .12s ease}.app__tab:hover{color:#e5e7eb;background:#ffffff0a}.app__tab.is-active{color:#fff;background:#5b8df72e;box-shadow:0 0 0 1px #5b8df74d inset}.app__tab-count{font-size:10.5px;color:#9ca3af;background:#ffffff0f;padding:1px 6px;border-radius:999px;min-width:18px;text-align:center}.app__tab.is-active .app__tab-count{background:#5b8df74d;color:#fff}.app__grid{display:grid;grid-template-columns:repeat(auto-fill,200px);gap:14px}.add-card{-webkit-app-region:no-drag;-webkit-appearance:none;-moz-appearance:none;appearance:none;width:200px;height:200px;background:transparent;border:1.5px dashed rgba(255,255,255,.12);border-radius:14px;color:#9ca3af;cursor:pointer;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;transition:border-color .16s ease,color .16s ease,background .16s ease}.add-card:hover{border-color:#5b8df780;color:#e5e7eb;background:#5b8df70d}.add-card__plus{width:36px;height:36px;border-radius:50%;display:grid;place-items:center;background:#ffffff0a;font-size:22px;font-weight:300;color:inherit}.add-card__label{font-size:13px;font-weight:500}.bcard--helper{background:linear-gradient(160deg,#5b8df729,#a78bfa24 60%,#1418208c);border-color:#a78bfa40}.bcard--helper .bcard__accent{background:linear-gradient(90deg,#5b8df7,#a78bfa);opacity:1}.bcard--helper:hover{border-color:#a78bfa73}.bcard__pill--helper{background:#a78bfa2e;border-color:#a78bfa4d;color:#c4b5fd;font-size:11px;font-weight:600;letter-spacing:.2px}.bcard__helper-icon{font-size:12px;filter:grayscale(.2)}.bcard__badge--free{background:#34d39926;color:#34d399;border:1px solid rgba(52,211,153,.3)}.bcard__badge--trial{background:#fbbf2426;color:#fcd34d;border:1px solid rgba(251,191,36,.3);font-size:9.5px;letter-spacing:.3px;padding:2px 7px}.bcard__badge--local{background:#5b8df72e;color:#a5c4ff;border:1px solid rgba(91,141,247,.35);font-size:9.5px;letter-spacing:.3px;padding:2px 7px}.bcard--helper .bcard__name{color:#f3f4f6}.bcard__desc{margin:4px 0 0;font-size:11.5px;line-height:1.5;color:#c7cdd6;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.bcard__fineprint{margin:4px 0 0;font-size:10.5px;color:#9ca3af;opacity:.8}.bcard__cta--helper{background:linear-gradient(90deg,#5b8df7,#a78bfa);box-shadow:0 6px 18px -4px #a78bfa73}.bcard__cta--helper:hover{filter:brightness(1.1)}.helper-aside__top{display:flex;align-items:center;justify-content:space-between;padding:12px 14px 10px;border-bottom:1px solid rgba(255,255,255,.05)}.helper-aside__title{font-size:12.5px;font-weight:600;color:#c7cdd6}.helper-aside__close{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:transparent;border:0;color:#6b7280;font-size:20px;line-height:1;width:26px;height:26px;border-radius:6px;cursor:pointer;transition:color .12s ease,background .12s ease}.helper-aside__close:hover{color:#e5e7eb;background:#ffffff0f}.helper-modal__backdrop{position:absolute;top:0;right:0;bottom:0;left:0;display:flex;align-items:center;justify-content:center;background:transparent;z-index:20;animation:fadein .14s ease;padding:16px;pointer-events:auto}.helper-modal{width:min(360px,100%);background:linear-gradient(180deg,#1f2733,#1a2029);border:1px solid rgba(255,255,255,.1);border-radius:12px;padding:20px 22px 16px;color:#e5e7eb;box-shadow:0 24px 48px -12px #0009,0 1px #ffffff14 inset;animation:fadein .18s ease}.helper-modal__title{font-size:14px;font-weight:600;color:#f3f4f6;margin-bottom:8px}.helper-modal__desc{font-size:12.5px;line-height:1.6;color:#b6bcc6;margin-bottom:18px}.helper-modal__desc code{font-family:ui-monospace,SFMono-Regular,SF Mono,Menlo,monospace;font-size:11.5px;padding:1px 6px;background:#ffffff14;border:1px solid rgba(255,255,255,.08);border-radius:4px;color:#e7eaef}.helper-modal__actions{display:flex;align-items:center;justify-content:flex-end;gap:8px}.helper-modal__btn{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:#ffffff0f;color:#d2d6dd;border:1px solid rgba(255,255,255,.1);border-radius:7px;padding:7px 14px;font-size:12.5px;font-weight:500;cursor:pointer;transition:background .12s ease,color .12s ease,transform 80ms ease}.helper-modal__btn:hover{background:#ffffff1a;color:#fff}.helper-modal__btn:active{transform:translateY(1px)}.helper-modal__btn--ghost{background:transparent;border-color:transparent;color:#ffffff8c;margin-right:auto;padding-left:4px;padding-right:4px}.helper-modal__btn--ghost:hover{background:transparent;color:#ffffffd9}.helper-modal__btn--primary{background:linear-gradient(180deg,#5b8df7,#4570d8);border-color:#ffffff2e;color:#fff}.helper-modal__btn--primary:hover{background:linear-gradient(180deg,#6c9af9,#5a82e0)}.helper-modal__btn:disabled{filter:grayscale(.4) brightness(.7);cursor:default}.helper-placeholder{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;padding:32px 40px;text-align:center;color:#9ca3af}.helper-placeholder__mark{font-size:48px;line-height:1;filter:grayscale(.2);opacity:.85}.helper-placeholder__title{margin:0;font-size:16px;font-weight:600;color:#e5e7eb}.helper-placeholder__sub{margin:0;font-size:12.5px;line-height:1.6;color:#9ca3af;max-width:320px}.helper-placeholder__note{margin-top:8px;font-size:11px;color:#6b7280;padding:4px 10px;background:#ffffff08;border:1px solid rgba(255,255,255,.06);border-radius:999px}.helper-placeholder__note code{font-family:ui-monospace,SFMono-Regular,SF Mono,Menlo,monospace;font-size:11px;color:#c7cdd6}.section{background:linear-gradient(180deg,#141820b8,#0d1016b8);border:1px solid rgba(255,255,255,.06);border-radius:14px;box-shadow:0 1px #ffffff0a inset,0 14px 30px #00000052;-webkit-backdrop-filter:blur(14px);backdrop-filter:blur(14px);padding:18px 18px 16px}.section-head{display:flex;align-items:center;gap:8px;padding:0 4px 14px;border-bottom:1px solid rgba(255,255,255,.04);margin-bottom:14px}.section-icon{font-size:14px;opacity:.8}.section-head h2{margin:0;font-size:13.5px;font-weight:600;color:#e5e7eb;letter-spacing:.2px}.section-sub{font-size:12px;color:#6b7280}.section-body{display:flex;flex-direction:column;gap:10px}.grid{display:grid;grid-template-columns:repeat(auto-fill,200px);gap:14px}.bcard{position:relative;width:200px;height:200px;background:#1418208c;border:1px solid rgba(255,255,255,.07);border-radius:14px;padding:16px;display:flex;flex-direction:column;overflow:hidden;transition:transform .18s ease,border-color .18s ease,box-shadow .18s ease;--brand: #5b8df7;--accent-cloud: #f59e0b}.bcard:hover{transform:translateY(-2px);border-color:#ffffff24;box-shadow:0 12px 32px -10px #00000073,0 1px #ffffff0a inset}.bcard__accent{position:absolute;top:0;left:0;right:0;height:3px;background:var(--brand);opacity:.9}.bcard--cloud .bcard__accent{background:var(--accent-cloud)}.bcard--online:before{content:"";position:absolute;bottom:-40%;right:-20%;width:140%;height:100%;background:radial-gradient(circle at center,rgba(91,141,247,.22) 0%,transparent 60%);pointer-events:none;opacity:.55}.bcard--cloud.bcard--online:before{background:radial-gradient(circle at center,rgba(245,158,11,.22) 0%,transparent 60%)}.bcard__top{display:flex;align-items:center;justify-content:space-between;position:relative;z-index:1}.bcard__pill{display:inline-flex;align-items:center;gap:6px;padding:4px 8px;background:#08090eb3;border:1px solid rgba(255,255,255,.07);border-radius:999px;color:#9ca3af}.bcard__pill svg{display:block}.bcard__dot{width:7px;height:7px;border-radius:50%;background:#6b7280;flex-shrink:0}.bcard__dot[data-tone=ok]{background:#34d399;box-shadow:0 0 0 2px #34d39940}.bcard__dot[data-tone=off]{background:#6b7280}.bcard__dot[data-tone=warn]{background:#fbbf24;box-shadow:0 0 0 2px #fbbf2440}.bcard__dot[data-tone=err]{background:#f87171;box-shadow:0 0 0 2px #f8717140}.bcard__badge{display:inline-flex;align-items:center;padding:2px 8px;border-radius:999px;background:#fbbf241f;color:#fbbf24;font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.5px}.bcard__body{flex:1;display:flex;flex-direction:column;gap:4px;padding-top:12px;padding-bottom:12px;position:relative;z-index:1;min-width:0}.bcard__name{margin:0;font-size:17px;font-weight:700;color:#e5e7eb;letter-spacing:-.015em;line-height:1.25;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.bcard__host{font-family:ui-monospace,SFMono-Regular,SF Mono,Menlo,monospace;font-size:11px;color:#9ca3af;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.bcard__meta{display:flex;flex-wrap:wrap;gap:4px;margin-top:4px}.bcard__chip{font-size:10.5px;color:#9ca3af;background:#ffffff08;border:1px solid rgba(255,255,255,.06);padding:2px 7px;border-radius:999px}.bcard__cta{position:relative;z-index:1;display:inline-flex;align-items:center;justify-content:center;gap:8px;width:100%;height:38px;border:0;border-radius:9px;background:var(--brand);color:#fff;font-size:13px;font-weight:600;letter-spacing:.1px;cursor:pointer;transition:transform 80ms ease,background .16s ease,box-shadow .16s ease,filter .16s ease;box-shadow:0 4px 12px -2px #5b8df773}.bcard--cloud .bcard__cta{background:var(--accent-cloud);box-shadow:0 4px 12px -2px #f59e0b73}.bcard__cta:hover{filter:brightness(1.08)}.bcard__cta:active{transform:translateY(1px)}.bcard__cta:disabled{background:#ffffff08;color:#6b7280;cursor:not-allowed;box-shadow:none;border:1px solid rgba(255,255,255,.06)}.empty{font-size:12.5px;color:#6b7280;padding:14px 16px;background:#ffffff05;border:1px dashed rgba(255,255,255,.08);border-radius:10px}.bcard__menuwrap{position:relative}.bcard__kebab{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border:1px solid transparent;border-radius:8px;background:transparent;color:#8b949e;cursor:pointer;transition:.15s}.bcard__kebab:hover:not(:disabled){color:#e6edf3;background:#ffffff0f;border-color:#ffffff1f}.bcard__kebab:disabled{opacity:.5;cursor:default}.bcard__kebab.has-dot{color:#e6edf3}.bcard__kebab.has-dot:after{content:"";position:absolute;top:3px;right:3px;width:7px;height:7px;border-radius:50%;background:#f59e0b;box-shadow:0 0 0 2px #0f1115}.bcard__menu{position:absolute;top:32px;right:0;z-index:20;min-width:150px;padding:5px;background:#1b2027;border:1px solid rgba(255,255,255,.12);border-radius:10px;box-shadow:0 10px 30px #00000073;display:flex;flex-direction:column;gap:2px}.bcard__menu-item{text-align:left;width:100%;border:none;background:transparent;color:#d1d5db;border-radius:7px;padding:7px 10px;font-size:12.5px;cursor:pointer;transition:.12s}.bcard__menu-item:hover{background:#ffffff12;color:#fff}.bcard__menu-item.is-accent{color:#fbbf24;font-weight:600}.bcard__menu-item.is-accent:hover{background:#f59e0b26;color:#fcd34d}.bcard__menu-item.is-danger{color:#f7a3a3}.bcard__menu-item.is-danger:hover{background:#ef444429;color:#fff}.bcard__chip--new{color:#fbbf24;background:#f59e0b1f;border-color:#f59e0b4d}.bcard__opmsg{display:flex;align-items:center;gap:6px;margin-top:9px;font-size:11.5px;color:#9ca3af;word-break:break-word}
@@ -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-CmSv3AFG.js"></script>
8
- <link rel="stylesheet" crossorigin href="./assets/index-U_RAjjQx.css">
7
+ <script type="module" crossorigin src="./assets/index-8G3gGQQt.js"></script>
8
+ <link rel="stylesheet" crossorigin href="./assets/index-TgJTnwYh.css">
9
9
  </head>
10
10
  <body>
11
11
  <div id="root"></div>
@@ -17,7 +17,7 @@ const fs = require("fs");
17
17
  const os = require("os");
18
18
  const http = require("http");
19
19
  const path = require("path");
20
- const { spawn } = require("child_process");
20
+ const { spawn, execFileSync } = require("child_process");
21
21
 
22
22
  const DEFAULT_PORT = Number(process.env.CICY_CODE_PORT || 8008);
23
23
 
@@ -101,22 +101,65 @@ async function start({ logPath, port = DEFAULT_PORT, force = false, version = nu
101
101
  return child;
102
102
  }
103
103
 
104
- async function stop({ timeoutMs = 5000 } = {}) {
105
- if (!child) return;
106
- const p = child;
107
- child = null;
108
- // Docker-launched (win32): not a real ChildProcess — remove the container.
109
- if (p && p.docker) {
110
- try { await require("./docker").stop(); } catch {}
111
- return;
104
+ // PIDs currently LISTENing on `port`, via lsof. Tries a few common paths
105
+ // because the GUI-launched Electron process often has a minimal PATH. Returns
106
+ // [] when lsof is missing or nothing is listening.
107
+ const LSOF_CANDIDATES = ["/usr/sbin/lsof", "/usr/bin/lsof", "lsof"];
108
+ function listPortPids(port) {
109
+ for (const bin of LSOF_CANDIDATES) {
110
+ try {
111
+ const out = execFileSync(bin, ["-nP", `-tiTCP:${port}`, "-sTCP:LISTEN"], {
112
+ encoding: "utf8",
113
+ stdio: ["ignore", "pipe", "ignore"],
114
+ });
115
+ return [...new Set(out.split(/\s+/).map(s => parseInt(s, 10)).filter(n => n > 0))];
116
+ } catch (e) {
117
+ if (e && e.code === "ENOENT") continue; // wrong path → try next candidate
118
+ return []; // lsof ran but matched nothing (non-zero exit)
119
+ }
120
+ }
121
+ return [];
122
+ }
123
+
124
+ // Kill whatever is LISTENing on `port` — even a detached/orphan (PPID=1)
125
+ // cicy-code from a prior launch that we never tracked as a child. SIGTERM,
126
+ // wait for the port to free, then SIGKILL the stragglers.
127
+ async function killPortListeners(port = DEFAULT_PORT, timeoutMs = 5000) {
128
+ const pids = listPortPids(port);
129
+ if (!pids.length) return 0;
130
+ for (const pid of pids) { try { process.kill(pid, "SIGTERM"); } catch {} }
131
+ const t0 = Date.now();
132
+ while (Date.now() - t0 < timeoutMs) {
133
+ if (!listPortPids(port).length) return pids.length;
134
+ await new Promise(r => setTimeout(r, 150));
112
135
  }
113
- try { p.kill("SIGTERM"); } catch {}
114
- const start = Date.now();
115
- while (p.exitCode === null && Date.now() - start < timeoutMs) {
116
- await new Promise(r => setTimeout(r, 100));
136
+ for (const pid of listPortPids(port)) { try { process.kill(pid, "SIGKILL"); } catch {} }
137
+ return pids.length;
138
+ }
139
+
140
+ async function stop({ timeoutMs = 5000, port = DEFAULT_PORT } = {}) {
141
+ // 1) The child we spawned this session (npx) or the Docker container.
142
+ if (child) {
143
+ const p = child;
144
+ child = null;
145
+ if (p.docker) {
146
+ try { await require("./docker").stop(); } catch {}
147
+ return;
148
+ }
149
+ try { p.kill("SIGTERM"); } catch {}
150
+ const t0 = Date.now();
151
+ while (p.exitCode === null && Date.now() - t0 < timeoutMs) {
152
+ await new Promise(r => setTimeout(r, 100));
153
+ }
154
+ if (p.exitCode === null) { try { p.kill("SIGKILL"); } catch {} }
117
155
  }
118
- if (p.exitCode === null) {
119
- try { p.kill("SIGKILL"); } catch {}
156
+
157
+ // 2) Anything STILL on :port we didn't spawn — a detached npx from a prior
158
+ // launch, a user-run daemon, a PPID=1 orphan. The homepage 停止/重启 must
159
+ // act on the REAL listener; otherwise (no tracked child) it would no-op.
160
+ // Docker (win32) owns its own lifecycle, so skip the port-kill there.
161
+ if (process.platform !== "win32") {
162
+ await killPortListeners(port, timeoutMs);
120
163
  }
121
164
  }
122
165
 
@@ -145,7 +188,7 @@ function clearNpxCache() {
145
188
  // Restart: stop the running daemon, let :8008 free, then force a fresh spawn
146
189
  // (reusing the same cached version — no registry round-trip).
147
190
  async function restart({ logPath, port = DEFAULT_PORT } = {}) {
148
- await stop();
191
+ await stop({ port });
149
192
  await new Promise(r => setTimeout(r, 300));
150
193
  return start({ logPath, port, force: true });
151
194
  }
@@ -155,7 +198,7 @@ async function restart({ logPath, port = DEFAULT_PORT } = {}) {
155
198
  // else → clear the npx cache + spawn `cicy-code@latest` so npx re-resolves
156
199
  // against the registry (npmmirror for CN) and pulls a newer build.
157
200
  async function update({ logPath, port = DEFAULT_PORT } = {}) {
158
- await stop();
201
+ await stop({ port });
159
202
  if (process.platform === "win32") {
160
203
  try { await require("./docker").loadImage(); } catch (e) {
161
204
  console.warn(`[cicy-code-sidecar] docker image reload failed: ${e.message}`);
@@ -630,48 +630,53 @@ body {
630
630
  border-radius: 10px;
631
631
  }
632
632
 
633
- /* ---- 本地 cicy-code 守护进程的控制条 (SidecarControl) ---- */
634
- .sidecar-bar {
635
- display: flex;
636
- align-items: center;
637
- gap: 12px;
638
- flex-wrap: wrap;
639
- margin-bottom: 12px;
640
- padding: 8px 12px;
641
- background: rgba(20,24,32,.55);
642
- border: 1px solid rgba(255,255,255,.08);
643
- border-radius: 10px;
644
- }
645
- .sidecar-bar__label {
646
- display: flex; align-items: center; gap: 8px;
647
- font-size: 12.5px; color: #c7cdd6;
648
- }
649
- .sidecar-bar__title { font-weight: 600; }
650
- .sidecar-bar__state { color: #9ca3af; font-size: 11.5px; }
651
- .sidecar-dot {
652
- width: 8px; height: 8px; border-radius: 50%;
653
- background: #6b7280; flex: none;
654
- }
655
- .sidecar-dot.is-on { background: #10b981; box-shadow: 0 0 0 3px rgba(16,185,129,.15); }
656
- .sidecar-dot.is-off { background: #ef4444; box-shadow: 0 0 0 3px rgba(239,68,68,.12); }
657
- .sidecar-dot.is-unknown { background: #6b7280; }
658
- .sidecar-bar__actions { display: flex; gap: 6px; margin-left: auto; }
659
- .sidecar-btn {
660
- display: inline-flex; align-items: center; gap: 6px;
661
- border: 1px solid rgba(255,255,255,.12);
662
- background: rgba(255,255,255,.03); color: #d1d5db;
663
- border-radius: 8px; padding: 5px 12px; font-size: 11.5px;
633
+ /* ---- 本地团队卡片:cicy-code 守护进程维护 (⋯ 菜单 + 新版提示) ---- */
634
+ .bcard__menuwrap { position: relative; }
635
+ .bcard__kebab {
636
+ display: inline-flex; align-items: center; justify-content: center;
637
+ width: 28px; height: 28px;
638
+ border: 1px solid transparent; border-radius: 8px;
639
+ background: transparent; color: #8b949e;
664
640
  cursor: pointer; transition: .15s;
665
641
  }
666
- .sidecar-btn:hover:not(:disabled) {
667
- color: #fff; background: rgba(255,255,255,.06); border-color: rgba(255,255,255,.2);
642
+ .bcard__kebab:hover:not(:disabled) {
643
+ color: #e6edf3; background: rgba(255,255,255,.06); border-color: rgba(255,255,255,.12);
668
644
  }
669
- .sidecar-btn:disabled { opacity: .4; cursor: default; }
670
- .sidecar-btn--danger { color: #f7a3a3; border-color: rgba(239,68,68,.25); }
671
- .sidecar-btn--danger:hover:not(:disabled) {
672
- color: #fff; background: rgba(239,68,68,.18); border-color: rgba(239,68,68,.5);
645
+ .bcard__kebab:disabled { opacity: .5; cursor: default; }
646
+ /* amber dot on the when an update is available */
647
+ .bcard__kebab.has-dot { color: #e6edf3; }
648
+ .bcard__kebab.has-dot::after {
649
+ content: ""; position: absolute; top: 3px; right: 3px;
650
+ width: 7px; height: 7px; border-radius: 50%;
651
+ background: #f59e0b; box-shadow: 0 0 0 2px #0f1115;
652
+ }
653
+ .bcard__menu {
654
+ position: absolute; top: 32px; right: 0; z-index: 20;
655
+ min-width: 150px; padding: 5px;
656
+ background: #1b2027; border: 1px solid rgba(255,255,255,.12);
657
+ border-radius: 10px; box-shadow: 0 10px 30px rgba(0,0,0,.45);
658
+ display: flex; flex-direction: column; gap: 2px;
659
+ }
660
+ .bcard__menu-item {
661
+ text-align: left; width: 100%;
662
+ border: none; background: transparent; color: #d1d5db;
663
+ border-radius: 7px; padding: 7px 10px; font-size: 12.5px;
664
+ cursor: pointer; transition: .12s;
665
+ }
666
+ .bcard__menu-item:hover { background: rgba(255,255,255,.07); color: #fff; }
667
+ .bcard__menu-item.is-accent { color: #fbbf24; font-weight: 600; }
668
+ .bcard__menu-item.is-accent:hover { background: rgba(245,158,11,.15); color: #fcd34d; }
669
+ .bcard__menu-item.is-danger { color: #f7a3a3; }
670
+ .bcard__menu-item.is-danger:hover { background: rgba(239,68,68,.16); color: #fff; }
671
+ /* "新版 vX.Y.Z" chip on the card face */
672
+ .bcard__chip--new {
673
+ color: #fbbf24;
674
+ background: rgba(245,158,11,.12);
675
+ border-color: rgba(245,158,11,.3);
673
676
  }
674
- .sidecar-bar__msg {
675
- flex-basis: 100%;
677
+ .bcard__opmsg {
678
+ display: flex; align-items: center; gap: 6px;
679
+ margin-top: 9px;
676
680
  font-size: 11.5px; color: #9ca3af;
681
+ word-break: break-word;
677
682
  }
@@ -349,8 +349,6 @@ export default function App() {
349
349
  ))}
350
350
  </div>
351
351
 
352
- {showLocal && <SidecarControl />}
353
-
354
352
  {profileError && (
355
353
  <div className="error" style={{ marginBottom: 12 }}>
356
354
  云端: {profileError}
@@ -362,7 +360,7 @@ export default function App() {
362
360
 
363
361
  <div className="app__grid">
364
362
  {showLocal && localTeams && localTeams.map((t) => (
365
- <LocalTeamCard key={"local:" + t.id} team={t} onOpen={() => openLocalTeam(t.id)} onRename={renameLocalTeam} />
363
+ <LocalTeamCard key={"local:" + t.id} team={t} onOpen={() => openLocalTeam(t.id)} onRename={renameLocalTeam} onRefresh={fetchLocalTeams} />
366
364
  ))}
367
365
  {showCloud && teams && teams.map((t) => (
368
366
  <TeamCard
@@ -427,106 +425,7 @@ function Section({ title, subtitle, icon, children }) {
427
425
  );
428
426
  }
429
427
 
430
- // Lifecycle controls for the locally-run cicy-code daemon (the sidecar on
431
- // :8008). Polls sidecar.status and offers 重启 / 更新 / 停止. Only meaningful
432
- // on a desktop where this app owns the npx/Docker-launched daemon.
433
- function SidecarControl() {
434
- const [running, setRunning] = useState(null); // null = unknown, then bool
435
- const [busy, setBusy] = useState(""); // "" | "restart" | "update" | "stop"
436
- const [msg, setMsg] = useState("");
437
-
438
- const probe = useCallback(async () => {
439
- if (!window.cicy?.sidecar?.status) return;
440
- try {
441
- const r = await window.cicy.sidecar.status();
442
- setRunning(!!r?.running);
443
- } catch { setRunning(false); }
444
- }, []);
445
-
446
- useEffect(() => {
447
- probe();
448
- const id = setInterval(probe, 4000);
449
- return () => clearInterval(id);
450
- }, [probe]);
451
-
452
- // The bridge isn't there at all (old build / non-desktop) → render nothing.
453
- if (!window.cicy?.sidecar?.restart) return null;
454
-
455
- const run = async (kind, fn, doneText) => {
456
- if (busy) return;
457
- setBusy(kind);
458
- setMsg("");
459
- try {
460
- const r = await fn();
461
- if (r?.ok) {
462
- setMsg(r.warning ? `${doneText}(${r.warning})` : doneText);
463
- } else {
464
- setMsg(tr("sidecar.failed", "失败") + (r?.error ? `: ${r.error}` : ""));
465
- }
466
- } catch (e) {
467
- setMsg(tr("sidecar.failed", "失败") + `: ${e?.message || e}`);
468
- } finally {
469
- setBusy("");
470
- probe();
471
- }
472
- };
473
-
474
- const dotClass = running == null ? "is-unknown" : running ? "is-on" : "is-off";
475
- const stateText = running == null
476
- ? tr("sidecar.checking", "检测中…")
477
- : running ? tr("sidecar.running", "运行中") : tr("sidecar.stopped", "已停止");
478
-
479
- return (
480
- <div data-id="SidecarControl" className="sidecar-bar">
481
- <div className="sidecar-bar__label">
482
- <span className={`sidecar-dot ${dotClass}`} aria-hidden />
483
- <span className="sidecar-bar__title">本地 cicy-code</span>
484
- <span className="sidecar-bar__state">{stateText}</span>
485
- </div>
486
- <div className="sidecar-bar__actions">
487
- <button
488
- type="button"
489
- data-id="SidecarControl-restart"
490
- className="sidecar-btn"
491
- disabled={!!busy}
492
- onClick={() => run("restart",
493
- () => window.cicy.sidecar.restart(),
494
- tr("sidecar.restarted", "已重启"))}
495
- >
496
- {busy === "restart" ? <Spinner /> : null}
497
- {tr("sidecar.restart", "重启")}
498
- </button>
499
- <button
500
- type="button"
501
- data-id="SidecarControl-update"
502
- className="sidecar-btn"
503
- disabled={!!busy}
504
- onClick={() => run("update",
505
- () => window.cicy.sidecar.update(),
506
- tr("sidecar.updated", "已更新到最新"))}
507
- >
508
- {busy === "update" ? <Spinner /> : null}
509
- {tr("sidecar.update", "更新")}
510
- </button>
511
- <button
512
- type="button"
513
- data-id="SidecarControl-stop"
514
- className="sidecar-btn sidecar-btn--danger"
515
- disabled={!!busy || running === false}
516
- onClick={() => run("stop",
517
- () => window.cicy.sidecar.stop(),
518
- tr("sidecar.stoppedDone", "已停止"))}
519
- >
520
- {busy === "stop" ? <Spinner /> : null}
521
- {tr("sidecar.stop", "停止")}
522
- </button>
523
- </div>
524
- {msg && <span data-id="SidecarControl-msg" className="sidecar-bar__msg">{msg}</span>}
525
- </div>
526
- );
527
- }
528
-
529
- function LocalTeamCard({ team, onOpen, onRename }) {
428
+ function LocalTeamCard({ team, onOpen, onRename, onRefresh }) {
530
429
  const statusInfo = LOCAL_STATUS[team.status] || LOCAL_STATUS.error;
531
430
  const tone = statusInfo.tone;
532
431
  // Inline rename: double-click the name or click ✎ → edit → Enter/blur saves.
@@ -539,6 +438,59 @@ function LocalTeamCard({ team, onOpen, onRename }) {
539
438
  const next = String(draft || "").trim();
540
439
  if (onRename && next && next !== team.name) await onRename(team.id, next);
541
440
  };
441
+
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.
446
+ const hasOps = !!window.cicy?.sidecar?.restart;
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);
479
+ if (busy) return;
480
+ setBusy(kind); setOpMsg("");
481
+ try {
482
+ const r = await fn();
483
+ setOpMsg(r?.ok
484
+ ? (r.warning ? `${doneText}(${r.warning})` : doneText)
485
+ : (tr("sidecar.failed", "操作失败") + (r?.error ? `: ${r.error}` : "")));
486
+ } catch (err) {
487
+ setOpMsg(tr("sidecar.failed", "操作失败") + `: ${err?.message || err}`);
488
+ } finally {
489
+ setBusy("");
490
+ onRefresh?.(); // re-probe so the status dot/chip catches up
491
+ }
492
+ };
493
+ const BUSY_LABEL = { start: "启动中…", restart: "重启中…", update: "更新中…", stop: "停止中…" };
542
494
  return (
543
495
  <div data-id="LocalTeamCard" className={`bcard bcard--local${tone === "ok" ? " bcard--online" : ""}`}>
544
496
  <div className="bcard__accent" />
@@ -547,6 +499,54 @@ function LocalTeamCard({ team, onOpen, onRename }) {
547
499
  <span className="bcard__dot" data-tone={tone} />
548
500
  <LaptopIcon />
549
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
+ )}
550
550
  </div>
551
551
  <div className="bcard__body">
552
552
  {editing ? (
@@ -578,21 +578,58 @@ function LocalTeamCard({ team, onOpen, onRename }) {
578
578
  <div className="bcard__meta">
579
579
  <span className="bcard__chip">{statusInfo.label}</span>
580
580
  {team.version && <span className="bcard__chip">v{team.version}</span>}
581
+ {updateAvailable && (
582
+ <span
583
+ className="bcard__chip bcard__chip--new"
584
+ data-id="LocalTeamCard-newbadge"
585
+ title={`${tr("sidecar.updateTo", "更新到")} v${latest}`}
586
+ >
587
+ {tr("sidecar.newVersion", "新版")} v{latest}
588
+ </span>
589
+ )}
581
590
  </div>
591
+ {(busy || opMsg) && (
592
+ <div className="bcard__opmsg" data-id="LocalTeamCard-opmsg">
593
+ {busy ? <><Spinner />{BUSY_LABEL[busy] || tr("sidecar.working", "处理中…")}</> : opMsg}
594
+ </div>
595
+ )}
582
596
  </div>
583
- <button
584
- type="button"
585
- className="bcard__cta"
586
- onClick={onOpen}
587
- disabled={team.status !== "running"}
588
- >
589
- <ArrowIcon />
590
- <span>{team.status === "running" ? "打开" : statusInfo.cta}</span>
591
- </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
+ )}
592
619
  </div>
593
620
  );
594
621
  }
595
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
+
596
633
  const LOCAL_STATUS = {
597
634
  running: { tone: "ok", label: "running", cta: "打开" },
598
635
  stopped: { tone: "off", label: "stopped", cta: "未运行" },
@@ -691,6 +728,15 @@ function LaptopIcon() {
691
728
  </svg>
692
729
  );
693
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
+ }
694
740
  function GlobeIcon() {
695
741
  return (
696
742
  <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">