cicy-desktop 2.1.43 → 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.
@@ -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
- // Pick the team that backs the Team Helper drawer. Any running local team
57
- // whose api_token is populated qualifies — `localTeams.list()` only marks
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("Phase 3 待接:让小助手帮你装 / 自己 docker run cicy-code 后 add");
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
- 还没有团队 — 让小助手帮你装一个本地 team,或在云端创建。
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;