cicy-desktop 2.1.69 → 2.1.71

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.
Files changed (61) hide show
  1. package/package.json +7 -7
  2. package/src/backends/homepage-react/assets/index-BpljolQs.js +365 -0
  3. package/src/backends/homepage-react/assets/{index-BniEbx_j.css → index-C9AZlTew.css} +1 -1
  4. package/src/backends/homepage-react/index.html +2 -2
  5. package/src/backends/local-teams.js +42 -4
  6. package/src/backends/sidecar-ipc.js +23 -1
  7. package/src/i18n/locales/en.json +9 -7
  8. package/src/i18n/locales/zh-CN.json +9 -7
  9. package/src/sidecar/cicy-code.js +54 -111
  10. package/src/sidecar/localbin.js +133 -0
  11. package/src/sidecar/native.js +4 -2
  12. package/src/sidecar/runtime.js +7 -3
  13. package/workers/render/src/App.css +156 -10
  14. package/workers/render/src/App.jsx +254 -39
  15. package/.env.dev +0 -7
  16. package/src/backends/homepage-react/assets/index-B8gGhz8B.js +0 -365
  17. package/workers/render.bak.20260528-2338/DESIGN_v2.md +0 -254
  18. package/workers/render.bak.20260528-2338/index.html +0 -12
  19. package/workers/render.bak.20260528-2338/package-lock.json +0 -827
  20. package/workers/render.bak.20260528-2338/package.json +0 -19
  21. package/workers/render.bak.20260528-2338/public/_headers +0 -5
  22. package/workers/render.bak.20260528-2338/public/manifest.json +0 -6
  23. package/workers/render.bak.20260528-2338/src/App.css +0 -224
  24. package/workers/render.bak.20260528-2338/src/App.jsx +0 -1028
  25. package/workers/render.bak.20260528-2338/src/api.js +0 -285
  26. package/workers/render.bak.20260528-2338/src/cicycode-ops.js +0 -222
  27. package/workers/render.bak.20260528-2338/src/components/BackendCard.css +0 -299
  28. package/workers/render.bak.20260528-2338/src/components/BackendCard.jsx +0 -133
  29. package/workers/render.bak.20260528-2338/src/components/BackendModal.css +0 -161
  30. package/workers/render.bak.20260528-2338/src/components/BackendModal.jsx +0 -199
  31. package/workers/render.bak.20260528-2338/src/components/Button.css +0 -72
  32. package/workers/render.bak.20260528-2338/src/components/Button.jsx +0 -37
  33. package/workers/render.bak.20260528-2338/src/components/Card.css +0 -42
  34. package/workers/render.bak.20260528-2338/src/components/Card.jsx +0 -21
  35. package/workers/render.bak.20260528-2338/src/components/Icon.jsx +0 -30
  36. package/workers/render.bak.20260528-2338/src/components/Menu.css +0 -55
  37. package/workers/render.bak.20260528-2338/src/components/Menu.jsx +0 -91
  38. package/workers/render.bak.20260528-2338/src/components/SidecarBanner.css +0 -79
  39. package/workers/render.bak.20260528-2338/src/components/SidecarBanner.jsx +0 -84
  40. package/workers/render.bak.20260528-2338/src/components/StatusChip.css +0 -19
  41. package/workers/render.bak.20260528-2338/src/components/StatusChip.jsx +0 -31
  42. package/workers/render.bak.20260528-2338/src/components/Toast.css +0 -31
  43. package/workers/render.bak.20260528-2338/src/components/Toast.jsx +0 -23
  44. package/workers/render.bak.20260528-2338/src/components/WslSetupBanner.css +0 -464
  45. package/workers/render.bak.20260528-2338/src/components/WslSetupBanner.jsx +0 -716
  46. package/workers/render.bak.20260528-2338/src/dockerInstaller.js +0 -0
  47. package/workers/render.bak.20260528-2338/src/i18n/en.json +0 -116
  48. package/workers/render.bak.20260528-2338/src/i18n/fr.json +0 -116
  49. package/workers/render.bak.20260528-2338/src/i18n/index.js +0 -69
  50. package/workers/render.bak.20260528-2338/src/i18n/ja.json +0 -116
  51. package/workers/render.bak.20260528-2338/src/i18n/zh-CN.json +0 -121
  52. package/workers/render.bak.20260528-2338/src/main.js +0 -475
  53. package/workers/render.bak.20260528-2338/src/main.jsx +0 -18
  54. package/workers/render.bak.20260528-2338/src/style.css +0 -275
  55. package/workers/render.bak.20260528-2338/src/styles/base.css +0 -98
  56. package/workers/render.bak.20260528-2338/src/styles/tokens.css +0 -90
  57. package/workers/render.bak.20260528-2338/src/tos.js +0 -72
  58. package/workers/render.bak.20260528-2338/src/worker.js +0 -40
  59. package/workers/render.bak.20260528-2338/src/wslInstaller.js +0 -1563
  60. package/workers/render.bak.20260528-2338/vite.config.js +0 -36
  61. package/workers/render.bak.20260528-2338/wrangler.toml +0 -17
@@ -1,4 +1,5 @@
1
1
  import { useEffect, useState, useCallback, useMemo, useRef } from "react";
2
+ import { createPortal } from "react-dom";
2
3
  import "./App.css";
3
4
  import { TERMS_VERSION, TERMS_FULL } from "./termsText";
4
5
 
@@ -15,6 +16,190 @@ const ACCESS_TOKEN_KEY = "cicy_access_token";
15
16
  const USER_ID_KEY = "cicy_user_id";
16
17
  const CLOUD_BASE = "https://cicy-ai.com";
17
18
 
19
+ // ── Toast: lightweight global notifications (bottom-right). Pub/sub store so
20
+ // any component can push without prop-drilling — one <ToastHost/> at the shell
21
+ // root renders them. Used for 更新/启动/重启 progress + result so feedback floats
22
+ // over the UI instead of being buried inside a card. show() upserts by id, so a
23
+ // long-running op keeps ONE toast and just streams message/progress into it.
24
+ const toastListeners = new Set();
25
+ let toastSeq = 0;
26
+ let toastItems = [];
27
+ const toastTimers = new Map();
28
+ function emitToasts() { toastListeners.forEach((l) => l(toastItems)); }
29
+ const toast = {
30
+ show(opts = {}) {
31
+ const id = opts.id || `t${++toastSeq}`;
32
+ const prev = toastItems.find((t) => t.id === id);
33
+ const next = { id, status: "running", ...prev, ...opts };
34
+ toastItems = prev ? toastItems.map((t) => (t.id === id ? next : t)) : [...toastItems, next];
35
+ emitToasts();
36
+ const old = toastTimers.get(id);
37
+ if (old) { clearTimeout(old); toastTimers.delete(id); }
38
+ if (opts.ttl) toastTimers.set(id, setTimeout(() => toast.dismiss(id), opts.ttl));
39
+ return id;
40
+ },
41
+ dismiss(id) {
42
+ toastItems = toastItems.filter((t) => t.id !== id);
43
+ const tm = toastTimers.get(id);
44
+ if (tm) { clearTimeout(tm); toastTimers.delete(id); }
45
+ emitToasts();
46
+ },
47
+ };
48
+ function ToastHost() {
49
+ const [items, setItems] = useState(toastItems);
50
+ useEffect(() => { toastListeners.add(setItems); return () => { toastListeners.delete(setItems); }; }, []);
51
+ if (!items.length) return null;
52
+ return (
53
+ <div className="toast-host" data-id="ToastHost">
54
+ {items.map((t) => (
55
+ <div key={t.id} className="toast" data-id={`Toast-${t.id}`} data-status={t.status || "running"}>
56
+ <button type="button" className="toast__x" data-id="Toast-dismiss" onClick={() => toast.dismiss(t.id)} aria-label="dismiss">×</button>
57
+ <span className="toast__msg">
58
+ {t.message}{Number.isFinite(t.progress) ? ` ${t.progress}%` : ""}
59
+ </span>
60
+ {Number.isFinite(t.progress) && (
61
+ <span className="toast__bar"><span style={{ width: `${Math.min(100, t.progress)}%` }} /></span>
62
+ )}
63
+ </div>
64
+ ))}
65
+ </div>
66
+ );
67
+ }
68
+
69
+ // ── Update drawer: a bottom sheet that streams the live update log (下载→切换→
70
+ // 完成), surfaces the exact step it's on, and offers 重试 when it fails — so a
71
+ // stuck/slow update is legible and recoverable instead of a frozen "更新中…".
72
+ // The sidecar update op emits {phase,status,message} on 'sidecar:op-progress';
73
+ // runOp tees those into this store. Single global instance, mounted at shell root.
74
+ const drawerListeners = new Set();
75
+ let drawerLogSeq = 0;
76
+ let drawerState = null; // null = closed
77
+ function emitDrawer() { drawerListeners.forEach((l) => l(drawerState)); }
78
+ function clockHHMMSS() { const d = new Date(); return d.toTimeString().slice(0, 8); }
79
+ const updateDrawer = {
80
+ open({ teamId, fromVer, toVer, onRetry } = {}) {
81
+ drawerState = {
82
+ teamId, fromVer: fromVer || null, toVer: toVer || null,
83
+ status: "running", // running | done | error
84
+ phase: "download", // download | swap | done
85
+ logs: [],
86
+ onRetry: onRetry || null,
87
+ lastAt: Date.now(),
88
+ };
89
+ emitDrawer();
90
+ },
91
+ push(ev = {}) {
92
+ if (!drawerState) return;
93
+ const line = { id: ++drawerLogSeq, t: clockHHMMSS(), phase: ev.phase || drawerState.phase, status: ev.status || "running", message: ev.message || "" };
94
+ drawerState = {
95
+ ...drawerState,
96
+ phase: ev.phase || drawerState.phase,
97
+ toVer: ev.toVer || drawerState.toVer,
98
+ logs: [...drawerState.logs, line],
99
+ lastAt: Date.now(),
100
+ };
101
+ emitDrawer();
102
+ },
103
+ finish({ ok, message } = {}) {
104
+ if (!drawerState) return;
105
+ const status = ok ? "done" : "error";
106
+ const line = { id: ++drawerLogSeq, t: clockHHMMSS(), phase: "done", status, message: message || (ok ? "更新完成" : "更新失败") };
107
+ drawerState = { ...drawerState, status, phase: "done", logs: [...drawerState.logs, line], lastAt: Date.now() };
108
+ emitDrawer();
109
+ },
110
+ close() { drawerState = null; emitDrawer(); },
111
+ };
112
+ const DRAWER_PHASES = [["download", "下载"], ["swap", "切换"], ["done", "完成"]];
113
+ function UpdateDrawerHost() {
114
+ const [st, setSt] = useState(drawerState);
115
+ useEffect(() => { drawerListeners.add(setSt); return () => { drawerListeners.delete(setSt); }; }, []);
116
+ const logRef = useRef(null);
117
+ useEffect(() => { const el = logRef.current; if (el) el.scrollTop = el.scrollHeight; }, [st?.logs?.length]);
118
+ // Stuck detector: running but no new log line for 25s → the verify/probe wait
119
+ // is taking long; nudge the user (they can keep it in the background).
120
+ const [stuck, setStuck] = useState(false);
121
+ useEffect(() => {
122
+ if (!st || st.status !== "running") { setStuck(false); return; }
123
+ const id = setInterval(() => setStuck(Date.now() - (st.lastAt || 0) > 25000), 3000);
124
+ return () => clearInterval(id);
125
+ }, [st?.lastAt, st?.status]);
126
+
127
+ if (!st) return null;
128
+ const running = st.status === "running";
129
+ const phaseIdx = DRAWER_PHASES.findIndex(([k]) => k === st.phase);
130
+ return (
131
+ <div className="drawer-scrim" data-id="UpdateDrawer-scrim" onClick={() => { if (!running) updateDrawer.close(); }}>
132
+ <div className="drawer" data-id="UpdateDrawer" data-status={st.status} onClick={(e) => e.stopPropagation()}>
133
+ <div className="drawer__head">
134
+ <div className="drawer__title">
135
+ <span className={`drawer__spark drawer__spark--${st.status}`}>
136
+ {running ? <Spinner /> : st.status === "done" ? "✓" : "!"}
137
+ </span>
138
+ <div>
139
+ <div className="drawer__h">更新 cicy-code</div>
140
+ <div className="drawer__sub">{st.fromVer ? `v${st.fromVer}` : "当前"} → {st.toVer ? `v${st.toVer}` : "最新版"}</div>
141
+ </div>
142
+ </div>
143
+ <button type="button" className="drawer__x" data-id="UpdateDrawer-close" disabled={running} title={running ? "更新进行中" : "关闭"} onClick={() => updateDrawer.close()} aria-label="close">×</button>
144
+ </div>
145
+
146
+ <div className="drawer__steps" data-id="UpdateDrawer-steps">
147
+ {DRAWER_PHASES.map(([k, label], i) => {
148
+ const done = st.status === "done" || i < phaseIdx;
149
+ const active = i === phaseIdx && running;
150
+ const err = st.status === "error" && i === phaseIdx;
151
+ return (
152
+ <div key={k} className={`drawer__step${active ? " is-active" : ""}${done ? " is-done" : ""}${err ? " is-error" : ""}`}>
153
+ <span className="drawer__step-dot">{done ? "✓" : err ? "!" : i + 1}</span>
154
+ <span className="drawer__step-label">{label}</span>
155
+ {i < DRAWER_PHASES.length - 1 && <span className="drawer__step-bar" />}
156
+ </div>
157
+ );
158
+ })}
159
+ </div>
160
+
161
+ <div className="drawer__log" data-id="UpdateDrawer-log" ref={logRef}>
162
+ {st.logs.length === 0
163
+ ? <div className="drawer__log-empty">准备中…</div>
164
+ : st.logs.map((l) => (
165
+ <div key={l.id} className="drawer__line" data-status={l.status}>
166
+ <span className="drawer__t">{l.t}</span>
167
+ <span className={`drawer__badge drawer__badge--${l.phase}`}>{({ download: "下载", swap: "切换", done: "完成" })[l.phase] || l.phase}</span>
168
+ <span className="drawer__linemsg">{l.message}</span>
169
+ </div>
170
+ ))}
171
+ </div>
172
+
173
+ {stuck && running && (
174
+ <div className="drawer__hint" data-id="UpdateDrawer-stuck">
175
+ 正在等待新版本就绪,耗时比平常久。可以放到后台继续,完成或失败都会提示。
176
+ </div>
177
+ )}
178
+
179
+ <div className="drawer__foot">
180
+ {running ? (
181
+ <>
182
+ <span className="drawer__foot-status">更新进行中…</span>
183
+ <button type="button" className="drawer__btn" data-id="UpdateDrawer-background" onClick={() => updateDrawer.close()}>在后台继续</button>
184
+ </>
185
+ ) : st.status === "error" ? (
186
+ <>
187
+ <span className="drawer__foot-status is-error">更新失败</span>
188
+ {st.onRetry && <button type="button" className="drawer__btn is-accent" data-id="UpdateDrawer-retry" onClick={() => st.onRetry()}>重试</button>}
189
+ <button type="button" className="drawer__btn" data-id="UpdateDrawer-dismiss" onClick={() => updateDrawer.close()}>关闭</button>
190
+ </>
191
+ ) : (
192
+ <>
193
+ <span className="drawer__foot-status is-done">已更新到最新</span>
194
+ <button type="button" className="drawer__btn is-accent" data-id="UpdateDrawer-finish" onClick={() => updateDrawer.close()}>完成</button>
195
+ </>
196
+ )}
197
+ </div>
198
+ </div>
199
+ </div>
200
+ );
201
+ }
202
+
18
203
  export default function App() {
19
204
  // First-run terms gate (合规第一道整体同意) — blocks the whole UI until
20
205
  // accepted. undefined = checking, false = must show gate, true = past it.
@@ -429,6 +614,8 @@ export default function App() {
429
614
  )}
430
615
  </main>
431
616
  </div>{/* /.shell__left */}
617
+ <ToastHost />
618
+ <UpdateDrawerHost />
432
619
  </div>
433
620
  );
434
621
  }
@@ -579,8 +766,16 @@ function MitmConsentCard({ team }) {
579
766
  setBusy("enable"); setError("");
580
767
  try {
581
768
  const r = await caFetch("/api/mitm/consent", { method: "POST", body: JSON.stringify({ enable: true }) });
769
+ // Any "I lack privilege to write the trust store" answer → elevate. Older
770
+ // daemons (esp. macOS) return the raw keychain error instead of the tidy
771
+ // "need_elevation" code, so match those too rather than dumping a scary
772
+ // "security add-trusted-cert: Write permissions error" on the user.
773
+ const elevText = `${r.json?.error || ""} ${r.json?.detail || ""}`;
774
+ const needsElevation = r.json?.error === "need_elevation"
775
+ || (!r.ok && r.status === 403)
776
+ || /need_elevation|add-trusted-cert|write permission|SecCertificate|not permitted|requires admin|administrator/i.test(elevText);
582
777
  if (r.ok && r.json?.ok && r.json?.trusted) { await refresh(); }
583
- else if (r.json?.error === "need_elevation" || (!r.ok && r.status === 403)) {
778
+ else if (needsElevation) {
584
779
  // fall back to the self-elevating CLI (OS prompt = the second consent)
585
780
  const ex = await window.cicy?.mitm?.caExec?.("install");
586
781
  if (ex?.ok) await refresh();
@@ -610,6 +805,29 @@ function MitmConsentCard({ team }) {
610
805
  const partial = status.consent && !status.trusted; // consented but not (re)installed
611
806
  const t = (k, fb) => tr(`mitmConsent.${k}`, fb);
612
807
 
808
+ // 已启用是稳态状态,不是决策 — 收成一个低调的小 pill(一行 + "关闭"),把显眼的
809
+ // 大卡片只留给首次"同意"那一下,不在首页常驻一个大块。
810
+ if (granted || busy === "disable") {
811
+ // Portal to <body> so the fixed pill sits in the ROOT stacking context and
812
+ // paints above the topbar (otherwise it's trapped in the content stack and the
813
+ // 顶栏 user chip covers it).
814
+ return createPortal(
815
+ <div data-id="MitmConsentCard" className="mitm-pill" title={t("grantedDesc", "HTTPS 审计已开启,仅对 CiCy 启动的 AI 工具生效;随时可关闭。")}>
816
+ <span className="mitm-pill__dot" data-busy={busy ? "1" : "0"} />
817
+ <span className="mitm-pill__text" data-id="MitmConsentCard-title">
818
+ {busy === "disable" ? t("processingRevoke", "正在关闭…") : t("statePillOn", "HTTPS 审计已开启")}
819
+ </span>
820
+ {!busy && (
821
+ <button type="button" data-id="MitmConsentCard-revoke" className="mitm-pill__off"
822
+ onClick={() => { if (window.confirm(t("revokeConfirm", "撤销后将停止解密审计并清除同意标记。确定?"))) disable(); }}>
823
+ {t("turnOff", "关闭")}
824
+ </button>
825
+ )}
826
+ </div>,
827
+ document.body
828
+ );
829
+ }
830
+
613
831
  return (
614
832
  <div data-id="MitmConsentCard" className={`mitm-card${granted ? " mitm-card--on" : ""}`}>
615
833
  <div className="mitm-card__head">
@@ -621,9 +839,9 @@ function MitmConsentCard({ team }) {
621
839
  </span>
622
840
  </div>
623
841
  <p className="mitm-card__desc" data-id="MitmConsentCard-desc">
624
- {granted ? t("grantedDesc", "HTTPS 审计已开启;可随时撤销并卸载证书。") : t("body", "启用后,本机到 AI 厂商(Claude / OpenAI / DeepSeek / Gemini)的 HTTPS 将被本地审计解密,数据留本地,可随时关闭。")}
842
+ {granted ? t("grantedDesc", "HTTPS 审计已开启,仅对 CiCy 启动的 AI 工具(claude / codex 等)生效;随时可关闭。") : t("body", "启用后,CiCy 启动的 AI 工具(claude / codex 等)访问 AI 厂商(Claude / OpenAI / DeepSeek / Gemini)的 HTTPS 流量将被本地审计解密,数据留本地,随时可关闭。")}
625
843
  {!granted && <>
626
- <br /><span className="mitm-card__note">{t("adminNote", "需写入系统根证书信任库,需要管理员授权。")}</span>
844
+ <br /><span className="mitm-card__note">{t("adminNote", "通过环境变量对 CiCy 启动的 AI 工具生效,不修改系统、无需管理员授权。")}</span>
627
845
  <br /><span className="mitm-card__sub">{t("scopeNote", "仅解密上述 AI 厂商域名,其余一切流量不被解密、不被读取。")}</span>
628
846
  </>}
629
847
  </p>
@@ -631,13 +849,13 @@ function MitmConsentCard({ team }) {
631
849
  <div className="mitm-card__actions">
632
850
  {granted ? (
633
851
  <button data-id="MitmConsentCard-revoke" className="mitm-card__btn mitm-card__btn--ghost"
634
- disabled={!!busy} onClick={() => { if (window.confirm(t("revokeConfirm", "撤销后将卸载证书、停止解密,并清除同意标记。确定?"))) disable(); }}>
635
- {busy === "disable" ? t("processingRevoke", "正在卸载证书…") : t("revoke", "撤销")}
852
+ disabled={!!busy} onClick={() => { if (window.confirm(t("revokeConfirm", "撤销后将停止解密审计并清除同意标记。确定?"))) disable(); }}>
853
+ {busy === "disable" ? t("processingRevoke", "正在关闭…") : t("revoke", "撤销")}
636
854
  </button>
637
855
  ) : (
638
856
  <button data-id="MitmConsentCard-enable" className="mitm-card__btn"
639
857
  disabled={!!busy} onClick={enable}>
640
- {busy === "enable" ? t("processingEnable", "正在安装证书…") : partial ? t("retry", "重试") : t("enable", "同意并启用")}
858
+ {busy === "enable" ? t("processingEnable", "正在启用…") : partial ? t("retry", "重试") : t("enable", "同意并启用")}
641
859
  </button>
642
860
  )}
643
861
  </div>
@@ -738,8 +956,6 @@ function LocalTeamCard({ team, onOpen, onRename, onRefresh }) {
738
956
  const local = hasBridge && isLocalSidecar(team.base_url);
739
957
  const running = team.status === "running";
740
958
  const [busy, setBusy] = useState(""); // "" | start | restart | update | stop
741
- const [opMsg, setOpMsg] = useState("");
742
- const [opProg, setOpProg] = useState(null); // live {message, progress?, status} during 更新
743
959
  const [menuOpen, setMenuOpen] = useState(false);
744
960
  const [latest, setLatest] = useState(null); // newest cicy-code on the registry
745
961
  const [checking, setChecking] = useState(false);
@@ -804,30 +1020,43 @@ function LocalTeamCard({ team, onOpen, onRename, onRefresh }) {
804
1020
  onRefresh?.();
805
1021
  };
806
1022
 
1023
+ // One toast per card-op, keyed by team — progress streams into it, the final
1024
+ // result replaces the message and auto-dismisses. Feedback floats over the UI
1025
+ // (toast), NOT inside the card; the card's only busy hint is the CTA spinner.
1026
+ const opToastId = `sidecar-op:${team.id}`;
807
1027
  const runOp = async (kind, fn, doneText) => {
808
1028
  setMenuOpen(false);
809
1029
  if (busy) return;
810
- setBusy(kind); setOpMsg(""); setOpProg(null);
811
- // 更新 streams real phase/percent events from the main process surface
812
- // them live on the card so the user SEES the download/swap happening.
1030
+ setBusy(kind);
1031
+ // 更新 gets its own drawer (live log + 阶段 + 重试); other ops use the toast.
1032
+ const isUpdate = kind === "update";
813
1033
  let unsub = null;
814
- if (kind === "update" && window.cicy?.sidecar?.onOpProgress) {
815
- unsub = window.cicy.sidecar.onOpProgress((ev) => {
816
- if (ev?.op === "update") setOpProg(ev);
817
- });
1034
+ if (isUpdate) {
1035
+ updateDrawer.open({ teamId: team.id, fromVer: team.version, toVer: latest, onRetry: () => runOp("update", fn, doneText) });
1036
+ if (window.cicy?.sidecar?.onOpProgress) {
1037
+ unsub = window.cicy.sidecar.onOpProgress((ev) => { if (ev?.op === "update") updateDrawer.push(ev); });
1038
+ }
1039
+ } else {
1040
+ toast.show({ id: opToastId, message: BUSY_LABEL[kind] || `${kind}…`, status: "running", progress: undefined });
818
1041
  }
819
1042
  try {
820
1043
  const r = await fn();
821
- setOpMsg(r?.ok
822
- ? (r.warning ? `${doneText}(${r.warning})` : doneText)
823
- : (tr("sidecar.failed", "操作失败") + (r?.error ? `: ${r.error}` : "")));
1044
+ const ok = !!r?.ok;
1045
+ const okMsg = r?.warning ? `${doneText}(${r.warning})` : doneText;
1046
+ const errMsg = tr("sidecar.failed", "操作失败") + (r?.error ? `: ${r.error}` : "");
1047
+ if (isUpdate) {
1048
+ updateDrawer.finish({ ok, message: ok ? okMsg : errMsg });
1049
+ } else {
1050
+ toast.show({ id: opToastId, message: ok ? okMsg : errMsg, progress: undefined, status: ok ? "done" : "error", ttl: ok ? 4000 : 8000 });
1051
+ }
824
1052
  } catch (err) {
825
- setOpMsg(tr("sidecar.failed", "操作失败") + `: ${err?.message || err}`);
1053
+ const m = tr("sidecar.failed", "操作失败") + `: ${err?.message || err}`;
1054
+ if (isUpdate) updateDrawer.finish({ ok: false, message: m });
1055
+ else toast.show({ id: opToastId, message: m, progress: undefined, status: "error", ttl: 8000 });
826
1056
  } finally {
827
1057
  try { unsub && unsub(); } catch {}
828
- setBusy(""); setOpProg(null);
1058
+ setBusy("");
829
1059
  onRefresh?.(); // re-probe so the status dot/chip catches up
830
- setTimeout(() => setOpMsg(""), 5000); // result line is transient
831
1060
  }
832
1061
  };
833
1062
  const BUSY_LABEL = { start: "启动中…", restart: "重启中…", update: "更新中…", stop: "停止中…" };
@@ -839,13 +1068,15 @@ function LocalTeamCard({ team, onOpen, onRename, onRefresh }) {
839
1068
  const handleOpen = async () => {
840
1069
  if (busy) return;
841
1070
  if (!running && local && window.cicy?.sidecar?.start) {
842
- setBusy("start"); setOpMsg("");
1071
+ setBusy("start");
1072
+ toast.show({ id: opToastId, message: BUSY_LABEL.start, status: "running", progress: undefined });
843
1073
  const r = await window.cicy.sidecar.start().catch((e) => ({ ok: false, error: e?.message || String(e) }));
844
1074
  setBusy(""); onRefresh?.();
845
1075
  if (!r?.ok || r?.warning) { // didn't come up — surface it, don't open a dead link
846
- setOpMsg(tr("sidecar.startFailed", "启动失败") + (r?.error ? `: ${r.error}` : r?.warning ? `: ${r.warning}` : ""));
1076
+ toast.show({ id: opToastId, message: tr("sidecar.startFailed", "启动失败") + (r?.error ? `: ${r.error}` : r?.warning ? `: ${r.warning}` : ""), status: "error", ttl: 8000 });
847
1077
  return;
848
1078
  }
1079
+ toast.dismiss(opToastId); // came up — no lingering toast, the window opens
849
1080
  }
850
1081
  onOpen(); // open regardless of health — the window/page handles the rest
851
1082
  };
@@ -978,22 +1209,6 @@ function LocalTeamCard({ team, onOpen, onRename, onRefresh }) {
978
1209
  <span className="bcard__ver" data-id="LocalTeamCard-version">v{team.version}</span>
979
1210
  )}
980
1211
  </div>
981
- {busy && (
982
- <div className="bcard__prog" data-id="LocalTeamCard-progress" data-status={opProg?.status || "running"}>
983
- <span className="bcard__progmsg">
984
- {opProg?.message || BUSY_LABEL[busy] || `${busy}…`}
985
- {Number.isFinite(opProg?.progress) ? ` ${opProg.progress}%` : ""}
986
- </span>
987
- {Number.isFinite(opProg?.progress) && (
988
- <span className="bcard__progbar"><span style={{ width: `${Math.min(100, opProg.progress)}%` }} /></span>
989
- )}
990
- </div>
991
- )}
992
- {!busy && opMsg && (
993
- <div className="bcard__prog" data-id="LocalTeamCard-progress" data-status={/失败|error/i.test(opMsg) ? "error" : "done"}>
994
- <span className="bcard__progmsg">{opMsg}</span>
995
- </div>
996
- )}
997
1212
  </div>
998
1213
  <button
999
1214
  type="button"
package/.env.dev DELETED
@@ -1,7 +0,0 @@
1
- # Dev mode overrides — loaded by cicy-dektop.command if this file exists.
2
- # Delete or rename this file to revert to production homepage URL behavior.
3
-
4
- # Load the homepage from the workers/render vite dev server (HMR enabled).
5
- # Requires `cd workers/render && npm run dev` to be running, and—if Mac is
6
- # remote—ssh -R 8173:127.0.0.1:8173 mac forwarding from the build host.
7
- CICY_HOMEPAGE_URL=http://localhost:8173