cicy-desktop 2.1.44 → 2.1.46

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
@@ -523,35 +359,8 @@ export default function App() {
523
359
  )}
524
360
 
525
361
  <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
362
  {showLocal && localTeams && localTeams.map((t) => (
554
- <LocalTeamCard key={"local:" + t.id} team={t} onOpen={() => openLocalTeam(t.id)} onRename={renameLocalTeam} />
363
+ <LocalTeamCard key={"local:" + t.id} team={t} onOpen={() => openLocalTeam(t.id)} onRename={renameLocalTeam} onRefresh={fetchLocalTeams} />
555
364
  ))}
556
365
  {showCloud && teams && teams.map((t) => (
557
366
  <TeamCard
@@ -565,7 +374,7 @@ export default function App() {
565
374
  ))}
566
375
  {showLocal && (
567
376
  <button type="button" className="add-card" onClick={() => {
568
- alert("Phase 3 待接:让小助手帮你装 / 自己 docker run cicy-code 后 add");
377
+ alert("装本地 cicy-code(npx cicy-code / docker run)后会自动出现,或在云端创建团队。");
569
378
  }}>
570
379
  <span className="add-card__plus">+</span>
571
380
  <span className="add-card__label">新建本地团队</span>
@@ -575,164 +384,11 @@ export default function App() {
575
384
 
576
385
  {!profileLoading && !profileError && teams && teams.length === 0 && !localTeams?.length && (
577
386
  <div className="empty" style={{ marginTop: 14 }}>
578
- 还没有团队 — 让小助手帮你装一个本地 team,或在云端创建。
387
+ 还没有团队 — 安装本地 cicy-code 起一个本地 team,或在云端创建。
579
388
  </div>
580
389
  )}
581
390
  </main>
582
391
  </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
392
  </div>
737
393
  );
738
394
  }
@@ -769,7 +425,7 @@ function Section({ title, subtitle, icon, children }) {
769
425
  );
770
426
  }
771
427
 
772
- function LocalTeamCard({ team, onOpen, onRename }) {
428
+ function LocalTeamCard({ team, onOpen, onRename, onRefresh }) {
773
429
  const statusInfo = LOCAL_STATUS[team.status] || LOCAL_STATUS.error;
774
430
  const tone = statusInfo.tone;
775
431
  // Inline rename: double-click the name or click ✎ → edit → Enter/blur saves.
@@ -782,6 +438,29 @@ function LocalTeamCard({ team, onOpen, onRename }) {
782
438
  const next = String(draft || "").trim();
783
439
  if (onRename && next && next !== team.name) await onRename(team.id, next);
784
440
  };
441
+
442
+ // Lifecycle of the local cicy-code daemon (the :8008 sidecar this card
443
+ // represents): 重启 / 更新 / 停止, inline on the card itself. Only shown when
444
+ // the bridge exists (desktop build that owns the daemon).
445
+ const [busy, setBusy] = useState(""); // "" | "restart" | "update" | "stop"
446
+ const [opMsg, setOpMsg] = useState("");
447
+ const hasOps = !!window.cicy?.sidecar?.restart;
448
+ const runOp = async (e, kind, fn, doneText) => {
449
+ e.stopPropagation();
450
+ if (busy) return;
451
+ setBusy(kind); setOpMsg("");
452
+ try {
453
+ const r = await fn();
454
+ setOpMsg(r?.ok
455
+ ? (r.warning ? `${doneText}(${r.warning})` : doneText)
456
+ : tr("sidecar.failed", "失败") + (r?.error ? `: ${r.error}` : ""));
457
+ } catch (err) {
458
+ setOpMsg(tr("sidecar.failed", "失败") + `: ${err?.message || err}`);
459
+ } finally {
460
+ setBusy("");
461
+ onRefresh?.(); // re-probe so the status dot/chip catches up
462
+ }
463
+ };
785
464
  return (
786
465
  <div data-id="LocalTeamCard" className={`bcard bcard--local${tone === "ok" ? " bcard--online" : ""}`}>
787
466
  <div className="bcard__accent" />
@@ -822,6 +501,41 @@ function LocalTeamCard({ team, onOpen, onRename }) {
822
501
  <span className="bcard__chip">{statusInfo.label}</span>
823
502
  {team.version && <span className="bcard__chip">v{team.version}</span>}
824
503
  </div>
504
+ {hasOps && (
505
+ <div className="bcard__ops" data-id="LocalTeamCard-ops" onClick={(e) => e.stopPropagation()}>
506
+ <button
507
+ type="button"
508
+ data-id="LocalTeamCard-restart"
509
+ className="bcard__op"
510
+ disabled={!!busy}
511
+ title={tr("sidecar.restartHint", "重启本地 cicy-code")}
512
+ onClick={(e) => runOp(e, "restart", () => window.cicy.sidecar.restart(), tr("sidecar.restarted", "已重启"))}
513
+ >
514
+ {busy === "restart" ? <Spinner /> : null}{tr("sidecar.restart", "重启")}
515
+ </button>
516
+ <button
517
+ type="button"
518
+ data-id="LocalTeamCard-update"
519
+ className="bcard__op"
520
+ disabled={!!busy}
521
+ title={tr("sidecar.updateHint", "更新到最新版并重启")}
522
+ onClick={(e) => runOp(e, "update", () => window.cicy.sidecar.update(), tr("sidecar.updated", "已更新到最新"))}
523
+ >
524
+ {busy === "update" ? <Spinner /> : null}{tr("sidecar.update", "更新")}
525
+ </button>
526
+ <button
527
+ type="button"
528
+ data-id="LocalTeamCard-stop"
529
+ className="bcard__op bcard__op--danger"
530
+ disabled={!!busy || team.status !== "running"}
531
+ title={tr("sidecar.stopHint", "停止本地 cicy-code")}
532
+ onClick={(e) => runOp(e, "stop", () => window.cicy.sidecar.stop(), tr("sidecar.stoppedDone", "已停止"))}
533
+ >
534
+ {busy === "stop" ? <Spinner /> : null}{tr("sidecar.stop", "停止")}
535
+ </button>
536
+ </div>
537
+ )}
538
+ {opMsg && <div className="bcard__opmsg" data-id="LocalTeamCard-opmsg">{opMsg}</div>}
825
539
  </div>
826
540
  <button
827
541
  type="button"