@spzxg/cli-ui 0.0.0-stage → 0.3.0

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.
@@ -0,0 +1,89 @@
1
+ // 图标:原样取自 operator-oss/app/icons.tsx,保持与 Operator 界面一致。
2
+ /* Orchestrator — icon set (ported from the design's icons.js) */
3
+ import type { ReactElement, ReactNode, SVGProps } from "react";
4
+
5
+ type P = SVGProps<SVGSVGElement>;
6
+
7
+ function S(children: ReactNode, props?: P) {
8
+ return (
9
+ <svg
10
+ viewBox="0 0 24 24"
11
+ fill="none"
12
+ stroke="currentColor"
13
+ strokeWidth={1.7}
14
+ strokeLinecap="round"
15
+ strokeLinejoin="round"
16
+ width="1em"
17
+ height="1em"
18
+ style={{ flex: "none" }}
19
+ {...props}
20
+ >
21
+ {children}
22
+ </svg>
23
+ );
24
+ }
25
+
26
+ export const Icon = {
27
+ plus: (p?: P) => S(<><line x1={12} y1={5} x2={12} y2={19} /><line x1={5} y1={12} x2={19} y2={12} /></>, p),
28
+ list: (p?: P) => S(<><line x1={9} y1={6} x2={20} y2={6} /><line x1={9} y1={12} x2={20} y2={12} /><line x1={9} y1={18} x2={20} y2={18} /><circle cx={5} cy={6} r={0.5} /><circle cx={5} cy={12} r={0.5} /><circle cx={5} cy={18} r={0.5} /></>, p),
29
+ board: (p?: P) => S(<><rect x={3} y={4} width={5} height={16} rx={1.5} /><rect x={10} y={4} width={5} height={11} rx={1.5} /><rect x={17} y={4} width={4} height={8} rx={1.5} /></>, p),
30
+ chart: (p?: P) => S(<><line x1={5} y1={20} x2={5} y2={13} /><line x1={12} y1={20} x2={12} y2={5} /><line x1={19} y1={20} x2={19} y2={9} /></>, p),
31
+ folder: (p?: P) => S(<path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />, p),
32
+ gear: (p?: P) => S(<><circle cx={12} cy={12} r={3} /><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09a1.65 1.65 0 0 0-1-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09a1.65 1.65 0 0 0 1.51-1 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" /></>, p),
33
+ send: (p?: P) => S(<><path d="M7 11l5-5 5 5" /><line x1={12} y1={6} x2={12} y2={18} /></>, p),
34
+ spark: (p?: P) => S(<path d="M12 3l1.6 5.4L19 10l-5.4 1.6L12 17l-1.6-5.4L5 10l5.4-1.6z" />, p),
35
+ clock: (p?: P) => S(<><circle cx={12} cy={12} r={9} /><path d="M12 7v5l3 2" /></>, p),
36
+ clear: (p?: P) => S(<><path d="M3 12a9 9 0 1 0 3-6.7" /><path d="M3 4v4h4" /></>, p),
37
+ dots: (p?: P) => S(<><circle cx={5} cy={12} r={1} /><circle cx={12} cy={12} r={1} /><circle cx={19} cy={12} r={1} /></>, p),
38
+ chevDown: (p?: P) => S(<path d="M6 9l6 6 6-6" />, p),
39
+ chevUp: (p?: P) => S(<path d="M6 15l6-6 6 6" />, p),
40
+ chevRight: (p?: P) => S(<path d="M9 6l6 6-6 6" />, p),
41
+ toBottom: (p?: P) => S(<><path d="M7 7l5 5 5-5" /><line x1={5} y1={17} x2={19} y2={17} /></>, p),
42
+ x: (p?: P) => S(<><line x1={6} y1={6} x2={18} y2={18} /><line x1={6} y1={18} x2={18} y2={6} /></>, p),
43
+ clip: (p?: P) => S(<path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48" />, p),
44
+ check: (p?: P) => S(<path d="M5 12l5 5 9-11" />, p),
45
+ lock: (p?: P) => S(<><rect x={4} y={11} width={16} height={9} rx={2} /><path d="M8 11V7a4 4 0 0 1 8 0v4" /></>, p),
46
+ edit: (p?: P) => S(<><path d="M12 20h9" /><path d="M16.5 3.5a2.1 2.1 0 0 1 3 3L7 19l-4 1 1-4z" /></>, p),
47
+ doc: (p?: P) => S(<><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" /><path d="M14 2v6h6" /></>, p),
48
+ github: (p?: P) => S(<path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22" />, p),
49
+ git: (p?: P) => S(<><circle cx={6} cy={6} r={2} /><circle cx={6} cy={18} r={2} /><circle cx={18} cy={9} r={2} /><path d="M18 11a8 8 0 0 1-8 8" /><line x1={6} y1={8} x2={6} y2={16} /></>, p),
50
+ flag: (p?: P) => S(<path d="M5 21V4h12l-2 4 2 4H5" />, p),
51
+ bolt: (p?: P) => S(<path d="M13 2L4 14h7l-1 8 9-12h-7z" />, p),
52
+ moon: (p?: P) => S(<path d="M21 12.8A9 9 0 1 1 11.2 3a7 7 0 0 0 9.8 9.8z" />, p),
53
+ sun: (p?: P) => S(<><circle cx={12} cy={12} r={4} /><line x1={12} y1={2} x2={12} y2={5} /><line x1={12} y1={19} x2={12} y2={22} /><line x1={2} y1={12} x2={5} y2={12} /><line x1={19} y1={12} x2={22} y2={12} /><line x1={4.9} y1={4.9} x2={6.8} y2={6.8} /><line x1={17.2} y1={17.2} x2={19.1} y2={19.1} /><line x1={4.9} y1={19.1} x2={6.8} y2={17.2} /><line x1={17.2} y1={6.8} x2={19.1} y2={4.9} /></>, p),
54
+ external: (p?: P) => S(<><path d="M14 4h6v6" /><path d="M20 4l-9 9" /><path d="M19 13v5a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h5" /></>, p),
55
+ copy: (p?: P) => S(<><rect x={9} y={9} width={12} height={12} rx={2} /><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" /></>, p),
56
+ play: (p?: P) => S(<path d="M7 5l11 7-11 7z" />, p),
57
+ stop: (p?: P) => S(<rect x={6} y={6} width={12} height={12} rx={2} />, p),
58
+ sliders: (p?: P) => S(<><line x1={4} y1={21} x2={4} y2={14} /><line x1={4} y1={10} x2={4} y2={3} /><line x1={12} y1={21} x2={12} y2={12} /><line x1={12} y1={8} x2={12} y2={3} /><line x1={20} y1={21} x2={20} y2={16} /><line x1={20} y1={12} x2={20} y2={3} /><line x1={1} y1={14} x2={7} y2={14} /><line x1={9} y1={8} x2={15} y2={8} /><line x1={17} y1={16} x2={23} y2={16} /></>, p),
59
+ search: (p?: P) => S(<><circle cx={11} cy={11} r={7} /><line x1={21} y1={21} x2={16.65} y2={16.65} /></>, p),
60
+ terminal: (p?: P) => S(<><rect x={3} y={4} width={18} height={16} rx={2} /><path d="M7 9l3 3-3 3" /><line x1={13} y1={15} x2={17} y2={15} /></>, p),
61
+ archive: (p?: P) => S(<><rect x={3} y={4} width={18} height={4} rx={1} /><path d="M5 8v11a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V8" /><line x1={10} y1={12} x2={14} y2={12} /></>, p),
62
+ restore: (p?: P) => S(<><path d="M3 12a9 9 0 1 0 3-6.7" /><path d="M3 4v4h4" /><path d="M12 8v4l3 2" /></>, p),
63
+ download: (p?: P) => S(<><path d="M12 4v11" /><path d="M7 11l5 5 5-5" /><line x1={5} y1={20} x2={19} y2={20} /></>, p),
64
+ };
65
+
66
+ // ---- agent brand marks ----
67
+ // Logos, not line icons: filled paths in `currentColor` (the stroked `S()` set
68
+ // above would smear these at avatar size). Keyed by agent driver id so any UI
69
+ // that knows a task's `agent` can stamp the right mark; unknown/legacy ids get
70
+ // no entry and callers fall back to a generic glyph.
71
+
72
+ function Brand(children: ReactNode, props?: P) {
73
+ return (
74
+ <svg viewBox="0 0 24 24" fill="currentColor" stroke="none" width="1em" height="1em" style={{ flex: "none" }} aria-hidden {...props}>
75
+ {children}
76
+ </svg>
77
+ );
78
+ }
79
+
80
+ export const AgentMark: Record<string, (p?: P) => ReactElement> = {
81
+ claude: (p?: P) => Brand(
82
+ <path d="M4.709 15.955l4.72-2.647.079-.23-.08-.128H9.2l-.79-.048-2.698-.073-2.339-.097-2.266-.122-.571-.121L0 11.784l.055-.352.48-.321.686.06 1.52.103 2.278.158 1.652.097 2.449.255h.389l.055-.157-.134-.098-.103-.097-2.358-1.596-2.552-1.688-1.336-.972-.724-.491-.364-.462-.158-1.008.656-.722.881.06.225.061.893.686 1.908 1.476 2.491 1.833.365.304.145-.103.019-.073-.164-.274-1.355-2.446-1.446-2.49-.644-1.032-.17-.619a2.97 2.97 0 0 1-.104-.729L6.283.134 6.696 0l.996.134.42.364.62 1.414 1.002 2.229 1.555 3.03.456.898.243.832.091.255h.158V9.01l.128-1.706.237-2.095.23-2.695.08-.76.376-.91.747-.492.584.28.48.685-.067.444-.286 1.851-.559 2.903-.364 1.942h.212l.243-.242.984-1.306 1.652-2.064.73-.82.85-.904.547-.431h1.033l.76 1.129-.34 1.166-1.064 1.347-.881 1.142-1.264 1.7-.79 1.36.073.11.188-.02 2.856-.606 1.543-.28 1.841-.315.833.388.091.395-.328.807-1.969.486-2.309.462-3.439.813-.042.03.049.061 1.549.146.662.036h1.622l3.02.225.79.522.474.638-.079.485-1.215.62-1.64-.389-3.829-.91-1.312-.329h-.182v.11l1.093 1.068 2.003 1.81 2.508 2.33.127.578-.322.455-.34-.049-2.204-1.657-.851-.747-1.926-1.62h-.128v.17l.444.649 2.345 3.521.122 1.08-.17.353-.608.213-.668-.122-1.374-1.925-1.415-2.167-1.143-1.943-.14.08-.674 7.254-.316.37-.729.28-.607-.461-.322-.747.322-1.476.389-1.924.315-1.53.286-1.9.17-.632-.012-.042-.14.018-1.434 1.967-2.18 2.945-1.726 1.845-.414.164-.717-.37.067-.662.401-.589 2.388-3.036 1.44-1.882.93-1.086-.006-.158h-.055L4.132 18.56l-1.13.146-.487-.456.061-.746.231-.243 1.908-1.312-.006.006z" />,
83
+ p,
84
+ ),
85
+ codex: (p?: P) => Brand(
86
+ <path d="M22.282 9.821a5.985 5.985 0 0 0-.516-4.911 6.046 6.046 0 0 0-6.51-2.9A6.065 6.065 0 0 0 4.981 4.182a5.985 5.985 0 0 0-3.998 2.9 6.046 6.046 0 0 0 .743 7.097 5.98 5.98 0 0 0 .51 4.911 6.051 6.051 0 0 0 6.515 2.9A5.985 5.985 0 0 0 13.26 24a6.056 6.056 0 0 0 5.772-4.206 5.99 5.99 0 0 0 3.998-2.9 6.056 6.056 0 0 0-.748-7.073zm-9.022 12.608a4.476 4.476 0 0 1-2.876-1.04l.142-.081 4.778-2.758a.795.795 0 0 0 .393-.681v-6.737l2.02 1.168a.071.071 0 0 1 .038.052v5.583a4.504 4.504 0 0 1-4.495 4.494zM3.6 18.304a4.471 4.471 0 0 1-.535-3.014l.142.085 4.783 2.758a.771.771 0 0 0 .78 0l5.843-3.368v2.332a.08.08 0 0 1-.033.062L9.74 19.95a4.499 4.499 0 0 1-6.14-1.646zM2.34 7.896a4.485 4.485 0 0 1 2.366-1.973V11.6a.766.766 0 0 0 .388.677l5.814 3.354-2.02 1.169a.076.076 0 0 1-.071 0l-4.83-2.787A4.504 4.504 0 0 1 2.34 7.872zm16.597 3.856L13.104 8.364 15.119 7.2a.076.076 0 0 1 .071 0l4.83 2.791a4.494 4.494 0 0 1-.676 8.105v-5.678a.79.79 0 0 0-.407-.667zm2.01-3.024l-.141-.085-4.774-2.782a.776.776 0 0 0-.785 0L9.409 9.23V6.897a.066.066 0 0 1 .028-.061l4.83-2.787a4.499 4.499 0 0 1 6.681 4.66zM8.307 12.863l-2.02-1.164a.08.08 0 0 1-.038-.057V6.074a4.499 4.499 0 0 1 7.376-3.453l-.142.08-4.778 2.758a.795.795 0 0 0-.393.682zm1.097-2.365l2.602-1.5 2.607 1.5v2.999l-2.597 1.5-2.607-1.5z" />,
87
+ p,
88
+ ),
89
+ };
@@ -0,0 +1,38 @@
1
+ // 小部件:状态点、头像、智能体标签、骨架条。与 Operator 的 shared.tsx 同样的类名和规则。
2
+ import type { CSSProperties } from "react";
3
+ import { AgentMark, Icon } from "./icons.js";
4
+
5
+ /** Operator 的状态色:r 未开始 · a 进行中 · h 搁置 · g 完成 · x 取消 · b 运行中(蓝,闪)· c 等你处理(珊瑚,闪)。 */
6
+ export type DotCls = "r" | "a" | "h" | "g" | "x";
7
+
8
+ export function StatusDot({ cls, running, awaiting, title }: { cls: DotCls; running?: boolean; awaiting?: boolean; title?: string }) {
9
+ const c = awaiting ? "c" : running ? "b" : cls;
10
+ return <span className={`sdot ${c} ${awaiting || running ? "pulse" : ""}`} title={title} />;
11
+ }
12
+
13
+ export function Avatar({ who, agent }: { who: "user" | "cc"; agent?: string | null }) {
14
+ if (who === "user") return <span className="av you">A</span>;
15
+ const mark = agent ? AgentMark[agent] : undefined;
16
+ return <span className={`av cc${mark ? ` ${agent}` : ""}`}>{mark ? mark() : Icon.bolt()}</span>;
17
+ }
18
+
19
+ export function AgentBadge({ label }: { label: string }) {
20
+ return <span className="agent-badge" title={`运行于 ${label}`}>{label}</span>;
21
+ }
22
+
23
+ export function Skel({ w, h = 10, r, style }: { w: number | string; h?: number; r?: number | string; style?: CSSProperties }) {
24
+ return <span className="skel" aria-hidden style={{ width: w, height: h, ...(r !== undefined ? { borderRadius: r } : null), ...style }} />;
25
+ }
26
+
27
+ export function Spinner({ size }: { size?: number }) {
28
+ return <span className="spinner" role="status" aria-label="加载中" style={size ? { width: size, height: size } : undefined} />;
29
+ }
30
+
31
+ export function ErrNote({ children, onRetry }: { children: React.ReactNode; onRetry?: () => void }) {
32
+ return (
33
+ <div className="err-note">
34
+ <span className="err-msg">⚠ {children}</span>
35
+ {onRetry && <button className="btn btn-line btn-sm" onClick={onRetry}>{Icon.restore()} 重试</button>}
36
+ </div>
37
+ );
38
+ }
@@ -0,0 +1,94 @@
1
+ // 实时转录背后的纯函数(无 React,可单测)。规则与 Operator 的 transcriptModel.ts 一致:
2
+ // 为长对话设计——成千上万条消息、几 MB 的工具输出。
3
+ import type { Msg, ToolData } from "../core/types.js";
4
+ import { buildSessions } from "./format.js";
5
+
6
+ /** 跟随最新输出时 DOM 里保留的行数。 */
7
+ export const RENDER_WINDOW = 120;
8
+ /** 往回翻(或再往下)时每次多显示的行数。 */
9
+ export const RENDER_STEP = 80;
10
+ /** DOM 里最多同时存在的行数,超过就丢掉远端的。 */
11
+ export const RENDER_MAX = 360;
12
+
13
+ // 工具消息解析结果按消息对象缓存:内容变了消息对象就会换,缓存不会过期。
14
+ const toolCache = new WeakMap<Msg, ToolData>();
15
+ /** tool 消息背后的工具卡片数据。只读,不要改返回值。 */
16
+ export function toolDataOf(m: Msg): ToolData {
17
+ let data = toolCache.get(m);
18
+ if (!data) {
19
+ try { data = sanitizeTool(JSON.parse(m.content)); } catch { data = { title: m.content }; }
20
+ toolCache.set(m, data);
21
+ }
22
+ return data;
23
+ }
24
+
25
+ /**
26
+ * 工具卡片的数据来自节点,格式不对的部分丢掉而不是让整个对话渲染崩掉:
27
+ * 提问卡片要有 questions 数组,浏览器输入卡片要有 fields 数组,其余字段按类型保留。
28
+ */
29
+ function sanitizeTool(raw: unknown): ToolData {
30
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return { title: typeof raw === "string" ? raw : String(raw) };
31
+ const d = { ...(raw as Record<string, unknown>) } as Record<string, unknown> & ToolData;
32
+ if (typeof d.title !== "string") d.title = d.title == null ? "" : String(d.title);
33
+ for (const k of ["detail", "result"] as const) if (d[k] != null && typeof d[k] !== "string") d[k] = JSON.stringify(d[k]);
34
+ const ask = d.ask as { questions?: unknown } | undefined;
35
+ if (ask && (typeof ask !== "object" || !Array.isArray(ask.questions))) delete d.ask;
36
+ const input = d.input as { fields?: unknown; id?: unknown } | undefined;
37
+ if (input && (typeof input !== "object" || !Array.isArray(input.fields))) delete d.input;
38
+ if (d.diff && !Array.isArray(d.diff)) delete d.diff;
39
+ if (d.peek && typeof d.peek !== "object") delete d.peek;
40
+ return d;
41
+ }
42
+
43
+ /** 从最新一端找这个 id 的消息(实时更新都落在末尾)。 */
44
+ export function indexFromEnd(arr: Msg[], id: string): number {
45
+ for (let i = arr.length - 1; i >= 0; i--) if (arr[i].id === id) return i;
46
+ return -1;
47
+ }
48
+
49
+ /** 末尾附近还有没回答的 AskUserQuestion 卡片。 */
50
+ export function hasOpenAsk(messages: Msg[], scan = 400): boolean {
51
+ for (let i = messages.length - 1, n = 0; i >= 0 && n < scan; i--, n++) {
52
+ const m = messages[i];
53
+ if (m.role !== "tool") continue;
54
+ const d = toolDataOf(m);
55
+ if (d.ask && !d.ask.answers) return true;
56
+ }
57
+ return false;
58
+ }
59
+
60
+ /**
61
+ * 把(重连时的)全量转录合进已有的:没变的消息保持对象引用,memo 的行就不会重渲染(不会重新解析 markdown)。
62
+ */
63
+ export function mergeSnapshot(prev: Msg[] | undefined, committed: Msg[]): Msg[] {
64
+ const byId = new Map<string, Msg>();
65
+ for (const m of prev ?? []) byId.set(m.id, m);
66
+ return committed.map((m) => {
67
+ const p = byId.get(m.id);
68
+ if (!p || p.role !== m.role || p.content !== m.content || p.generation !== m.generation) return m;
69
+ return p.ts === m.ts ? p : { ...p, ts: m.ts };
70
+ });
71
+ }
72
+
73
+ export type TranscriptItem =
74
+ | { kind: "label"; key: string; n: number; current: boolean }
75
+ | { kind: "break"; key: string; summary: string }
76
+ | { kind: "msg"; key: string; m: Msg; initial: boolean; hideWho: boolean };
77
+
78
+ /** 转录展开成扁平的行(会话标签、/clear 分隔、消息),界面可以只渲染其中一个窗口。 */
79
+ export function buildTranscriptItems(messages: Msg[]): TranscriptItem[] {
80
+ const sessions = buildSessions(messages);
81
+ const items: TranscriptItem[] = [];
82
+ sessions.forEach((s, si) => {
83
+ if (si > 0 && s.summaryBefore) items.push({ kind: "break", key: `b-${s.n}`, summary: s.summaryBefore });
84
+ items.push({ kind: "label", key: `s-${s.n}`, n: s.n, current: si === sessions.length - 1 });
85
+ s.messages.forEach((m, mi) => {
86
+ const prev = s.messages[mi - 1];
87
+ // 一段连续的智能体输出(文字 → 工具 → 文字)只显示一次头像行
88
+ const hideWho = m.role === "assistant" && !!prev && (prev.role === "assistant" || prev.role === "tool");
89
+ const initial = mi === 0 && m.role === "user";
90
+ items.push({ kind: "msg", key: m.id, m, initial, hideWho });
91
+ });
92
+ });
93
+ return items;
94
+ }