cicy-desktop 2.1.96 → 2.1.97

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.
@@ -0,0 +1 @@
1
+ :root{--accent: #5b8df7;--accent-soft: rgba(91,141,247,.16);--accent-line: rgba(91,141,247,.32);--accent-text: #a5c4ff;--ok: #4ade80;--danger: #f7a3a3;--text: #e5e7eb;--text-dim: #9da7b3;--text-mute: #6b7280;--line: rgba(255,255,255,.08)}*{box-sizing:border-box}html,body,#root{margin:0;height:100%}html,body{overflow:hidden}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:hidden}.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:relative;flex:0 0 auto;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-top:34px}.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:var(--text-dim);padding:4px 10px;border-radius:999px;background:#ffffff0d;border:1px solid var(--line);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}.user-chip{position:relative}.user-chip__trigger{-webkit-app-region:no-drag;display:inline-flex;align-items:center;gap:8px;border:1px solid transparent;background:transparent;border-radius:999px;padding:3px 8px 3px 3px;cursor:pointer;transition:.12s}.user-chip__trigger:hover,.user-chip__trigger.is-open{background:#ffffff0f;border-color:#ffffff1f}.user-chip__caret{font-size:10px;color:#8a93a3;transition:transform .12s}.user-chip__trigger.is-open .user-chip__caret{transform:rotate(180deg)}.user-chip__menu{position:absolute;top:38px;right:0;z-index:30;min-width:200px;padding:6px;background:#1b2027;border:1px solid rgba(255,255,255,.12);border-radius:12px;box-shadow:0 12px 34px #00000080;display:flex;flex-direction:column;gap:2px;animation:fadein .14s ease}.user-chip__menu-item{text-align:left;width:100%;border:none;background:transparent;color:#d1d5db;border-radius:8px;padding:9px 11px;font-size:13px;cursor:pointer;transition:.12s}.user-chip__menu-item:hover{background:#ffffff12;color:#fff}.user-chip__menu-item.is-danger{color:#f7a3a3}.user-chip__menu-item.is-danger:hover{background:#ef444429;color:#fff}.user-chip__menu-sep{height:1px;margin:4px 2px;background:#ffffff14}.user-chip__menu-version{margin-top:4px;padding:8px 11px 4px;font-size:11px;color:#6b7280;text-align:center;font-variant-numeric:tabular-nums;-webkit-user-select:text;user-select:text;border-top:1px solid rgba(255,255,255,.06)}.user-chip__mitm-row{display:flex;align-items:center;justify-content:space-between;gap:10px;cursor:default}.user-chip__mitm-row:hover{background:transparent}.user-chip__mitm-label{font-size:13px;color:var(--text)}.mini-switch{-webkit-app-region:no-drag;position:relative;flex:0 0 auto;width:36px;height:20px;padding:0;border-radius:999px;border:1px solid var(--line);background:#ffffff14;cursor:pointer;transition:background .16s ease,border-color .16s ease}.mini-switch.is-on{background:var(--accent);border-color:var(--accent)}.mini-switch.is-busy{opacity:.6;cursor:default}.mini-switch:disabled{cursor:default}.mini-switch__knob{position:absolute;top:1px;left:1px;width:16px;height:16px;border-radius:50%;background:#fff;box-shadow:0 1px 2px #00000073;transition:transform .16s ease}.mini-switch.is-on .mini-switch__knob{transform:translate(16px)}.mini-switch.is-busy .mini-switch__knob{animation:spin 1s linear infinite}.user-chip__mitm-note{margin:0 4px;padding:2px 4px 6px;font-size:11px;color:var(--text-mute)}.user-chip__mitm-err{margin:2px 4px 6px;padding:5px 8px;font-size:11px;color:var(--danger);background:#ef44441a;border-radius:6px;line-height:1.4}.bcard__top-right{display:inline-flex;align-items:center;gap:6px}.bcard__billing-btn{-webkit-app-region:no-drag;border:1px solid var(--line);background:transparent;color:var(--text-dim);border-radius:7px;padding:3px 9px;font-size:11px;cursor:pointer;transition:color .12s,border-color .12s,background .12s}.bcard__billing-btn:hover{color:var(--accent-text);border-color:var(--accent-line);background:var(--accent-soft)}.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;flex:1 1 auto;min-height:0;overflow-y:auto}.main::-webkit-scrollbar{width:8px}.main::-webkit-scrollbar-track{background:transparent}.main::-webkit-scrollbar-thumb{background:#7d87964d;border-radius:999px;border:2px solid transparent;background-clip:padding-box}.main::-webkit-scrollbar-thumb:hover{background-color:#7d87968c}.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__tabsrow{display:flex;align-items:center;justify-content:space-between;gap:12px;width:100%}.app__add-team{-webkit-app-region:no-drag;-webkit-appearance:none;-moz-appearance:none;appearance:none;cursor:pointer;flex:none;color:#5b8df7;font-weight:600;font-size:12.5px;background:#5b8df71a;border:1px solid rgba(91,141,247,.35);border-radius:9px;padding:7px 14px;transition:color .12s ease,background .12s ease,border-color .12s ease}.app__add-team:hover{color:#fff;background:#5b8df738;border-color:#5b8df7a6}.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:#ffffff0f;color:var(--text-dim);border:1px solid var(--line);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:#ffffff0d;box-shadow:none}.bcard__cta--helper:hover{background:var(--accent-soft)}.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;--accent-custom: #8b5cf6}.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--custom .bcard__accent{background:var(--accent-custom)}.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--custom.bcard--online:before{background:radial-gradient(circle at center,rgba(139,92,246,.22) 0%,transparent 60%)}.bcard__top{display:flex;align-items:center;justify-content:space-between;position:relative;z-index:5}.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:var(--accent-soft);color:var(--accent-text);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:1px solid var(--line);border-radius:9px;background:#ffffff0d;color:var(--text);font-size:13px;font-weight:600;letter-spacing:.1px;cursor:pointer;transition:transform 80ms ease,background .14s ease,border-color .14s ease,color .14s ease;box-shadow:none}.bcard--cloud .bcard__cta,.bcard--custom .bcard__cta{background:#ffffff0d;box-shadow:none}.bcard__cta:hover{background:var(--accent-soft);border-color:var(--accent-line);color:var(--accent-text)}.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:var(--accent-text);font-weight:600}.bcard__menu-item.is-accent:hover{background:var(--accent-soft);color:#c7dbff}.bcard__menu-item.is-danger{color:#f7a3a3}.bcard__menu-item.is-danger:hover{background:#ef444429;color:#fff}.bcard__menu--portal{z-index:9990}.bcard__menu--portal .bcard__menu-item{white-space:normal;word-break:break-word}.docker-setup{margin-bottom:14px;padding:14px 16px;background:linear-gradient(180deg,#5b8df714,#14182080);border:1px solid rgba(91,141,247,.22);border-radius:12px}.docker-setup__head{display:flex;flex-direction:column;gap:3px;margin-bottom:12px}.docker-setup__title{font-size:13.5px;font-weight:600;color:#e6edf3}.docker-setup__sub{font-size:11.5px;color:#9ca3af;line-height:1.5}.docker-setup__steps{display:flex;flex-direction:column;gap:6px;margin-bottom:12px}.docker-step{display:flex;align-items:center;gap:8px;font-size:12px;color:#9ca3af}.docker-step__dot{width:8px;height:8px;border-radius:50%;flex:none;background:#3a4150;transition:.2s}.docker-step__label{color:#c7cdd6;min-width:96px}.docker-step__msg{color:#8b949e;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.docker-step__pct{margin-left:auto;font-variant-numeric:tabular-nums;color:#5b8df7;flex:none}.docker-step.is-running .docker-step__dot{background:#5b8df7;box-shadow:0 0 0 3px #5b8df72e;animation:dpulse 1.2s ease-in-out infinite}.docker-step.is-running .docker-step__label{color:#e6edf3}.docker-step.is-done .docker-step__dot,.docker-step.is-skip .docker-step__dot{background:#10b981}.docker-step.is-skip{opacity:.7}.docker-step.is-retry .docker-step__dot{background:#f59e0b}.docker-step.is-error .docker-step__dot{background:#ef4444;box-shadow:0 0 0 3px #ef444426}.docker-step.is-error .docker-step__msg{color:#f7a3a3}@keyframes dpulse{0%,to{opacity:1}50%{opacity:.45}}.docker-setup__actions{display:flex;gap:8px;align-items:center}.docker-setup__actions .btn-primary{width:auto;padding:7px 16px;font-size:12.5px}.bcard__ver{font-size:11px;color:#8b949e;font-variant-numeric:tabular-nums}.bcard__chip--new{color:var(--accent-text);background:var(--accent-soft);border-color:var(--accent-line)}.bcard__opmsg{display:flex;align-items:center;gap:6px;margin-top:9px;font-size:11.5px;color:#9ca3af;word-break:break-word}.toast-host{position:fixed;right:16px;bottom:16px;z-index:9999;display:flex;flex-direction:column;gap:8px;max-width:min(360px,calc(100vw - 32px));pointer-events:none}.toast{pointer-events:auto;position:relative;display:flex;flex-direction:column;gap:6px;padding:10px 30px 10px 12px;font-size:12.5px;line-height:1.4;color:var(--text, #e6eaf0);background:#161a21f5;border:1px solid rgba(125,135,150,.22);border-left:3px solid var(--accent, #3b82f6);border-radius:10px;box-shadow:0 8px 28px #0000006b;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);animation:toast-in .18s ease}@keyframes toast-in{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}.toast[data-status=error]{border-left-color:#f87171}.toast[data-status=error] .toast__msg{color:#f87171}.toast[data-status=done]{border-left-color:#4ade80}.toast[data-status=done] .toast__msg{color:#4ade80}.toast__msg{word-break:break-word}.toast__x{position:absolute;top:6px;right:8px;background:none;border:none;cursor:pointer;padding:0;font-size:15px;line-height:1;color:var(--text-dim, #9da7b3)}.toast__x:hover{color:var(--text, #e6eaf0)}.toast__bar{display:block;height:4px;border-radius:2px;background:#7d879640;overflow:hidden}.toast__bar>span{display:block;height:100%;border-radius:2px;background:var(--accent, #3b82f6);transition:width .25s ease}.drawer-scrim{position:fixed;top:0;right:0;bottom:0;left:0;z-index:10000;display:flex;align-items:flex-end;justify-content:center;background:#06080c80;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);animation:drawer-fade .18s ease}@keyframes drawer-fade{0%{opacity:0}to{opacity:1}}.drawer{width:min(560px,calc(100vw - 24px));max-height:76vh;display:flex;flex-direction:column;margin-bottom:12px;background:#14181ffa;border:1px solid rgba(125,135,150,.2);border-radius:16px 16px 12px 12px;box-shadow:0 -10px 44px #00000080;overflow:hidden;animation:drawer-up .24s cubic-bezier(.22,1,.36,1)}@keyframes drawer-up{0%{opacity:0;transform:translateY(28px)}to{opacity:1;transform:none}}.drawer__head{display:flex;align-items:center;justify-content:space-between;padding:14px 14px 12px 16px;border-bottom:1px solid rgba(125,135,150,.14)}.drawer__title{display:flex;align-items:center;gap:11px}.drawer__spark{display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;border-radius:8px;font-size:14px;font-weight:700;background:#5b8df729;color:var(--brand, #5b8df7)}.drawer__spark--done{background:#4ade8029;color:#4ade80}.drawer__spark--error{background:#f8717129;color:#f87171}.drawer__h{font-size:13.5px;font-weight:650;color:var(--text, #e6eaf0)}.drawer__sub{font-size:11.5px;color:var(--text-dim, #9da7b3);margin-top:1px}.drawer__x{background:none;border:none;cursor:pointer;padding:2px 6px;font-size:19px;line-height:1;color:var(--text-dim, #9da7b3);border-radius:6px}.drawer__x:hover:not(:disabled){color:var(--text, #e6eaf0);background:#7d87961f}.drawer__x:disabled{opacity:.35;cursor:default}.drawer__steps{display:flex;align-items:center;gap:0;padding:14px 18px 4px}.drawer__step{display:flex;align-items:center;gap:7px;color:var(--text-dim, #9da7b3);font-size:12px}.drawer__step-dot{display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;border-radius:50%;font-size:11px;font-weight:700;border:1.5px solid rgba(125,135,150,.35);color:var(--text-dim, #9da7b3);background:transparent;flex:none}.drawer__step-bar{width:30px;height:1.5px;background:#7d879640;margin:0 8px}.drawer__step.is-active .drawer__step-dot{border-color:var(--brand, #5b8df7);color:var(--brand, #5b8df7);box-shadow:0 0 0 3px #5b8df72e}.drawer__step.is-active .drawer__step-label{color:var(--text, #e6eaf0)}.drawer__step.is-done .drawer__step-dot{border-color:#4ade80;color:#06210f;background:#4ade80}.drawer__step.is-done .drawer__step-bar{background:#4ade8080}.drawer__step.is-error .drawer__step-dot{border-color:#f87171;color:#f87171}.drawer__log{flex:1;min-height:96px;overflow-y:auto;margin:10px 14px;padding:10px 12px;background:#0a0c10b3;border:1px solid rgba(125,135,150,.12);border-radius:10px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:11.5px;line-height:1.7}.drawer__log--scroll{max-height:168px;scrollbar-width:thin;scrollbar-color:rgba(125,135,150,.4) transparent}.drawer__log--scroll::-webkit-scrollbar{width:7px}.drawer__log--scroll::-webkit-scrollbar-track{background:transparent;margin:4px 0}.drawer__log--scroll::-webkit-scrollbar-thumb{background:#7d879652;border-radius:6px;border:1.5px solid transparent;background-clip:padding-box}.drawer__log--scroll::-webkit-scrollbar-thumb:hover{background:#7d87968c;background-clip:padding-box}.drawer__dlbars{display:flex;flex-direction:column;gap:10px;margin:12px 14px 2px}.dlbar{padding:10px 12px;border-radius:10px;background:#0a0c108c;border:1px solid rgba(125,135,150,.14)}.dlbar__head{display:flex;align-items:baseline;justify-content:space-between;gap:10px;margin-bottom:7px}.dlbar__name{font-size:12.5px;font-weight:600;color:var(--text, #e6eaf0)}.dlbar__pct{font-size:11px;color:var(--text-dim, #9da7b3);font-variant-numeric:tabular-nums}.dlbar__track{height:6px;border-radius:4px;background:#7d879629;overflow:hidden}.dlbar__fill{height:100%;border-radius:4px;background:linear-gradient(90deg,#5b8df7,#2496ed);transition:width .2s ease}.dlbar__fill.is-done{background:#4ade80}.dlbar__url{margin-top:7px;font-size:10.5px;color:#6b7686;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.drawer__log-empty{color:var(--text-dim, #9da7b3)}.drawer__line{display:flex;align-items:baseline;gap:8px;padding:1px 0}.drawer__t{color:#6b7686;flex:none;font-variant-numeric:tabular-nums}.drawer__badge{flex:none;padding:0 6px;border-radius:4px;font-size:10px;font-weight:600;background:#5b8df729;color:#8fb0f5}.drawer__badge--swap{background:#f59e0b29;color:#f5b342}.drawer__badge--done{background:#4ade8029;color:#6ee79b}.drawer__linemsg{color:var(--text, #e6eaf0);word-break:break-word}.drawer__line[data-status=error] .drawer__linemsg{color:#f87171}.drawer__line[data-status=done] .drawer__linemsg{color:#6ee79b}.drawer__line[data-status=skip] .drawer__linemsg{color:var(--text-dim, #9da7b3)}.drawer__hint{margin:0 14px 8px;padding:8px 12px;background:#f59e0b1a;border:1px solid rgba(245,158,11,.28);border-radius:8px;color:#f5b342;font-size:11.5px;line-height:1.5}.drawer__foot{display:flex;align-items:center;gap:8px;padding:12px 14px;border-top:1px solid rgba(125,135,150,.14)}.drawer__foot-status{font-size:12.5px;color:var(--text-dim, #9da7b3);margin-right:auto}.drawer__foot-status.is-error{color:#f87171}.drawer__foot-status.is-done{color:#4ade80}.drawer__btn{padding:7px 16px;border-radius:8px;cursor:pointer;font-size:12.5px;font-weight:550;background:#7d879624;border:1px solid rgba(125,135,150,.2);color:var(--text, #e6eaf0)}.drawer__btn:hover{background:#7d879638}.drawer__btn.is-accent{background:var(--brand, #5b8df7);border-color:var(--brand, #5b8df7);color:#fff}.drawer__btn.is-accent:hover{filter:brightness(1.08)}.mitm-card{border:1px solid rgba(125,135,150,.22);border-radius:12px;padding:14px 16px;margin-bottom:14px;background:#1e242e66}.mitm-card--on{border-color:#4ade8059;background:#16281e66}.mitm-pill{position:fixed;top:64px;right:16px;z-index:9998;-webkit-app-region:no-drag;display:inline-flex;align-items:center;gap:8px;padding:4px 6px 4px 11px;border-radius:999px;width:fit-content;max-width:calc(100vw - 32px);background:#141e18d9;border:1px solid rgba(74,222,128,.3);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);box-shadow:0 4px 14px #0000004d;font-size:12px;line-height:1}.mitm-pill__dot{width:7px;height:7px;border-radius:50%;flex:none;background:#4ade80;box-shadow:0 0 0 3px #4ade8029}.mitm-pill__dot[data-busy="1"]{background:#9da7b3;box-shadow:0 0 0 3px #9da7b329}.mitm-pill__text{color:var(--text-dim, #9da7b3);white-space:nowrap}.mitm-pill__off{background:none;border:none;cursor:pointer;color:#6b7686;font-size:11.5px;padding:3px 7px;border-radius:999px}.mitm-pill__off:hover{color:#f87171;background:#f871711a}.mitm-card__head{display:flex;align-items:center;gap:8px;margin-bottom:6px}.mitm-card__dot{width:8px;height:8px;border-radius:50%;background:#9da7b3;flex:0 0 auto}.mitm-card__dot[data-state=on]{background:#4ade80;box-shadow:0 0 6px #4ade8099}.mitm-card__dot[data-state=warn]{background:#fbbf24}.mitm-card__dot[data-state=off]{background:#60a5fa}.mitm-card__title{font-size:14px;font-weight:600;color:var(--text, #e6edf3)}.mitm-card__desc{font-size:12.5px;line-height:1.5;color:var(--text-dim, #9da7b3);margin:0 0 10px}.mitm-card__note{color:#fbbf24}.mitm-card__error{font-size:12px;color:#f87171;margin-bottom:8px}.mitm-card__actions{display:flex;gap:8px}.mitm-card__btn{font-size:13px;padding:7px 16px;border-radius:8px;border:none;cursor:pointer;background:var(--accent, #3b82f6);color:#fff;font-weight:500}.mitm-card__btn:disabled{opacity:.6;cursor:default}.mitm-card__btn--ghost{background:transparent;border:1px solid rgba(125,135,150,.35);color:var(--text-dim, #9da7b3)}.mitm-card__sub{color:var(--text-dim, #9da7b3);opacity:.8;font-size:11.5px}.terms-gate{display:flex;align-items:center;justify-content:center;padding:24px}.terms-gate__panel,.terms-gate__panel *{-webkit-app-region:no-drag}.terms-gate__panel{position:relative;z-index:1;width:min(680px,94vw);max-height:90vh;display:flex;flex-direction:column;background:#141921eb;border:1px solid rgba(125,135,150,.22);border-radius:16px;padding:28px 30px;box-shadow:0 24px 60px #00000080}.terms-gate__title{font-size:20px;font-weight:700;margin:0 0 4px;color:var(--text, #e6edf3)}.terms-gate__subtitle{font-size:13px;color:var(--text-dim, #9da7b3);margin:0 0 16px}.terms-gate__body{overflow-y:auto;flex:1 1 auto;min-height:0;padding-right:8px;border-top:1px solid rgba(125,135,150,.15);border-bottom:1px solid rgba(125,135,150,.15);padding-top:14px;padding-bottom:14px}.terms-gate__h2{font-size:14px;font-weight:600;margin:0 0 10px;color:var(--text, #e6edf3)}.terms-gate__summary{margin:0 0 14px;padding-left:20px}.terms-gate__summary li{font-size:13px;line-height:1.6;color:var(--text-dim, #c2cbd6);margin-bottom:8px}.terms-gate__viewfull{background:none;border:none;color:var(--accent, #3b82f6);cursor:pointer;font-size:13px;padding:4px 0;text-decoration:underline}.terms-gate__fulltext{white-space:pre-wrap;word-break:break-word;font-size:12px;line-height:1.6;color:var(--text-dim, #b3bcc8);background:#0003;border-radius:8px;padding:14px;margin:10px 0 0;font-family:inherit}.terms-gate__scrollhint{font-size:12px;color:#fbbf24;text-align:center;margin:12px 0 0}.terms-gate__actions{display:flex;gap:12px;justify-content:flex-end;margin-top:18px}.terms-gate__btn{font-size:14px;padding:10px 22px;border-radius:9px;border:none;cursor:pointer;background:var(--accent, #3b82f6);color:#fff;font-weight:600}.terms-gate__btn:disabled{opacity:.45;cursor:not-allowed}.terms-gate__btn--ghost{background:transparent;border:1px solid rgba(125,135,150,.35);color:var(--text-dim, #9da7b3);font-weight:500}
@@ -6,8 +6,8 @@
6
6
  <link rel="icon" type="image/svg+xml" href="./favicon.svg" />
7
7
  <link rel="icon" type="image/png" sizes="256x256" href="./favicon-256.png" />
8
8
  <title>CiCy Desktop</title>
9
- <script type="module" crossorigin src="./assets/index-BtVG2Py6.js"></script>
10
- <link rel="stylesheet" crossorigin href="./assets/index-CKpaMBKz.css">
9
+ <script type="module" crossorigin src="./assets/index-B04YSZUc.js"></script>
10
+ <link rel="stylesheet" crossorigin href="./assets/index-Bs9ihcPL.css">
11
11
  </head>
12
12
  <body>
13
13
  <div id="root"></div>
@@ -1,7 +1,7 @@
1
1
  const fs = require("fs");
2
2
  const os = require("os");
3
3
  const path = require("path");
4
- const { spawn } = require("child_process");
4
+ const { spawn, execFileSync } = require("child_process");
5
5
  const { isPortOpen } = require("../utils/process-utils");
6
6
  const { waitForDebugger, getVersion } = require("./chrome-cdp-client");
7
7
 
@@ -73,19 +73,79 @@ function isDirectPath(binaryPath) {
73
73
  return binaryPath.includes(path.sep) || (process.platform === "win32" && /^[a-zA-Z]:\\/.test(binaryPath));
74
74
  }
75
75
 
76
+ // Windows: Chrome registers its exact install path under "App Paths" on install,
77
+ // independent of where it landed (per-user vs per-machine, custom drive). This is
78
+ // far more reliable than the %ProgramFiles% guesses — those miss per-user installs
79
+ // and break when the launching process runs with a stripped env (e.g. the
80
+ // StartElectron scheduled task), which is why an installed Chrome can still read
81
+ // as "not found".
82
+ function queryWindowsChromeFromRegistry() {
83
+ if (process.platform !== "win32") return null;
84
+ const keys = [
85
+ "HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\chrome.exe",
86
+ "HKCU\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\chrome.exe",
87
+ "HKLM\\SOFTWARE\\WOW6432Node\\Microsoft\\Windows\\CurrentVersion\\App Paths\\chrome.exe",
88
+ ];
89
+ for (const key of keys) {
90
+ try {
91
+ const out = execFileSync("reg", ["query", key, "/ve"], {
92
+ encoding: "utf8",
93
+ windowsHide: true,
94
+ timeout: 5000,
95
+ });
96
+ const m = out.match(/REG_SZ\s+(.+?\.exe)\s*$/im);
97
+ if (m && fs.existsSync(m[1].trim())) return m[1].trim();
98
+ } catch (_) {}
99
+ }
100
+ return null;
101
+ }
102
+
103
+ // PATH lookup for a bare command — `where` on Windows, `which` on posix. Returns
104
+ // the first existing match, else null.
105
+ function whichBinary(cmd) {
106
+ try {
107
+ const tool = process.platform === "win32" ? "where" : "which";
108
+ const out = execFileSync(tool, [cmd], { encoding: "utf8", windowsHide: true, timeout: 5000 });
109
+ const first = out.split(/\r?\n/).map((s) => s.trim()).filter(Boolean)[0];
110
+ if (first && fs.existsSync(first)) return first;
111
+ } catch (_) {}
112
+ return null;
113
+ }
114
+
76
115
  function resolveChromeBinary(binaryPath) {
77
- const candidates = [binaryPath, ...getBinaryCandidates()].filter(Boolean);
116
+ // 1) Explicit override (config.chromeBinary / --chrome-binary) that exists.
117
+ if (binaryPath && isDirectPath(binaryPath) && fs.existsSync(binaryPath)) return binaryPath;
118
+
119
+ // 2) Windows registry App Paths — authoritative, install-location independent.
120
+ const regPath = queryWindowsChromeFromRegistry();
121
+ if (regPath) return regPath;
78
122
 
123
+ // 3) Platform candidates: concrete paths checked for existence; bare commands
124
+ // resolved via PATH (where/which) so we only accept a Chrome that's actually
125
+ // present.
126
+ const candidates = [binaryPath, ...getBinaryCandidates()].filter(Boolean);
127
+ const bareCommands = [];
79
128
  for (const candidate of candidates) {
80
129
  if (isDirectPath(candidate)) {
81
- if (fs.existsSync(candidate)) {
82
- return candidate;
83
- }
84
- continue;
130
+ if (fs.existsSync(candidate)) return candidate;
131
+ } else {
132
+ bareCommands.push(candidate);
133
+ const resolved = whichBinary(candidate);
134
+ if (resolved) return resolved;
85
135
  }
86
- return candidate;
87
136
  }
88
137
 
138
+ // 4) Name-based last resort.
139
+ const byName =
140
+ whichBinary(process.platform === "win32" ? "chrome" : "google-chrome") ||
141
+ whichBinary(process.platform === "win32" ? "chrome.exe" : "chromium");
142
+ if (byName) return byName;
143
+
144
+ // 5) Nothing concrete found. On posix, let spawn try the first bare command
145
+ // (covers exotic PATH setups where `which` itself isn't available); on
146
+ // Windows everything is a concrete path, so fail with a clear message.
147
+ if (bareCommands.length) return bareCommands[0];
148
+
89
149
  throw new Error(
90
150
  "Chrome/Chromium binary not found. Please configure chromeBinary or --chrome-binary."
91
151
  );
@@ -178,13 +178,17 @@ async function ensureDownloaded(url, dest, mirror, { emit, phase, label, freshOn
178
178
  let lastPct = -1; // throttle: chunks arrive dozens/s — only emit on whole-percent change
179
179
  const attempted = withRetry(async (attempt) => {
180
180
  const src = sources[Math.min(attempt - 1, sources.length - 1)];
181
+ // 断点续传 (主人): resume the partial via a Range request instead of
182
+ // restarting from 0 — efficient on a flaky network. The post-download size
183
+ // check below + loadImage's load-failure cleanup guard against a bad partial.
181
184
  await download(src, dest, {
182
185
  resume: true,
183
186
  onProgress: ({ received, total }) => {
184
187
  const pct = total ? Math.round((received / total) * 100) : 0;
185
188
  if (pct === lastPct) return;
186
189
  lastPct = pct;
187
- emit && emit({ phase, status: "running", message: label, progress: pct, received, total });
190
+ // `url` lets the drawer show the actual source (incl. mirror fallback).
191
+ emit && emit({ phase, status: "running", message: label, progress: pct, received, total, url: src });
188
192
  },
189
193
  });
190
194
  if (expected > 0) {
@@ -251,7 +255,7 @@ function imageTarballPath() {
251
255
  // install (主人: 装 Docker 的同时下载 R2 镜像). Returns the tarball path.
252
256
  async function downloadImageTarball({ emit } = {}) {
253
257
  const dest = imageTarballPath();
254
- await ensureDownloaded(R2_TARBALL, dest, null, { emit, phase: "image", label: "下载镜像", freshOnIncomplete: true });
258
+ await ensureDownloaded(R2_TARBALL, dest, null, { emit, phase: "image", label: "下载镜像" });
255
259
  return dest;
256
260
  }
257
261
 
@@ -260,7 +264,16 @@ async function downloadImageTarball({ emit } = {}) {
260
264
  async function loadImageFromTarball(tmp, { emit } = {}) {
261
265
  emit && emit({ phase: "image", status: "running", message: "docker load…", progress: 100 });
262
266
  console.log(`[docker-sidecar] docker load…`);
263
- const { stdout } = await run(["load", "-i", tmp], { timeout: 300000 });
267
+ let stdout;
268
+ try {
269
+ ({ stdout } = await run(["load", "-i", tmp], { timeout: 300000 }));
270
+ } catch (e) {
271
+ // A resumed download can leave a byte-correct-size but corrupt tarball that
272
+ // `docker load` rejects. Delete it so the next attempt re-downloads fresh
273
+ // (断点续传 normally, fresh only when proven bad).
274
+ try { fs.unlinkSync(tmp); } catch {}
275
+ throw e;
276
+ }
264
277
  // The tarball's embedded tag may be a pinned version (e.g. :2.1.6) while we
265
278
  // run IMAGE (:latest). Re-tag whatever was loaded so imagePresent()/start()
266
279
  // match — otherwise every start() re-downloads the tarball forever.
@@ -364,7 +377,7 @@ async function installDocker({ emit, dest } = {}) {
364
377
  try { fs.mkdirSync(path.dirname(target), { recursive: true }); } catch {}
365
378
  e({ phase: "install-docker", status: "running", message: "下载 Docker Desktop 安装包…", progress: 0 });
366
379
  await ensureDownloaded(DOCKER_DESKTOP_URL, target, DOCKER_DESKTOP_MIRROR, {
367
- emit, phase: "install-docker", label: "下载 Docker Desktop", freshOnIncomplete: true,
380
+ emit, phase: "install-docker", label: "下载 Docker Desktop",
368
381
  });
369
382
  e({ phase: "install-docker", status: "running", message: "安装 Docker Desktop(请在弹出的授权框点「是」,装完可能需重启)…" });
370
383
  await new Promise((resolve) => {
@@ -213,6 +213,13 @@ body {
213
213
  .user-chip__menu-item.is-danger { color: #f7a3a3; }
214
214
  .user-chip__menu-item.is-danger:hover { background: rgba(239,68,68,.16); color: #fff; }
215
215
  .user-chip__menu-sep { height: 1px; margin: 4px 2px; background: rgba(255,255,255,.08); }
216
+ /* cicy-desktop version — bottom of the avatar dropdown (主人) */
217
+ .user-chip__menu-version {
218
+ margin-top: 4px; padding: 8px 11px 4px;
219
+ font-size: 11px; color: #6b7280; text-align: center;
220
+ font-variant-numeric: tabular-nums; user-select: text;
221
+ border-top: 1px solid rgba(255,255,255,.06);
222
+ }
216
223
  /* HTTPS audit tip, rendered as a flat menu row with an on/off switch */
217
224
  .user-chip__mitm-row {
218
225
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
@@ -969,6 +976,34 @@ body {
969
976
  background: rgba(10, 12, 16, 0.7); border: 1px solid rgba(125, 135, 150, 0.12); border-radius: 10px;
970
977
  font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 11.5px; line-height: 1.7;
971
978
  }
979
+ /* Custom thin scrollbar for the install log (主人: scroll 用自定义 style) */
980
+ .drawer__log--scroll { max-height: 168px; scrollbar-width: thin; scrollbar-color: rgba(125,135,150,.4) transparent; }
981
+ .drawer__log--scroll::-webkit-scrollbar { width: 7px; }
982
+ .drawer__log--scroll::-webkit-scrollbar-track { background: transparent; margin: 4px 0; }
983
+ .drawer__log--scroll::-webkit-scrollbar-thumb { background: rgba(125,135,150,.32); border-radius: 6px; border: 1.5px solid transparent; background-clip: padding-box; }
984
+ .drawer__log--scroll::-webkit-scrollbar-thumb:hover { background: rgba(125,135,150,.55); background-clip: padding-box; }
985
+
986
+ /* Fixed download progress bars (Docker Desktop / 基础镜像) — no scroll spam */
987
+ .drawer__dlbars { display: flex; flex-direction: column; gap: 10px; margin: 12px 14px 2px; }
988
+ .dlbar {
989
+ padding: 10px 12px; border-radius: 10px;
990
+ background: rgba(10, 12, 16, 0.55); border: 1px solid rgba(125, 135, 150, 0.14);
991
+ }
992
+ .dlbar__head { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; margin-bottom: 7px; }
993
+ .dlbar__name { font-size: 12.5px; font-weight: 600; color: var(--text, #e6eaf0); }
994
+ .dlbar__pct { font-size: 11px; color: var(--text-dim, #9da7b3); font-variant-numeric: tabular-nums; }
995
+ .dlbar__track { height: 6px; border-radius: 4px; background: rgba(125, 135, 150, 0.16); overflow: hidden; }
996
+ .dlbar__fill {
997
+ height: 100%; border-radius: 4px;
998
+ background: linear-gradient(90deg, #5b8df7, #2496ed);
999
+ transition: width .2s ease;
1000
+ }
1001
+ .dlbar__fill.is-done { background: #4ade80; }
1002
+ .dlbar__url {
1003
+ margin-top: 7px; font-size: 10.5px; color: #6b7686;
1004
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
1005
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
1006
+ }
972
1007
  .drawer__log-empty { color: var(--text-dim, #9da7b3); }
973
1008
  .drawer__line { display: flex; align-items: baseline; gap: 8px; padding: 1px 0; }
974
1009
  .drawer__t { color: #6b7686; flex: none; font-variant-numeric: tabular-nums; }
@@ -189,7 +189,6 @@ function UpdateDrawerHost() {
189
189
  {running ? (
190
190
  <>
191
191
  <span className="drawer__foot-status">更新进行中…</span>
192
- <button type="button" className="drawer__btn" data-id="UpdateDrawer-background" onClick={() => updateDrawer.close()}>在后台继续</button>
193
192
  </>
194
193
  ) : st.status === "error" ? (
195
194
  <>
@@ -1022,7 +1021,14 @@ function Header({ me, welcome, onLogout, mitmTeam }) {
1022
1021
  const [trustOpen, setTrustOpen] = useState(false);
1023
1022
  const [auditOpen, setAuditOpen] = useState(false);
1024
1023
  const [termsOpen, setTermsOpen] = useState(false);
1024
+ const [appVer, setAppVer] = useState("");
1025
1025
  const wrap = useRef(null);
1026
+ // cicy-desktop's own version, shown at the very bottom of this menu (主人).
1027
+ useEffect(() => {
1028
+ let alive = true;
1029
+ window.cicy?.app?.getVersion?.().then((v) => { if (alive) setAppVer(String(v || "")); }).catch(() => {});
1030
+ return () => { alive = false; };
1031
+ }, []);
1026
1032
  // Click-outside closes the dropdown (mirrors LocalTeamCard's ⋯ menu).
1027
1033
  useEffect(() => {
1028
1034
  if (!open) return;
@@ -1077,6 +1083,9 @@ function Header({ me, welcome, onLogout, mitmTeam }) {
1077
1083
  <button type="button" data-id="UserChip-logout" className="user-chip__menu-item is-danger" onClick={() => { setOpen(false); onLogout(); }}>
1078
1084
  退出
1079
1085
  </button>
1086
+ <div className="user-chip__menu-version" data-id="UserChip-version">
1087
+ CiCy Desktop {appVer ? `v${appVer}` : "…"}
1088
+ </div>
1080
1089
  </div>
1081
1090
  )}
1082
1091
  </div>
@@ -1435,14 +1444,30 @@ let dockerDrawerState = null; // null = closed
1435
1444
  function emitDockerDrawer() { dockerDrawerListeners.forEach((l) => l(dockerDrawerState)); }
1436
1445
  const dockerDrawer = {
1437
1446
  open({ onRetry } = {}) {
1438
- dockerDrawerState = { status: "running", phase: "install-docker", logs: [], onRetry: onRetry || null, lastAt: Date.now() };
1447
+ dockerDrawerState = { status: "running", phase: "install-docker", logs: [], bars: {}, onRetry: onRetry || null, lastAt: Date.now() };
1439
1448
  emitDockerDrawer();
1440
1449
  },
1441
1450
  push(ev = {}) {
1442
1451
  if (!dockerDrawerState) return;
1443
1452
  const phase = ev.phase === "health" ? "container" : (ev.phase || dockerDrawerState.phase);
1444
- const line = { id: ++dockerDrawerLogSeq, t: clockHHMMSS(), phase, status: ev.status || "running", message: ev.message || "", progress: ev.progress };
1445
- dockerDrawerState = { ...dockerDrawerState, phase, logs: [...dockerDrawerState.logs, line], lastAt: Date.now() };
1453
+ const next = { ...dockerDrawerState, phase, lastAt: Date.now() };
1454
+ const hasPct = Number.isFinite(ev.progress);
1455
+ // Per-byte download ticks (status running + a %) drive a PROGRESS BAR, not a
1456
+ // log line — so the log doesn't scroll-spam (主人: 下载不要输出滚动/日志太多).
1457
+ const isDownloadTick = ev.status === "running" && hasPct && (phase === "install-docker" || phase === "image");
1458
+ if (isDownloadTick) {
1459
+ const prev = dockerDrawerState.bars?.[phase] || {};
1460
+ next.bars = { ...dockerDrawerState.bars, [phase]: { progress: ev.progress, received: ev.received, total: ev.total, url: ev.url || prev.url, message: ev.message || prev.message } };
1461
+ } else {
1462
+ // Meaningful event → one log line (phase change / skip / done / error / retry).
1463
+ const line = { id: ++dockerDrawerLogSeq, t: clockHHMMSS(), phase, status: ev.status || "running", message: ev.message || "" };
1464
+ next.logs = [...dockerDrawerState.logs, line];
1465
+ if (ev.url) {
1466
+ const prev = dockerDrawerState.bars?.[phase] || {};
1467
+ next.bars = { ...dockerDrawerState.bars, [phase]: { ...prev, url: ev.url } };
1468
+ }
1469
+ }
1470
+ dockerDrawerState = next;
1446
1471
  emitDockerDrawer();
1447
1472
  },
1448
1473
  finish({ ok, message } = {}) {
@@ -1456,6 +1481,30 @@ const dockerDrawer = {
1456
1481
  };
1457
1482
  const DOCKER_PHASES = [["install-docker", "装 Docker"], ["image", "加载镜像"], ["container", "启动容器"], ["done", "完成"]];
1458
1483
  const DOCKER_BADGE = { "install-docker": "Docker", image: "镜像", container: "容器", health: "容器", done: "完成" };
1484
+ const DOCKER_DL_LABEL = { "install-docker": "Docker Desktop", image: "基础镜像" };
1485
+ function fmtBytes(n) {
1486
+ if (!Number.isFinite(n)) return "?";
1487
+ if (n < 1024) return n + " B";
1488
+ if (n < 1048576) return (n / 1024).toFixed(0) + " KB";
1489
+ if (n < 1073741824) return (n / 1048576).toFixed(1) + " MB";
1490
+ return (n / 1073741824).toFixed(2) + " GB";
1491
+ }
1492
+ // One fixed (non-scrolling) progress bar per download (Docker Desktop / image),
1493
+ // showing the source URL + % + bytes (主人: 下载做进度条、显示地址、不要滚动).
1494
+ function DownloadBar({ phaseKey, bar }) {
1495
+ const pct = Number.isFinite(bar?.progress) ? Math.max(0, Math.min(100, bar.progress)) : 0;
1496
+ const done = pct >= 100;
1497
+ return (
1498
+ <div className="dlbar" data-id={`DockerDrawer-dlbar-${phaseKey}`}>
1499
+ <div className="dlbar__head">
1500
+ <span className="dlbar__name">{DOCKER_DL_LABEL[phaseKey] || phaseKey}</span>
1501
+ <span className="dlbar__pct">{pct}%{bar?.total ? ` · ${fmtBytes(bar.received)} / ${fmtBytes(bar.total)}` : ""}</span>
1502
+ </div>
1503
+ <div className="dlbar__track"><div className={`dlbar__fill${done ? " is-done" : ""}`} style={{ width: `${pct}%` }} /></div>
1504
+ {bar?.url && <div className="dlbar__url" title={bar.url}>{bar.url}</div>}
1505
+ </div>
1506
+ );
1507
+ }
1459
1508
  function DockerInstallDrawerHost() {
1460
1509
  const [st, setSt] = useState(dockerDrawerState);
1461
1510
  useEffect(() => { dockerDrawerListeners.add(setSt); return () => { dockerDrawerListeners.delete(setSt); }; }, []);
@@ -1464,6 +1513,7 @@ function DockerInstallDrawerHost() {
1464
1513
  if (!st) return null;
1465
1514
  const running = st.status === "running";
1466
1515
  const phaseIdx = DOCKER_PHASES.findIndex(([k]) => k === st.phase);
1516
+ const dlBars = ["install-docker", "image"].filter((k) => st.bars?.[k]);
1467
1517
  return (
1468
1518
  <div className="drawer-scrim" data-id="DockerDrawer-scrim" onClick={() => { if (!running) dockerDrawer.close(); }}>
1469
1519
  <div className="drawer" data-id="DockerDrawer" data-status={st.status} onClick={(e) => e.stopPropagation()}>
@@ -1495,14 +1545,20 @@ function DockerInstallDrawerHost() {
1495
1545
  })}
1496
1546
  </div>
1497
1547
 
1498
- <div className="drawer__log" data-id="DockerDrawer-log" ref={logRef}>
1548
+ {dlBars.length > 0 && (
1549
+ <div className="drawer__dlbars" data-id="DockerDrawer-dlbars">
1550
+ {dlBars.map((k) => <DownloadBar key={k} phaseKey={k} bar={st.bars[k]} />)}
1551
+ </div>
1552
+ )}
1553
+
1554
+ <div className="drawer__log drawer__log--scroll" data-id="DockerDrawer-log" ref={logRef}>
1499
1555
  {st.logs.length === 0
1500
1556
  ? <div className="drawer__log-empty">{tr("docker.preparing", "准备中…")}</div>
1501
1557
  : st.logs.map((l) => (
1502
1558
  <div key={l.id} className="drawer__line" data-status={l.status}>
1503
1559
  <span className="drawer__t">{l.t}</span>
1504
1560
  <span className={`drawer__badge drawer__badge--${l.phase}`}>{DOCKER_BADGE[l.phase] || l.phase}</span>
1505
- <span className="drawer__linemsg">{l.message}{Number.isFinite(l.progress) ? ` ${l.progress}%` : ""}</span>
1561
+ <span className="drawer__linemsg">{l.message}</span>
1506
1562
  </div>
1507
1563
  ))}
1508
1564
  </div>
@@ -1511,7 +1567,6 @@ function DockerInstallDrawerHost() {
1511
1567
  {running ? (
1512
1568
  <>
1513
1569
  <span className="drawer__foot-status">{tr("docker.installing2", "安装进行中…")}</span>
1514
- <button type="button" className="drawer__btn" data-id="DockerDrawer-background" onClick={() => dockerDrawer.close()}>{tr("docker.background", "在后台继续")}</button>
1515
1570
  </>
1516
1571
  ) : st.status === "error" ? (
1517
1572
  <>