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.
- package/package.json +7 -7
- package/src/backends/homepage-react/assets/index-BpljolQs.js +365 -0
- package/src/backends/homepage-react/assets/{index-BniEbx_j.css → index-C9AZlTew.css} +1 -1
- package/src/backends/homepage-react/index.html +2 -2
- package/src/backends/local-teams.js +42 -4
- package/src/backends/sidecar-ipc.js +23 -1
- package/src/i18n/locales/en.json +9 -7
- package/src/i18n/locales/zh-CN.json +9 -7
- package/src/sidecar/cicy-code.js +54 -111
- package/src/sidecar/localbin.js +133 -0
- package/src/sidecar/native.js +4 -2
- package/src/sidecar/runtime.js +7 -3
- package/workers/render/src/App.css +156 -10
- package/workers/render/src/App.jsx +254 -39
- package/.env.dev +0 -7
- package/src/backends/homepage-react/assets/index-B8gGhz8B.js +0 -365
- package/workers/render.bak.20260528-2338/DESIGN_v2.md +0 -254
- package/workers/render.bak.20260528-2338/index.html +0 -12
- package/workers/render.bak.20260528-2338/package-lock.json +0 -827
- package/workers/render.bak.20260528-2338/package.json +0 -19
- package/workers/render.bak.20260528-2338/public/_headers +0 -5
- package/workers/render.bak.20260528-2338/public/manifest.json +0 -6
- package/workers/render.bak.20260528-2338/src/App.css +0 -224
- package/workers/render.bak.20260528-2338/src/App.jsx +0 -1028
- package/workers/render.bak.20260528-2338/src/api.js +0 -285
- package/workers/render.bak.20260528-2338/src/cicycode-ops.js +0 -222
- package/workers/render.bak.20260528-2338/src/components/BackendCard.css +0 -299
- package/workers/render.bak.20260528-2338/src/components/BackendCard.jsx +0 -133
- package/workers/render.bak.20260528-2338/src/components/BackendModal.css +0 -161
- package/workers/render.bak.20260528-2338/src/components/BackendModal.jsx +0 -199
- package/workers/render.bak.20260528-2338/src/components/Button.css +0 -72
- package/workers/render.bak.20260528-2338/src/components/Button.jsx +0 -37
- package/workers/render.bak.20260528-2338/src/components/Card.css +0 -42
- package/workers/render.bak.20260528-2338/src/components/Card.jsx +0 -21
- package/workers/render.bak.20260528-2338/src/components/Icon.jsx +0 -30
- package/workers/render.bak.20260528-2338/src/components/Menu.css +0 -55
- package/workers/render.bak.20260528-2338/src/components/Menu.jsx +0 -91
- package/workers/render.bak.20260528-2338/src/components/SidecarBanner.css +0 -79
- package/workers/render.bak.20260528-2338/src/components/SidecarBanner.jsx +0 -84
- package/workers/render.bak.20260528-2338/src/components/StatusChip.css +0 -19
- package/workers/render.bak.20260528-2338/src/components/StatusChip.jsx +0 -31
- package/workers/render.bak.20260528-2338/src/components/Toast.css +0 -31
- package/workers/render.bak.20260528-2338/src/components/Toast.jsx +0 -23
- package/workers/render.bak.20260528-2338/src/components/WslSetupBanner.css +0 -464
- package/workers/render.bak.20260528-2338/src/components/WslSetupBanner.jsx +0 -716
- package/workers/render.bak.20260528-2338/src/dockerInstaller.js +0 -0
- package/workers/render.bak.20260528-2338/src/i18n/en.json +0 -116
- package/workers/render.bak.20260528-2338/src/i18n/fr.json +0 -116
- package/workers/render.bak.20260528-2338/src/i18n/index.js +0 -69
- package/workers/render.bak.20260528-2338/src/i18n/ja.json +0 -116
- package/workers/render.bak.20260528-2338/src/i18n/zh-CN.json +0 -121
- package/workers/render.bak.20260528-2338/src/main.js +0 -475
- package/workers/render.bak.20260528-2338/src/main.jsx +0 -18
- package/workers/render.bak.20260528-2338/src/style.css +0 -275
- package/workers/render.bak.20260528-2338/src/styles/base.css +0 -98
- package/workers/render.bak.20260528-2338/src/styles/tokens.css +0 -90
- package/workers/render.bak.20260528-2338/src/tos.js +0 -72
- package/workers/render.bak.20260528-2338/src/worker.js +0 -40
- package/workers/render.bak.20260528-2338/src/wslInstaller.js +0 -1563
- package/workers/render.bak.20260528-2338/vite.config.js +0 -36
- 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 (
|
|
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
|
|
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", "
|
|
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", "
|
|
635
|
-
{busy === "disable" ? t("processingRevoke", "
|
|
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", "
|
|
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);
|
|
811
|
-
// 更新
|
|
812
|
-
|
|
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 (
|
|
815
|
-
|
|
816
|
-
|
|
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
|
-
|
|
822
|
-
|
|
823
|
-
|
|
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
|
-
|
|
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("");
|
|
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");
|
|
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
|
-
|
|
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
|