cicy-desktop 2.1.44 → 2.1.45
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 +1 -1
- package/src/backends/homepage-preload.js +3 -0
- package/src/backends/homepage-react/assets/index-CmSv3AFG.js +49 -0
- package/src/backends/homepage-react/assets/{index-CNVsvsZX.css → index-U_RAjjQx.css} +1 -1
- package/src/backends/homepage-react/index.html +2 -2
- package/src/backends/sidecar-ipc.js +47 -2
- package/src/sidecar/cicy-code.js +56 -5
- package/workers/render/src/App.css +46 -0
- package/workers/render/src/App.jsx +105 -348
- package/src/backends/homepage-react/assets/index-BqRSij9W.js +0 -49
|
@@ -13,18 +13,6 @@ const TOKEN_KEY = "cicy_token";
|
|
|
13
13
|
const ACCESS_TOKEN_KEY = "cicy_access_token";
|
|
14
14
|
const USER_ID_KEY = "cicy_user_id";
|
|
15
15
|
const CLOUD_BASE = "https://cicy-ai.com";
|
|
16
|
-
const HELPER_WIDTH_KEY = "cicy_helper_width";
|
|
17
|
-
// v1 MVP: shared helper container on the cloud VM. All trial users hit
|
|
18
|
-
// the same instance — will be replaced by per-user dynamic allocation
|
|
19
|
-
// from /api/helper/start once w-10032 ships that endpoint.
|
|
20
|
-
const HELPER_URL_BASE = "http://43.99.56.150:8011";
|
|
21
|
-
const HELPER_SHARED_TOKEN = "cicy_9170fc02080e5d744cc4e80e423486ca";
|
|
22
|
-
// Team Helper pane id — produced by cicy-code --helper=1, which spawns a
|
|
23
|
-
// single OpenCode worker on port 6002 (see cicy-code setup.go
|
|
24
|
-
// helperModeBuiltinWorker). The SPA uses hash routing #/agent/<session> to
|
|
25
|
-
// land directly inside that pane.
|
|
26
|
-
const HELPER_PANE_ID = "w-6002:main.0";
|
|
27
|
-
const HELPER_AGENT_SESSION = "w-6002";
|
|
28
16
|
|
|
29
17
|
export default function App() {
|
|
30
18
|
// sk-xxx (LLM API). Used by /v1/chat/completions etc.
|
|
@@ -53,119 +41,8 @@ export default function App() {
|
|
|
53
41
|
// Used to distinguish "not yet probed" (unknown) from "probed and empty"
|
|
54
42
|
// (cloud-only) in localHelperState below.
|
|
55
43
|
const [localTeamsFetched, setLocalTeamsFetched] = useState(false);
|
|
56
|
-
//
|
|
57
|
-
|
|
58
|
-
// status="running" when /api/health returned 200. Once one exists, the
|
|
59
|
-
// card switches from "30-min cloud trial" to "persistent local helper" and
|
|
60
|
-
// the drawer dials its own w-6002. Null until a real team comes online.
|
|
61
|
-
const localHelperTeam = useMemo(() => {
|
|
62
|
-
return (localTeams || []).find(
|
|
63
|
-
(t) => t && t.status === "running" && t.base_url && t.api_token,
|
|
64
|
-
) || null;
|
|
65
|
-
}, [localTeams]);
|
|
66
|
-
// localHelperState — four-way. Drives both the Helper card and onStart.
|
|
67
|
-
//
|
|
68
|
-
// "unknown" : first list() probe hasn't returned. Don't decide.
|
|
69
|
-
// "local-ready" : at least one local team is healthy → use it.
|
|
70
|
-
// "local-pending" : probe done, no healthy team yet, BUT user has
|
|
71
|
-
// local nodes configured (cicy-code starting up,
|
|
72
|
-
// wrong token, transient error, …). Wait.
|
|
73
|
-
// "cloud-only" : probe done AND no local nodes configured. Always
|
|
74
|
-
// show the cloud-trial helper — it's the one that
|
|
75
|
-
// walks users through installing Docker + cicy-code
|
|
76
|
-
// on Windows, so we need to be able to summon it
|
|
77
|
-
// every launch until the install completes (then
|
|
78
|
-
// cicyDesktopNodes lands an entry and state flips
|
|
79
|
-
// to local-ready / local-pending).
|
|
80
|
-
const hasLocalConfigured = useMemo(() => {
|
|
81
|
-
// Any configured node counts — even "stopped" or "auth_error" — because
|
|
82
|
-
// the user has signaled intent to use a local instance. The only state
|
|
83
|
-
// that means "no local intent" is an empty list.
|
|
84
|
-
return Array.isArray(localTeams) && localTeams.length > 0;
|
|
85
|
-
}, [localTeams]);
|
|
86
|
-
const localHelperState = useMemo(() => {
|
|
87
|
-
if (!localTeamsFetched) return "unknown";
|
|
88
|
-
if (localHelperTeam) return "local-ready";
|
|
89
|
-
if (hasLocalConfigured) return "local-pending";
|
|
90
|
-
return "cloud-only";
|
|
91
|
-
}, [localTeamsFetched, localHelperTeam, hasLocalConfigured]);
|
|
92
|
-
const localHelperUrl = useMemo(() => {
|
|
93
|
-
if (!localHelperTeam) return null;
|
|
94
|
-
const base = String(localHelperTeam.base_url).replace(/\/$/, "");
|
|
95
|
-
return `${base}/?token=${encodeURIComponent(localHelperTeam.api_token)}#/agent/w-6002`;
|
|
96
|
-
}, [localHelperTeam]);
|
|
97
|
-
const cloudHelperUrl = useMemo(
|
|
98
|
-
() => `${HELPER_URL_BASE}/?token=${encodeURIComponent(HELPER_SHARED_TOKEN)}#/agent/${HELPER_AGENT_SESSION}`,
|
|
99
|
-
[],
|
|
100
|
-
);
|
|
101
|
-
// Tab + helper drawer state (the v1 layout: tabs row over a unified grid,
|
|
102
|
-
// right-edge full-height webview drawer for the team-helper agent).
|
|
103
|
-
const [tab, setTab] = useState("all"); // "all" | "local" | "cloud"
|
|
104
|
-
const [helperWidth, setHelperWidth] = useState(() => {
|
|
105
|
-
const saved = parseInt(safeGet(HELPER_WIDTH_KEY) || "0", 10);
|
|
106
|
-
if (saved > 0) return saved;
|
|
107
|
-
if (typeof window === "undefined") return 560;
|
|
108
|
-
return Math.round(window.innerWidth * 0.42);
|
|
109
|
-
});
|
|
110
|
-
const [helperResizing, setHelperResizing] = useState(false);
|
|
111
|
-
// Drawer is collapsed by default. Opens when the user clicks the "团队
|
|
112
|
-
// 小助手" onboarding card (or any future "summon helper" trigger).
|
|
113
|
-
const [helperOpen, setHelperOpen] = useState(false);
|
|
114
|
-
// Helper-instance URL — null until Phase 4 wires /api/helper/start.
|
|
115
|
-
const [helperUrl, setHelperUrl] = useState(null);
|
|
116
|
-
const helperWebviewRef = useRef(null);
|
|
117
|
-
// Auto-promote: if the drawer is open with a placeholder (helperUrl===null
|
|
118
|
-
// because we were in unknown/local-pending), and the local helper team
|
|
119
|
-
// becomes ready, swap in the local URL. Only when helperUrl is still
|
|
120
|
-
// null — once a webview (cloud or local) is loaded we never silently swap
|
|
121
|
-
// it out from under the user (they may be mid-conversation).
|
|
122
|
-
useEffect(() => {
|
|
123
|
-
if (helperOpen && helperUrl === null && localHelperUrl) {
|
|
124
|
-
setHelperUrl(localHelperUrl);
|
|
125
|
-
}
|
|
126
|
-
}, [helperOpen, helperUrl, localHelperUrl]);
|
|
127
|
-
// Centered modal asking the user to confirm sending "start". Shown each
|
|
128
|
-
// time the drawer opens unless the user picked "不再显示" (persisted in
|
|
129
|
-
// localStorage). Manual fallback for when server-side helper-kick didn't
|
|
130
|
-
// fire (drawer reopened too quickly, opencode dropped the first message).
|
|
131
|
-
const [helperModalShown, setHelperModalShown] = useState(false);
|
|
132
|
-
const [helperModalSuppressed, setHelperModalSuppressed] = useState(
|
|
133
|
-
() => { try { return localStorage.getItem("helper_modal_suppressed") === "1"; } catch { return false; } }
|
|
134
|
-
);
|
|
135
|
-
useEffect(() => {
|
|
136
|
-
// The "start" confirm modal is cloud-trial-specific (it posts to
|
|
137
|
-
// HELPER_URL_BASE/api/tmux/send to kick the cloud opencode). For
|
|
138
|
-
// local-ready (persistent w-6002 already has its intro queue) and
|
|
139
|
-
// unknown/local-pending (no webview yet) it's wrong to show it.
|
|
140
|
-
if (helperOpen && !helperModalSuppressed && localHelperState === "cloud-only") {
|
|
141
|
-
setHelperModalShown(true);
|
|
142
|
-
}
|
|
143
|
-
if (!helperOpen) setHelperModalShown(false);
|
|
144
|
-
}, [helperOpen, helperModalSuppressed, localHelperState]);
|
|
145
|
-
const suppressHelperModal = useCallback(() => {
|
|
146
|
-
try { localStorage.setItem("helper_modal_suppressed", "1"); } catch {}
|
|
147
|
-
setHelperModalSuppressed(true);
|
|
148
|
-
setHelperModalShown(false);
|
|
149
|
-
}, []);
|
|
150
|
-
const [helperSending, setHelperSending] = useState(false);
|
|
151
|
-
const sendHelperStart = useCallback(async () => {
|
|
152
|
-
if (helperSending) return;
|
|
153
|
-
setHelperSending(true);
|
|
154
|
-
try {
|
|
155
|
-
await window.cicy?.cloud?.fetch?.(`${HELPER_URL_BASE}/api/tmux/send`, {
|
|
156
|
-
method: "POST",
|
|
157
|
-
headers: {
|
|
158
|
-
"Content-Type": "application/json",
|
|
159
|
-
"Authorization": `Bearer ${HELPER_SHARED_TOKEN}`,
|
|
160
|
-
},
|
|
161
|
-
body: JSON.stringify({ pane_id: HELPER_PANE_ID, text: "start", submit: true }),
|
|
162
|
-
});
|
|
163
|
-
setHelperModalShown(false);
|
|
164
|
-
} catch {} finally {
|
|
165
|
-
setHelperSending(false);
|
|
166
|
-
}
|
|
167
|
-
}, [helperSending]);
|
|
168
|
-
// (userContextSentRef gone — server-side --helper kick owns the trigger.)
|
|
44
|
+
// Tab state for the team grid: "all" | "local" | "cloud".
|
|
45
|
+
const [tab, setTab] = useState("all");
|
|
169
46
|
|
|
170
47
|
// Pull /api/user/self + /api/teams in parallel using the access_token.
|
|
171
48
|
// Goes through window.cicy.cloud.fetch — main does the actual request,
|
|
@@ -264,18 +141,6 @@ export default function App() {
|
|
|
264
141
|
try {
|
|
265
142
|
if (msg?.type === "localTeams:add") {
|
|
266
143
|
result = await window.cicy.localTeams.add(msg.spec || {});
|
|
267
|
-
// Hand-off: when the cloud Team Helper registered our new local
|
|
268
|
-
// backend, swap the drawer's webview to that backend's own
|
|
269
|
-
// w-6002 pane after a short pause so the user can read the
|
|
270
|
-
// farewell message first. Heuristic: install_source starts
|
|
271
|
-
// with "helper-".
|
|
272
|
-
if (result?.ok && /^helper(-|$)/.test(msg.spec?.install_source || "") && result?.team?.base_url) {
|
|
273
|
-
const team = result.team;
|
|
274
|
-
setTimeout(() => {
|
|
275
|
-
const tok = team.api_token ? `?token=${encodeURIComponent(team.api_token)}` : "";
|
|
276
|
-
setHelperUrl(`${team.base_url}${tok}#/agent/w-6002`);
|
|
277
|
-
}, 2500);
|
|
278
|
-
}
|
|
279
144
|
} else if (msg?.type === "localTeams:remove") {
|
|
280
145
|
result = await window.cicy.localTeams.remove(msg.id);
|
|
281
146
|
} else if (msg?.type === "localTeams:update") {
|
|
@@ -303,35 +168,6 @@ export default function App() {
|
|
|
303
168
|
try { await window.cicy.localTeams.open(teamId); } catch {}
|
|
304
169
|
}, []);
|
|
305
170
|
|
|
306
|
-
// Drag-resize the right helper drawer. Mousedown on the 6 px handle
|
|
307
|
-
// attaches window-level listeners so the drag survives the cursor leaving
|
|
308
|
-
// the handle. Webview captures its own mouse events in a child renderer
|
|
309
|
-
// process — without the fullscreen mask the host page loses mousemove
|
|
310
|
-
// the instant the cursor crosses into the webview.
|
|
311
|
-
const startHelperResize = useCallback((ev) => {
|
|
312
|
-
ev.preventDefault();
|
|
313
|
-
setHelperResizing(true);
|
|
314
|
-
const min = 320;
|
|
315
|
-
const onMove = (e) => {
|
|
316
|
-
const w = window.innerWidth - e.clientX;
|
|
317
|
-
const max = window.innerWidth - 320;
|
|
318
|
-
const clamped = Math.max(min, Math.min(max, w));
|
|
319
|
-
setHelperWidth(clamped);
|
|
320
|
-
};
|
|
321
|
-
const onUp = () => {
|
|
322
|
-
setHelperResizing(false);
|
|
323
|
-
window.removeEventListener("mousemove", onMove);
|
|
324
|
-
window.removeEventListener("mouseup", onUp);
|
|
325
|
-
// Persist the final width so a relaunch keeps the user's layout.
|
|
326
|
-
try { localStorage.setItem(HELPER_WIDTH_KEY, String(parseInt(getComputedStyle(document.querySelector(".helper-aside") || document.body).getPropertyValue("width") || "0", 10) || 0)); } catch {}
|
|
327
|
-
};
|
|
328
|
-
window.addEventListener("mousemove", onMove);
|
|
329
|
-
window.addEventListener("mouseup", onUp);
|
|
330
|
-
}, []);
|
|
331
|
-
useEffect(() => {
|
|
332
|
-
try { localStorage.setItem(HELPER_WIDTH_KEY, String(helperWidth)); } catch {}
|
|
333
|
-
}, [helperWidth]);
|
|
334
|
-
|
|
335
171
|
// (USER_CONTEXT push retired — cicy-code 2.1.7's --helper mode fires the
|
|
336
172
|
// open-protocol trigger server-side from watchHelperOpencodeReadyAndKick,
|
|
337
173
|
// gated on BOTH opencode-ready AND a connected web-* chat client. That
|
|
@@ -513,6 +349,8 @@ export default function App() {
|
|
|
513
349
|
))}
|
|
514
350
|
</div>
|
|
515
351
|
|
|
352
|
+
{showLocal && <SidecarControl />}
|
|
353
|
+
|
|
516
354
|
{profileError && (
|
|
517
355
|
<div className="error" style={{ marginBottom: 12 }}>
|
|
518
356
|
云端: {profileError}
|
|
@@ -523,33 +361,6 @@ export default function App() {
|
|
|
523
361
|
)}
|
|
524
362
|
|
|
525
363
|
<div className="app__grid">
|
|
526
|
-
{/* Team Helper card — always rendered. Routing decision is the
|
|
527
|
-
three-way localHelperState (see useMemo above):
|
|
528
|
-
local-ready → open local team's w-6002 (persistent, no trial cap)
|
|
529
|
-
local-pending → open drawer with placeholder, wait for cicy-code
|
|
530
|
-
to come up; auto-promote to local URL when it
|
|
531
|
-
does (effect below).
|
|
532
|
-
unknown → same as local-pending: never silently fall
|
|
533
|
-
through to cloud during the launch race.
|
|
534
|
-
cloud-only → open the 30-min cloud trial. Only here. */}
|
|
535
|
-
<HelperOnboardCard
|
|
536
|
-
state={localHelperState}
|
|
537
|
-
onStart={() => {
|
|
538
|
-
if (localHelperState === "local-ready") {
|
|
539
|
-
setHelperUrl(localHelperUrl);
|
|
540
|
-
} else if (localHelperState === "cloud-only") {
|
|
541
|
-
setHelperUrl(cloudHelperUrl);
|
|
542
|
-
} else {
|
|
543
|
-
// unknown / local-pending — open the drawer but leave
|
|
544
|
-
// helperUrl null so HelperPlaceholder renders. For
|
|
545
|
-
// local-pending the effect below upgrades to localHelperUrl
|
|
546
|
-
// as soon as cicy-code comes up.
|
|
547
|
-
setHelperUrl(null);
|
|
548
|
-
}
|
|
549
|
-
setHelperOpen(true);
|
|
550
|
-
}}
|
|
551
|
-
/>
|
|
552
|
-
|
|
553
364
|
{showLocal && localTeams && localTeams.map((t) => (
|
|
554
365
|
<LocalTeamCard key={"local:" + t.id} team={t} onOpen={() => openLocalTeam(t.id)} onRename={renameLocalTeam} />
|
|
555
366
|
))}
|
|
@@ -565,7 +376,7 @@ export default function App() {
|
|
|
565
376
|
))}
|
|
566
377
|
{showLocal && (
|
|
567
378
|
<button type="button" className="add-card" onClick={() => {
|
|
568
|
-
alert("
|
|
379
|
+
alert("装本地 cicy-code(npx cicy-code / docker run)后会自动出现,或在云端创建团队。");
|
|
569
380
|
}}>
|
|
570
381
|
<span className="add-card__plus">+</span>
|
|
571
382
|
<span className="add-card__label">新建本地团队</span>
|
|
@@ -575,164 +386,11 @@ export default function App() {
|
|
|
575
386
|
|
|
576
387
|
{!profileLoading && !profileError && teams && teams.length === 0 && !localTeams?.length && (
|
|
577
388
|
<div className="empty" style={{ marginTop: 14 }}>
|
|
578
|
-
还没有团队 —
|
|
389
|
+
还没有团队 — 安装本地 cicy-code 起一个本地 team,或在云端创建。
|
|
579
390
|
</div>
|
|
580
391
|
)}
|
|
581
392
|
</main>
|
|
582
393
|
</div>{/* /.shell__left */}
|
|
583
|
-
|
|
584
|
-
{/* Drag mask: during resize, fullscreen invisible div above the webview
|
|
585
|
-
so the host page keeps receiving mousemove (webview is a separate
|
|
586
|
-
renderer process that eats its own mouse events). */}
|
|
587
|
-
{helperResizing && (
|
|
588
|
-
<div
|
|
589
|
-
className="helper-mask"
|
|
590
|
-
style={{ position: "fixed", inset: 0, cursor: "ew-resize", background: "transparent", userSelect: "none", zIndex: 9999 }}
|
|
591
|
-
/>
|
|
592
|
-
)}
|
|
593
|
-
|
|
594
|
-
{/* 🤖 团队助手 — 通栏右侧抽屉。默认收起,点 onboard card 才开。 */}
|
|
595
|
-
{helperOpen && (
|
|
596
|
-
<aside
|
|
597
|
-
className="helper-aside"
|
|
598
|
-
style={{ width: helperWidth }}
|
|
599
|
-
>
|
|
600
|
-
<div
|
|
601
|
-
onMouseDown={startHelperResize}
|
|
602
|
-
style={{ position: "absolute", left: -3, top: 0, bottom: 0, width: 6, cursor: "ew-resize", zIndex: 1 }}
|
|
603
|
-
title="拖动调整宽度"
|
|
604
|
-
/>
|
|
605
|
-
{/* Top bar with close button. Subtle to not steal focus from the
|
|
606
|
-
assistant content below. */}
|
|
607
|
-
<div className="helper-aside__top">
|
|
608
|
-
<span className="helper-aside__title">🤖 团队小助手</span>
|
|
609
|
-
<button
|
|
610
|
-
type="button"
|
|
611
|
-
className="helper-aside__close"
|
|
612
|
-
onClick={() => setHelperOpen(false)}
|
|
613
|
-
aria-label="关闭"
|
|
614
|
-
>×</button>
|
|
615
|
-
</div>
|
|
616
|
-
{helperModalShown && (
|
|
617
|
-
<div
|
|
618
|
-
className="helper-modal__backdrop"
|
|
619
|
-
onClick={() => setHelperModalShown(false)}
|
|
620
|
-
>
|
|
621
|
-
<div
|
|
622
|
-
className="helper-modal"
|
|
623
|
-
role="dialog"
|
|
624
|
-
aria-modal="true"
|
|
625
|
-
onClick={(e) => e.stopPropagation()}
|
|
626
|
-
>
|
|
627
|
-
<div className="helper-modal__title">让小助手开始工作</div>
|
|
628
|
-
<div className="helper-modal__desc">
|
|
629
|
-
点击「确认发送」会向团队小助手发送 <code>start</code>,
|
|
630
|
-
它会探测您的系统并按需安装本地团队后端。
|
|
631
|
-
</div>
|
|
632
|
-
<div className="helper-modal__actions">
|
|
633
|
-
<button
|
|
634
|
-
type="button"
|
|
635
|
-
className="helper-modal__btn helper-modal__btn--ghost"
|
|
636
|
-
onClick={suppressHelperModal}
|
|
637
|
-
>不再显示</button>
|
|
638
|
-
<button
|
|
639
|
-
type="button"
|
|
640
|
-
className="helper-modal__btn"
|
|
641
|
-
onClick={() => setHelperModalShown(false)}
|
|
642
|
-
>关闭</button>
|
|
643
|
-
<button
|
|
644
|
-
type="button"
|
|
645
|
-
className="helper-modal__btn helper-modal__btn--primary"
|
|
646
|
-
onClick={sendHelperStart}
|
|
647
|
-
disabled={helperSending}
|
|
648
|
-
>{helperSending ? "发送中…" : "确认发送"}</button>
|
|
649
|
-
</div>
|
|
650
|
-
</div>
|
|
651
|
-
</div>
|
|
652
|
-
)}
|
|
653
|
-
{helperUrl ? (
|
|
654
|
-
<webview
|
|
655
|
-
ref={helperWebviewRef}
|
|
656
|
-
key={helperUrl}
|
|
657
|
-
src={helperUrl}
|
|
658
|
-
{...(window.cicy?.webviewPreloadPath ? { preload: `file://${window.cicy.webviewPreloadPath}` } : {})}
|
|
659
|
-
style={{ flex: 1, border: 0, width: "100%", height: "100%" }}
|
|
660
|
-
allowpopups="true"
|
|
661
|
-
/>
|
|
662
|
-
) : (
|
|
663
|
-
<HelperPlaceholder state={localHelperState} />
|
|
664
|
-
)}
|
|
665
|
-
</aside>
|
|
666
|
-
)}
|
|
667
|
-
</div>
|
|
668
|
-
);
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
function HelperOnboardCard({ onStart, state = "unknown" }) {
|
|
672
|
-
// state: "unknown" | "local-ready" | "local-pending" | "cloud-only"
|
|
673
|
-
// local-ready : open helper drawer pointing at local w-6002
|
|
674
|
-
// local-pending : has local config, cicy-code not healthy yet → wait
|
|
675
|
-
// unknown : first probe in flight → behave like local-pending
|
|
676
|
-
// cloud-only : no local installed → always-available cloud helper that
|
|
677
|
-
// walks the user through installing Docker + cicy-code.
|
|
678
|
-
// Shown on every launch until install lands a team in
|
|
679
|
-
// cicyDesktopNodes (then state flips to local-ready/-pending).
|
|
680
|
-
const isLocal = state === "local-ready";
|
|
681
|
-
const isPending = state === "unknown" || state === "local-pending";
|
|
682
|
-
const isCloud = state === "cloud-only";
|
|
683
|
-
return (
|
|
684
|
-
<div className="bcard bcard--helper">
|
|
685
|
-
<div className="bcard__accent" />
|
|
686
|
-
<div className="bcard__top">
|
|
687
|
-
<div className="bcard__pill bcard__pill--helper">
|
|
688
|
-
<span className="bcard__helper-icon">🤖</span>
|
|
689
|
-
<span>小助手</span>
|
|
690
|
-
</div>
|
|
691
|
-
{isLocal ? (
|
|
692
|
-
<span className="bcard__badge bcard__badge--local">本地常驻</span>
|
|
693
|
-
) : isPending ? (
|
|
694
|
-
<span className="bcard__badge bcard__badge--local">本地启动中</span>
|
|
695
|
-
) : (
|
|
696
|
-
<span className="bcard__badge bcard__badge--trial">30 分钟试用</span>
|
|
697
|
-
)}
|
|
698
|
-
</div>
|
|
699
|
-
<div className="bcard__body">
|
|
700
|
-
<h3 className="bcard__name">团队小助手</h3>
|
|
701
|
-
{isLocal ? (
|
|
702
|
-
<p className="bcard__desc">管理本地团队 · 升级 / 加新团队</p>
|
|
703
|
-
) : isPending ? (
|
|
704
|
-
<p className="bcard__desc">本地小助手准备中,请稍候…</p>
|
|
705
|
-
) : (
|
|
706
|
-
<>
|
|
707
|
-
<p className="bcard__desc">协助您完成本地私有化团队部署</p>
|
|
708
|
-
<p className="bcard__fineprint">过期后需购买会员</p>
|
|
709
|
-
</>
|
|
710
|
-
)}
|
|
711
|
-
</div>
|
|
712
|
-
<button type="button" className="bcard__cta bcard__cta--helper" onClick={onStart}>
|
|
713
|
-
<span>{isLocal ? "打开助手" : isPending ? "等待本地" : "召唤助手"}</span>
|
|
714
|
-
</button>
|
|
715
|
-
</div>
|
|
716
|
-
);
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
function HelperPlaceholder({ state = "unknown" }) {
|
|
720
|
-
const pending = state === "unknown" || state === "local-pending";
|
|
721
|
-
return (
|
|
722
|
-
<div className="helper-placeholder">
|
|
723
|
-
<div className="helper-placeholder__mark">🤖</div>
|
|
724
|
-
<h3 className="helper-placeholder__title">团队助手</h3>
|
|
725
|
-
{pending ? (
|
|
726
|
-
<p className="helper-placeholder__sub">
|
|
727
|
-
正在等待本地小助手就绪…<br />
|
|
728
|
-
确保 cicy-code 已启动并监听 8008。就绪后会自动连接,无需刷新。
|
|
729
|
-
</p>
|
|
730
|
-
) : (
|
|
731
|
-
<p className="helper-placeholder__sub">
|
|
732
|
-
点击「召唤助手」会启动 30 分钟试用版小助手,
|
|
733
|
-
引导你装 Docker + cicy-code,帮你跑起第一个本地团队。
|
|
734
|
-
</p>
|
|
735
|
-
)}
|
|
736
394
|
</div>
|
|
737
395
|
);
|
|
738
396
|
}
|
|
@@ -769,6 +427,105 @@ function Section({ title, subtitle, icon, children }) {
|
|
|
769
427
|
);
|
|
770
428
|
}
|
|
771
429
|
|
|
430
|
+
// Lifecycle controls for the locally-run cicy-code daemon (the sidecar on
|
|
431
|
+
// :8008). Polls sidecar.status and offers 重启 / 更新 / 停止. Only meaningful
|
|
432
|
+
// on a desktop where this app owns the npx/Docker-launched daemon.
|
|
433
|
+
function SidecarControl() {
|
|
434
|
+
const [running, setRunning] = useState(null); // null = unknown, then bool
|
|
435
|
+
const [busy, setBusy] = useState(""); // "" | "restart" | "update" | "stop"
|
|
436
|
+
const [msg, setMsg] = useState("");
|
|
437
|
+
|
|
438
|
+
const probe = useCallback(async () => {
|
|
439
|
+
if (!window.cicy?.sidecar?.status) return;
|
|
440
|
+
try {
|
|
441
|
+
const r = await window.cicy.sidecar.status();
|
|
442
|
+
setRunning(!!r?.running);
|
|
443
|
+
} catch { setRunning(false); }
|
|
444
|
+
}, []);
|
|
445
|
+
|
|
446
|
+
useEffect(() => {
|
|
447
|
+
probe();
|
|
448
|
+
const id = setInterval(probe, 4000);
|
|
449
|
+
return () => clearInterval(id);
|
|
450
|
+
}, [probe]);
|
|
451
|
+
|
|
452
|
+
// The bridge isn't there at all (old build / non-desktop) → render nothing.
|
|
453
|
+
if (!window.cicy?.sidecar?.restart) return null;
|
|
454
|
+
|
|
455
|
+
const run = async (kind, fn, doneText) => {
|
|
456
|
+
if (busy) return;
|
|
457
|
+
setBusy(kind);
|
|
458
|
+
setMsg("");
|
|
459
|
+
try {
|
|
460
|
+
const r = await fn();
|
|
461
|
+
if (r?.ok) {
|
|
462
|
+
setMsg(r.warning ? `${doneText}(${r.warning})` : doneText);
|
|
463
|
+
} else {
|
|
464
|
+
setMsg(tr("sidecar.failed", "失败") + (r?.error ? `: ${r.error}` : ""));
|
|
465
|
+
}
|
|
466
|
+
} catch (e) {
|
|
467
|
+
setMsg(tr("sidecar.failed", "失败") + `: ${e?.message || e}`);
|
|
468
|
+
} finally {
|
|
469
|
+
setBusy("");
|
|
470
|
+
probe();
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
const dotClass = running == null ? "is-unknown" : running ? "is-on" : "is-off";
|
|
475
|
+
const stateText = running == null
|
|
476
|
+
? tr("sidecar.checking", "检测中…")
|
|
477
|
+
: running ? tr("sidecar.running", "运行中") : tr("sidecar.stopped", "已停止");
|
|
478
|
+
|
|
479
|
+
return (
|
|
480
|
+
<div data-id="SidecarControl" className="sidecar-bar">
|
|
481
|
+
<div className="sidecar-bar__label">
|
|
482
|
+
<span className={`sidecar-dot ${dotClass}`} aria-hidden />
|
|
483
|
+
<span className="sidecar-bar__title">本地 cicy-code</span>
|
|
484
|
+
<span className="sidecar-bar__state">{stateText}</span>
|
|
485
|
+
</div>
|
|
486
|
+
<div className="sidecar-bar__actions">
|
|
487
|
+
<button
|
|
488
|
+
type="button"
|
|
489
|
+
data-id="SidecarControl-restart"
|
|
490
|
+
className="sidecar-btn"
|
|
491
|
+
disabled={!!busy}
|
|
492
|
+
onClick={() => run("restart",
|
|
493
|
+
() => window.cicy.sidecar.restart(),
|
|
494
|
+
tr("sidecar.restarted", "已重启"))}
|
|
495
|
+
>
|
|
496
|
+
{busy === "restart" ? <Spinner /> : null}
|
|
497
|
+
{tr("sidecar.restart", "重启")}
|
|
498
|
+
</button>
|
|
499
|
+
<button
|
|
500
|
+
type="button"
|
|
501
|
+
data-id="SidecarControl-update"
|
|
502
|
+
className="sidecar-btn"
|
|
503
|
+
disabled={!!busy}
|
|
504
|
+
onClick={() => run("update",
|
|
505
|
+
() => window.cicy.sidecar.update(),
|
|
506
|
+
tr("sidecar.updated", "已更新到最新"))}
|
|
507
|
+
>
|
|
508
|
+
{busy === "update" ? <Spinner /> : null}
|
|
509
|
+
{tr("sidecar.update", "更新")}
|
|
510
|
+
</button>
|
|
511
|
+
<button
|
|
512
|
+
type="button"
|
|
513
|
+
data-id="SidecarControl-stop"
|
|
514
|
+
className="sidecar-btn sidecar-btn--danger"
|
|
515
|
+
disabled={!!busy || running === false}
|
|
516
|
+
onClick={() => run("stop",
|
|
517
|
+
() => window.cicy.sidecar.stop(),
|
|
518
|
+
tr("sidecar.stoppedDone", "已停止"))}
|
|
519
|
+
>
|
|
520
|
+
{busy === "stop" ? <Spinner /> : null}
|
|
521
|
+
{tr("sidecar.stop", "停止")}
|
|
522
|
+
</button>
|
|
523
|
+
</div>
|
|
524
|
+
{msg && <span data-id="SidecarControl-msg" className="sidecar-bar__msg">{msg}</span>}
|
|
525
|
+
</div>
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
|
|
772
529
|
function LocalTeamCard({ team, onOpen, onRename }) {
|
|
773
530
|
const statusInfo = LOCAL_STATUS[team.status] || LOCAL_STATUS.error;
|
|
774
531
|
const tone = statusInfo.tone;
|