@xgspz/cli-ui 0.6.15 → 0.6.16

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.
Files changed (68) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +21 -1
  3. package/dist/{cli-ui-embed-vIXVNs6k.js → cli-ui-embed-D9xgur2y.js} +18299 -18587
  4. package/dist/{cli-ui-index-R24x28OX.js → cli-ui-index-WBtxmw23.js} +2 -2
  5. package/dist/{cli-ui-jszip.min-DUghzByz.js → cli-ui-jszip.min-CU3ZzoXb.js} +1 -1
  6. package/dist/{cli-ui-jszip.min-DfH6rdQD.js → cli-ui-jszip.min-DdU0X3VJ.js} +2 -2
  7. package/dist/{cli-ui-xlsx-CP0lJhff.js → cli-ui-xlsx-Dx6T7qhC.js} +1 -1
  8. package/dist/cli-ui.css +1 -1
  9. package/dist/cli-ui.js +1 -1
  10. package/package.json +1 -1
  11. package/src/board/ChangesPane.tsx +10 -4
  12. package/src/board/CliBoard.tsx +48 -16
  13. package/src/board/FileViewer.tsx +34 -1
  14. package/src/board/diffRefresh.ts +63 -0
  15. package/src/board/linkage.ts +68 -0
  16. package/src/board/pathResolve.ts +108 -0
  17. package/src/chat/CliChat.tsx +76 -181
  18. package/src/chat/Composer.tsx +4 -3
  19. package/src/core/conversation.ts +9 -1
  20. package/src/core/store.ts +112 -38
  21. package/src/msglist/CliMsgList.tsx +3 -2
  22. package/src/operator/node/AgentConnect.tsx +19 -15
  23. package/src/operator/node/AppearancePanel.tsx +1 -1
  24. package/src/operator/node/BranchNavigator.tsx +1 -1
  25. package/src/operator/node/BrowserPane.tsx +1 -1
  26. package/src/operator/node/Chat.tsx +16 -261
  27. package/src/operator/node/CommandPalette.tsx +1 -1
  28. package/src/operator/node/Composer.tsx +1 -1
  29. package/src/operator/node/Layout.tsx +1 -1
  30. package/src/operator/node/Markdown.tsx +6 -36
  31. package/src/operator/node/Modal.tsx +1 -1
  32. package/src/operator/node/OnboardingWizard.tsx +9 -5
  33. package/src/operator/node/Orchestrator.tsx +1 -1
  34. package/src/operator/node/ProjectLanding.tsx +1 -1
  35. package/src/operator/node/ProjectsColumn.tsx +1 -1
  36. package/src/operator/node/ProviderAuth.tsx +12 -10
  37. package/src/operator/node/RunSwitch.tsx +1 -1
  38. package/src/operator/node/Services.tsx +1 -1
  39. package/src/operator/node/SessionRail.tsx +27 -8
  40. package/src/operator/node/SessionView.tsx +1 -1
  41. package/src/operator/node/SettingsView.tsx +1 -1
  42. package/src/operator/node/TaskBoard.tsx +1 -1
  43. package/src/operator/node/TaskChanges.tsx +34 -5
  44. package/src/operator/node/TaskHero.tsx +1 -1
  45. package/src/operator/node/TasksColumn.tsx +1 -1
  46. package/src/operator/node/Transcript.tsx +10 -237
  47. package/src/operator/node/Welcome.tsx +1 -1
  48. package/src/operator/node/WorktreePrune.tsx +1 -1
  49. package/src/operator/node/boardInteraction.ts +6 -3
  50. package/src/operator/node/boardState.ts +12 -13
  51. package/src/operator/node/fileLinks.ts +4 -38
  52. package/src/operator/node/format.ts +12 -104
  53. package/src/operator/node/github.tsx +10 -8
  54. package/src/operator/node/modals.tsx +15 -13
  55. package/src/operator/node/recheckOnReturn.ts +28 -0
  56. package/src/operator/node/shared.tsx +10 -50
  57. package/src/operator/node/useTaskStream.ts +2 -2
  58. package/src/styles.css +5 -0
  59. package/src/ui/Markdown.tsx +36 -21
  60. package/src/ui/Transcript.tsx +8 -5
  61. package/src/ui/fileLinks.ts +6 -3
  62. package/src/ui/format.ts +11 -5
  63. package/src/ui/shared.tsx +11 -7
  64. package/src/ui/transcriptModel.ts +40 -9
  65. package/src/ui/useTranscriptWindow.ts +253 -0
  66. package/src/operator/node/boundedPoll.ts +0 -47
  67. package/src/operator/node/icons.tsx +0 -88
  68. package/src/operator/node/transcriptModel.ts +0 -129
@@ -29,8 +29,11 @@ function normalize(p: string): string {
29
29
  return (abs ? "/" : "") + out.join("/");
30
30
  }
31
31
 
32
- /** 链接指向本地文件时返回路径(相对工作目录或绝对路径);网址、锚点等返回 null。规则与 Operator 一致。 */
33
- export function localFilePath(href: string | undefined | null): string | null {
32
+ /**
33
+ * 链接指向本地文件时返回路径(相对工作目录或绝对路径);网址、锚点等返回 null。中枢与节点适配共用。
34
+ * baseDir:相对路径相对的目录(节点的 Markdown 预览页里是 md 文件所在目录)。
35
+ */
36
+ export function localFilePath(href: string | undefined | null, baseDir = ""): string | null {
34
37
  let h = String(href ?? "").trim();
35
38
  if (!h || h.startsWith("#") || h.startsWith("//") || h.startsWith("/api/") || h.startsWith("/view?")) return null;
36
39
  if (/^file:/i.test(h)) h = h.replace(/^file:(\/\/localhost)?(\/\/)?/i, "");
@@ -42,7 +45,7 @@ export function localFilePath(href: string | undefined | null): string | null {
42
45
  if (!h) return null;
43
46
  if (h === "~" || h.startsWith("~/")) return h;
44
47
  if (h.startsWith("/") || WIN_ABS.test(h)) return WIN_ABS.test(h) ? h : normalize(h);
45
- return normalize(h);
48
+ return normalize(baseDir ? `${baseDir.replace(/\/+$/, "")}/${h}` : h);
46
49
  }
47
50
 
48
51
  /** File-shaped inline code is clickable; commands, models and code blocks remain code. */
package/src/ui/format.ts CHANGED
@@ -66,15 +66,20 @@ const AGENT_LABELS: Record<string, string> = { claude: "Claude Code", codex: "Co
66
66
  export const agentLabel = (agent: string | null | undefined) => (agent ? AGENT_LABELS[agent] ?? agent : "智能体");
67
67
 
68
68
  // 已解析的模型 id 的友好名("claude-opus-5" → "Opus 5";非 Claude 的原样显示)。
69
- export function modelLabel(id: string | null | undefined): string {
69
+ /** caps:智能体声明的模型列表(节点适配);认不出版本的 id 用它的名字。 */
70
+ export function modelLabel(id: string | null | undefined, caps?: { models?: ReadonlyArray<{ value: string; label: string }> }): string {
70
71
  if (!id) return "";
71
72
  const s = id.toLowerCase();
72
73
  const long = s.includes("[1m]") ? " (1M)" : "";
73
74
  const fam = ["fable", "opus", "sonnet", "haiku"].find((f) => s.includes(f));
74
- if (!fam) return id;
75
- const cap = fam[0].toUpperCase() + fam.slice(1);
76
- const v = s.match(new RegExp(`${fam}-(\\d+)(?:-(\\d+))?`));
77
- return v ? `${cap} ${v[1]}${v[2] ? `.${v[2]}` : ""}${long}` : `${cap}${long}`;
75
+ if (fam) {
76
+ const cap = fam[0].toUpperCase() + fam.slice(1);
77
+ const v = s.match(new RegExp(`${fam}-(\\d+)(?:-(\\d+))?`));
78
+ // 认不出版本时不套用能力列表里的名字:不给只是猜出来的版本打标。
79
+ return v ? `${cap} ${v[1]}${v[2] ? `.${v[2]}` : ""}${long}` : `${cap}${long}`;
80
+ }
81
+ const hit = (caps?.models ?? []).filter((o) => s.includes(o.value.toLowerCase())).sort((a, b) => b.value.length - a.value.length)[0];
82
+ return hit ? hit.label : id;
78
83
  }
79
84
 
80
85
  // ── Operator 的附件标记行 ─────────────────────────────
@@ -83,6 +88,7 @@ export function modelLabel(id: string | null | undefined): string {
83
88
  const ATTACHMENT_RE = /^\[Attached (image|file): (.+)\]$/;
84
89
  export interface MsgAttachment { path: string; kind: "image" | "file"; name: string; taskId: string }
85
90
 
91
+ /** 用户消息拆成文字 + 附件卡片。路径不是 uploads/<任务>/<文件>(手打的仿冒行)的标记行原样留在文字里。中枢与节点适配共用。 */
86
92
  export function splitAttachments(content: string): { text: string; attachments: MsgAttachment[] } {
87
93
  if (!content.includes("[Attached image: ") && !content.includes("[Attached file: ")) return { text: content, attachments: [] };
88
94
  const attachments: MsgAttachment[] = [];
package/src/ui/shared.tsx CHANGED
@@ -1,13 +1,13 @@
1
- // 小部件:状态点、头像、智能体标签、骨架条。与 Operator 的 shared.tsx 同样的类名和规则。
1
+ // 小部件:状态点、头像、智能体标签、骨架条、错误提示。中枢与节点适配共用(节点的 shared.tsx 在这些之上加按任务状态的状态点等)。
2
2
  import type { CSSProperties } from "react";
3
3
  import { AgentMark, Icon } from "./icons";
4
4
 
5
5
  /** Operator 的状态色:r 未开始 · a 进行中 · h 搁置 · g 完成 · x 取消 · b 运行中(蓝,闪)· c 等你处理(珊瑚,闪)。 */
6
6
  export type DotCls = "r" | "a" | "h" | "g" | "x";
7
7
 
8
- export function StatusDot({ cls, running, awaiting, title }: { cls: DotCls; running?: boolean; awaiting?: boolean; title?: string }) {
8
+ export function StatusDot({ cls, running, awaiting, title, lg }: { cls: DotCls; running?: boolean; awaiting?: boolean; title?: string; lg?: boolean }) {
9
9
  const c = awaiting ? "c" : running ? "b" : cls;
10
- return <span className={`sdot ${c} ${awaiting || running ? "pulse" : ""}`} title={title} />;
10
+ return <span className={`sdot ${c} ${lg ? "lg" : ""} ${awaiting || running ? "pulse" : ""}`} title={title} />;
11
11
  }
12
12
 
13
13
  export function Avatar({ who, agent }: { who: "user" | "cc"; agent?: string | null }) {
@@ -16,7 +16,9 @@ export function Avatar({ who, agent }: { who: "user" | "cc"; agent?: string | nu
16
16
  return <span className={`av cc${mark ? ` ${agent}` : ""}`}>{mark ? mark() : Icon.bolt()}</span>;
17
17
  }
18
18
 
19
- export function AgentBadge({ label }: { label: string }) {
19
+ /** multi=false:只配了一个智能体时不显示(节点适配)。 */
20
+ export function AgentBadge({ label, multi = true }: { label: string; multi?: boolean }) {
21
+ if (!multi) return null;
20
22
  return <span className="agent-badge" title={`运行于 ${label}`}>{label}</span>;
21
23
  }
22
24
 
@@ -28,11 +30,13 @@ export function Spinner({ size }: { size?: number }) {
28
30
  return <span className="spinner" role="status" aria-label="加载中" style={size ? { width: size, height: size } : undefined} />;
29
31
  }
30
32
 
31
- export function ErrNote({ children, onRetry }: { children: React.ReactNode; onRetry?: () => void }) {
33
+ export function ErrNote({ children, onRetry, retryLabel = "重试", style }: {
34
+ children: React.ReactNode; onRetry?: () => void; retryLabel?: string; style?: CSSProperties;
35
+ }) {
32
36
  return (
33
- <div className="err-note">
37
+ <div className="err-note" style={style}>
34
38
  <span className="err-msg">⚠ {children}</span>
35
- {onRetry && <button className="btn btn-line btn-sm" onClick={onRetry}>{Icon.restore()} 重试</button>}
39
+ {onRetry && <button className="btn btn-line btn-sm" onClick={onRetry}>{Icon.restore()} {retryLabel}</button>}
36
40
  </div>
37
41
  );
38
42
  }
@@ -10,6 +10,34 @@ export const RENDER_STEP = 80;
10
10
  /** DOM 里最多同时存在的行数,超过就丢掉远端的。 */
11
11
  export const RENDER_MAX = 360;
12
12
 
13
+ // 中枢适配(CliChat)与节点适配(Operator Chat / useTaskStream)共用这一份;节点适配的快照与历史分页大小也在这里。
14
+ /** 节点实时视图快照带的消息条数(GET /messages?tail=N)。 */
15
+ export const SNAPSHOT_TAIL = 300;
16
+ /** 往上翻时每页向 /history 取的条数(节点适配)。 */
17
+ export const HISTORY_PAGE = 300;
18
+ /**
19
+ * 一个标签页在内存里留的转录(中枢与节点适配共用):打开着的,加上最近离开的这么多个(切回去立刻显示)。
20
+ * 离开时每个都截到快照大小(trimToTail):真实数据下每个通常约 0.3 MB,100 个约 30 MB,最坏约 300 MB。
21
+ */
22
+ export const KEEP_TRANSCRIPTS = 100;
23
+
24
+ /**
25
+ * 离开的转录截回快照的大小(最新 tail 条;older 加上丢掉的条数):往上翻过很远的任务在后台不留几千条消息。
26
+ * 本来就不多时原样返回。
27
+ */
28
+ export function trimToTail<M extends Msg>(msgs: M[], older: number, tail = SNAPSHOT_TAIL): { msgs: M[]; older: number } {
29
+ const queued = msgs.filter((m) => m.role === "queued");
30
+ const rest = msgs.filter((m) => m.role !== "queued");
31
+ if (rest.length <= tail) return { msgs, older };
32
+ return { msgs: [...rest.slice(rest.length - tail), ...queued], older: older + rest.length - tail };
33
+ }
34
+
35
+ /** 打开 id 之后,留着的转录的先后(最近的在前),以及挤出去的。 */
36
+ export function touchRecent(recent: string[], id: string, keep = KEEP_TRANSCRIPTS): { recent: string[]; evicted: string[] } {
37
+ const next = [id, ...recent.filter((t) => t !== id)];
38
+ return { recent: next.slice(0, keep), evicted: next.slice(keep) };
39
+ }
40
+
13
41
  // 工具消息解析结果按消息对象缓存:内容变了消息对象就会换,缓存不会过期。
14
42
  const toolCache = new WeakMap<Msg, ToolData>();
15
43
  /** tool 消息背后的工具卡片数据。只读,不要改返回值。 */
@@ -61,25 +89,27 @@ export function hasOpenAsk(messages: Msg[], scan = 400): boolean {
61
89
  * 把(重连时的)快照合进已有的:没变的消息保持对象引用,memo 的行就不会重渲染(不会重新解析 markdown)。
62
90
  * 快照只是最新一段(older > 0)时,保留这个页面已经往上翻出来的更早的消息(与 Operator 的 mergeSnapshot 一致)。
63
91
  */
64
- export function mergeSnapshot(prev: Msg[] | undefined, committed: Msg[], older = 0): { msgs: Msg[]; older: number } {
65
- const byId = new Map<string, Msg>();
92
+ export function mergeSnapshot<M extends Msg>(prev: M[] | undefined, committed: M[], older = 0, queued: M[] = []): { msgs: M[]; older: number } {
93
+ const byId = new Map<string, M>();
66
94
  for (const m of prev ?? []) byId.set(m.id, m);
67
- const fresh = committed.map((m) => {
95
+ const reuse = (m: M): M => {
68
96
  const p = byId.get(m.id);
69
97
  if (!p || p.role !== m.role || p.content !== m.content || p.generation !== m.generation) return m;
70
98
  return p.ts === m.ts ? p : { ...p, ts: m.ts };
71
- });
72
- let kept: Msg[] = [];
99
+ };
100
+ const fresh = committed.map(reuse);
101
+ let kept: M[] = [];
73
102
  if (prev && fresh.length && older > 0) {
74
103
  const at = prev.findIndex((m) => m.id === fresh[0].id);
75
104
  // 只留落过库的(带时间的):本地补的说明行随旧视图一起作废。
76
105
  if (at > 0) kept = prev.slice(0, at).filter((m) => m.role !== "queued" && m.ts != null);
77
106
  }
78
- return { msgs: [...kept, ...fresh], older: Math.max(0, older - kept.length) };
107
+ // queued:节点适配里还排着的消息(中枢适配的排队在轮次里,不经过这里),排在最后。
108
+ return { msgs: [...kept, ...fresh, ...queued.map(reuse)], older: Math.max(0, older - kept.length) };
79
109
  }
80
110
 
81
111
  /** 往上翻取到的一页更早的消息接在最前面(已经有的跳过)。 */
82
- export function prependOlder(prev: Msg[], page: Msg[]): Msg[] {
112
+ export function prependOlder<M extends Msg>(prev: M[], page: M[]): M[] {
83
113
  const have = new Set(prev.map((m) => m.id));
84
114
  const add = page.filter((m) => !have.has(m.id));
85
115
  return add.length ? [...add, ...prev] : prev;
@@ -125,8 +155,9 @@ function waitRun(msgs: Msg[], start: number): { end: number; waits: number } | n
125
155
  /**
126
156
  * 转录展开成扁平的行(会话标签、/clear 分隔、消息),界面可以只渲染其中一个窗口。
127
157
  * hasOlder:更早的消息还没取,第一条就不一定是它那个会话的开场。
158
+ * fold:连续的浏览器续等收成一行(缺省收;节点适配的界面不画折叠行,传 false)。
128
159
  */
129
- export function buildTranscriptItems(messages: Msg[], hasOlder = false): TranscriptItem[] {
160
+ export function buildTranscriptItems(messages: Msg[], hasOlder = false, { fold = true }: { fold?: boolean } = {}): TranscriptItem[] {
130
161
  const sessions = buildSessions(messages);
131
162
  const items: TranscriptItem[] = [];
132
163
  sessions.forEach((s, si) => {
@@ -134,7 +165,7 @@ export function buildTranscriptItems(messages: Msg[], hasOlder = false): Transcr
134
165
  items.push({ kind: "label", key: `s-${s.n}`, n: s.n, current: si === sessions.length - 1 });
135
166
  for (let mi = 0; mi < s.messages.length; mi++) {
136
167
  const m = s.messages[mi];
137
- const run = isWaitCall(m) ? waitRun(s.messages, mi) : null;
168
+ const run = fold && isWaitCall(m) ? waitRun(s.messages, mi) : null;
138
169
  if (run) {
139
170
  items.push({ kind: "fold", key: `f-${m.id}`, msgs: s.messages.slice(mi, run.end), waits: run.waits });
140
171
  mi = run.end - 1;
@@ -0,0 +1,253 @@
1
+ // 分窗渲染的转录(中枢 CliChat 与节点 Chat 共用):DOM 里最多 RENDER_MAX 行,几千条消息也照样流畅。
2
+ //
3
+ // 跟随最新输出时是最新的 RENDER_WINDOW 行;往上翻时每次多显示 RENDER_STEP 行(手里的用完了就向后端取更早的一页),
4
+ // 同时丢掉远在下面的行;往下翻再接回来。「跳到最新」回到跟随。
5
+ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, type RefObject } from "react";
6
+ import { RENDER_MAX, RENDER_STEP, RENDER_WINDOW, type TranscriptItem } from "./transcriptModel";
7
+
8
+ export interface TranscriptWindowOptions {
9
+ scrollRef: RefObject<HTMLDivElement | null>;
10
+ items: TranscriptItem[];
11
+ /** 比 items 更早、还没取的消息条数。 */
12
+ older: number;
13
+ /** 取更早的一页:beforeApply 在这一页接到最前面之前调用(量出当时第一行的位置,接上后视图不跳)。返回取到没有。 */
14
+ loadOlder?: (beforeApply: () => void) => Promise<boolean>;
15
+ /** 跟随时,这两个变了就贴到最底。 */
16
+ messages: unknown;
17
+ running: boolean;
18
+ /** 换了它(例如换了对话)就回到最新、重新跟随;挂上时不算。 */
19
+ resetKey?: unknown;
20
+ /** 滚动区重新挂上(手机端切换视图、会话出现)时重新观察。 */
21
+ remountKey?: unknown;
22
+ }
23
+
24
+ type Anchor = { key: string; top: number };
25
+ type Win = { start: string | null; end: string | null };
26
+
27
+ export function useTranscriptWindow({ scrollRef, items, older, loadOlder, messages, running, resetKey, remountKey }: TranscriptWindowOptions) {
28
+ // 只在用户停在最底时自动滚动;往上翻着看时新消息照常进来,但不动用户的位置,给「跳到最新」。
29
+ const pinned = useRef(true);
30
+ const [atBottom, setAtBottom] = useState(true);
31
+ // 渲染的第一行 / 最后一行(消息行的 key);null = 跟着最新的算。
32
+ const [win, setWin] = useState<Win>({ start: null, end: null });
33
+ const { startIdx, endIdx } = useMemo(() => {
34
+ const find = (key: string | null) => (key ? items.findIndex((it) => it.key === key) : -1);
35
+ const e = find(win.end);
36
+ const last = e >= 0 ? e : items.length - 1;
37
+ let s = find(win.start);
38
+ // 窗口钉在消息行上(往前接一页时会话标签会移动):第一条消息上面的会话标签 / 分隔一起显示。
39
+ if (s >= 0) while (s > 0 && items[s - 1].kind !== "msg") s--;
40
+ return { startIdx: s >= 0 && s <= last ? s : Math.max(0, last + 1 - RENDER_WINDOW), endIdx: last };
41
+ }, [items, win]);
42
+ const shown = useMemo(() => items.slice(startIdx, endIdx + 1), [items, startIdx, endIdx]);
43
+ const hiddenAbove = useMemo(() => {
44
+ let n = older;
45
+ for (let i = 0; i < startIdx; i++) if (items[i].kind === "msg") n++;
46
+ return n;
47
+ }, [items, startIdx, older]);
48
+ const hiddenBelow = useMemo(() => {
49
+ let n = 0;
50
+ for (let i = endIdx + 1; i < items.length; i++) if (items[i].kind === "msg") n++;
51
+ return n;
52
+ }, [items, endIdx]);
53
+ const [loadingOlder, setLoadingOlder] = useState(false);
54
+ const [expandReq, setExpandReq] = useState(0);
55
+ const itemsRef = useRef(items); itemsRef.current = items;
56
+ const rangeRef = useRef({ startIdx, endIdx }); rangeRef.current = { startIdx, endIdx };
57
+ const winRef = useRef(win); winRef.current = win;
58
+ const olderRef = useRef(older); olderRef.current = older;
59
+ const loadOlderRef = useRef(loadOlder); loadOlderRef.current = loadOlder;
60
+ const hiddenRef = useRef({ above: hiddenAbove, below: hiddenBelow }); hiddenRef.current = { above: hiddenAbove, below: hiddenBelow };
61
+ const loadingOlderRef = useRef(false);
62
+ // 窗口变化之前量下的一行和它在屏幕上的位置;那次窗口变化提交后放回原处,读的人视图不动。
63
+ const anchorRef = useRef<(Anchor & { win: Win }) | null>(null);
64
+ // 更早的一页接上去之前量下的(见 revealEarlier)。
65
+ const pageAnchorRef = useRef<Anchor | null>(null);
66
+ // 消息导航要先显示出更多的行:显示完再接着跳。
67
+ const navAfterRevealRef = useRef<0 | -1 | 1>(0);
68
+
69
+ const measure = useCallback((key?: string): Anchor | null => {
70
+ const el = scrollRef.current;
71
+ const node = key ? el?.querySelector<HTMLElement>(`[data-tkey="${CSS.escape(key)}"]`) : el?.querySelector<HTMLElement>("[data-tkey][data-msg]");
72
+ return node?.dataset.tkey ? { key: node.dataset.tkey, top: node.getBoundingClientRect().top } : null;
73
+ }, [scrollRef]);
74
+ // 窗口两端记成消息行的 key:往前接一页时会话标签的位置会变,消息不会。
75
+ const setRange = useCallback((s: number, e: number, anchor?: Anchor | null) => {
76
+ const its = itemsRef.current;
77
+ let a = s;
78
+ while (a < its.length && its[a].kind !== "msg") a++;
79
+ let b = e;
80
+ while (b > 0 && its[b].kind !== "msg") b--;
81
+ const next = { start: its[a]?.key ?? null, end: e >= its.length - 1 ? null : its[b]?.key ?? null };
82
+ // 窗口没变:不改状态(也不留一个等不到提交的锚点)。
83
+ if (winRef.current.start === next.start && winRef.current.end === next.end) return;
84
+ if (anchor) anchorRef.current = { ...anchor, win: next };
85
+ setWin(next);
86
+ }, []);
87
+
88
+ // 在手里已有的行里往上扩(丢掉远在下面的);当前第一行仍在(RENDER_MAX > RENDER_STEP),用它做锚点。
89
+ const growUp = useCallback((anchor?: Anchor | null): boolean => {
90
+ const { startIdx: s, endIdx: e } = rangeRef.current;
91
+ if (s <= 0) return false;
92
+ const ns = Math.max(0, s - RENDER_STEP);
93
+ setRange(ns, Math.min(e, ns + RENDER_MAX - 1), anchor ?? measure());
94
+ return true;
95
+ }, [measure, setRange]);
96
+
97
+ // 显示更早的:先显示手里的,用完了向后端取一页。
98
+ const revealEarlier = useCallback(async () => {
99
+ if (loadingOlderRef.current || growUp()) return;
100
+ const load = loadOlderRef.current;
101
+ if (olderRef.current > 0 && load) {
102
+ // 窗口先停在现在这些行上,取到的一页接在它上面;下面的 effect 再显示那一页的末尾,锚点是接上之前第一行的位置。
103
+ const { startIdx: s, endIdx: e } = rangeRef.current;
104
+ setRange(s, e);
105
+ loadingOlderRef.current = true;
106
+ setLoadingOlder(true);
107
+ try {
108
+ if (await load(() => { pageAnchorRef.current = measure(); })) setExpandReq((n) => n + 1);
109
+ } finally {
110
+ loadingOlderRef.current = false;
111
+ setLoadingOlder(false);
112
+ }
113
+ }
114
+ }, [growUp, setRange, measure]);
115
+ // 往下接回后面的行(往上翻过之后),丢掉远在上面的;锚点是留下来的第一行。
116
+ const revealLater = useCallback(() => {
117
+ const its = itemsRef.current;
118
+ const { startIdx: s, endIdx: e } = rangeRef.current;
119
+ if (e >= its.length - 1) return;
120
+ const ne = Math.min(its.length - 1, e + RENDER_STEP);
121
+ const ns = Math.max(s, ne - RENDER_MAX + 1);
122
+ setRange(ns, ne, ns > s ? measure(its[ns].key) : null);
123
+ }, [measure, setRange]);
124
+ const revealRef = useRef({ earlier: revealEarlier, later: revealLater });
125
+ revealRef.current = { earlier: revealEarlier, later: revealLater };
126
+
127
+ // 更早的一页接到了窗口上面:显示那一页的末尾。
128
+ useLayoutEffect(() => {
129
+ if (!expandReq) return;
130
+ const anchor = pageAnchorRef.current;
131
+ pageAnchorRef.current = null;
132
+ growUp(anchor);
133
+ }, [expandReq, growUp]);
134
+
135
+ const onScroll = useCallback(() => {
136
+ const el = scrollRef.current;
137
+ if (!el) return;
138
+ // 「在最底」是指在最新的输出,不只是停在一个没到最新的窗口末尾。
139
+ const bottom = el.scrollHeight - el.scrollTop - el.clientHeight < 60 && winRef.current.end === null;
140
+ pinned.current = bottom;
141
+ setAtBottom(bottom);
142
+ if (!bottom && winRef.current.start === null) {
143
+ // 往回看:窗口定住,新输出进来不会把上面的行挤掉。
144
+ const first = itemsRef.current.slice(rangeRef.current.startIdx).find((it) => it.kind === "msg");
145
+ if (first) setWin((w) => ({ ...w, start: first.key }));
146
+ } else if (bottom && winRef.current.start !== null) {
147
+ // 回到最新:重新跟随(丢掉上面多显示的行)。
148
+ setWin({ start: null, end: null });
149
+ }
150
+ }, [scrollRef]);
151
+
152
+ // 读的人快到窗口两端时多显示一些(跟随最新时不做)。
153
+ const topSentinelRef = useRef<HTMLDivElement>(null);
154
+ const bottomSentinelRef = useRef<HTMLDivElement>(null);
155
+ const hasAbove = hiddenAbove > 0;
156
+ const hasBelow = hiddenBelow > 0;
157
+ useEffect(() => {
158
+ const root = scrollRef.current;
159
+ if (!root || typeof IntersectionObserver === "undefined") return;
160
+ const watch = (target: HTMLElement | null, margin: string, go: () => void) => {
161
+ if (!target) return null;
162
+ const io = new IntersectionObserver((entries) => { if (!pinned.current && entries.some((e) => e.isIntersecting)) go(); }, { root, rootMargin: margin });
163
+ io.observe(target);
164
+ return io;
165
+ };
166
+ const up = hasAbove ? watch(topSentinelRef.current, "1800px 0px 0px 0px", () => void revealRef.current.earlier()) : null;
167
+ const down = hasBelow ? watch(bottomSentinelRef.current, "0px 0px 1800px 0px", () => revealRef.current.later()) : null;
168
+ return () => { up?.disconnect(); down?.disconnect(); };
169
+ }, [hasAbove, hasBelow, remountKey, scrollRef]);
170
+
171
+ // 往回看着、长回合还在输出:窗口下沿跟着输出走,所以限在 RENDER_MAX 行。读的人在上半部分 → 不再往下扩
172
+ // (新输出等在「显示后面的消息」下面);在下半部分 → 丢掉最上面的行,视图不动。
173
+ useLayoutEffect(() => {
174
+ // +2:第一条消息上面可能还有会话分隔和标签。
175
+ if (win.start === null || win.end !== null || endIdx - startIdx + 1 <= RENDER_MAX + 2) return;
176
+ const el = scrollRef.current;
177
+ if (!el) return;
178
+ if (el.scrollTop < (el.scrollHeight - el.clientHeight) / 2) setRange(startIdx, startIdx + RENDER_MAX - 1);
179
+ else { const ns = endIdx - RENDER_MAX + 1; setRange(ns, endIdx, measure(itemsRef.current[ns].key)); }
180
+ }, [win, startIdx, endIdx, setRange, measure, scrollRef]);
181
+
182
+ const scrollToBottom = useCallback((behavior: ScrollBehavior = "smooth") => {
183
+ const el = scrollRef.current;
184
+ if (!el) return;
185
+ pinned.current = true;
186
+ setAtBottom(true);
187
+ // 没在显示最新的行:切回跟随,下面贴底的 effect 在它们画出来后落到最底。
188
+ if (winRef.current.start !== null || winRef.current.end !== null) { setWin({ start: null, end: null }); return; }
189
+ el.scrollTo({ top: el.scrollHeight, behavior });
190
+ }, [scrollRef]);
191
+
192
+ // 在「你」的消息之间跳(排队中的不算);到了窗口边上先多显示一些再跳。
193
+ const scrollToUserMsg = useCallback((dir: -1 | 1) => {
194
+ const el = scrollRef.current;
195
+ if (!el) return;
196
+ const base = el.getBoundingClientRect().top;
197
+ const tops = Array.from(el.querySelectorAll<HTMLElement>(".msg.user:not(.queued)")).map((n) => n.getBoundingClientRect().top - base + el.scrollTop);
198
+ const cur = el.scrollTop;
199
+ const candidates = dir < 0 ? tops.filter((t) => t < cur - 8) : tops.filter((t) => t > cur + 8);
200
+ if (!candidates.length && (dir < 0 ? hiddenRef.current.above : hiddenRef.current.below) > 0) {
201
+ navAfterRevealRef.current = dir;
202
+ if (dir < 0) void revealRef.current.earlier();
203
+ else revealRef.current.later();
204
+ return;
205
+ }
206
+ if (!tops.length) return;
207
+ const target = candidates.length ? (dir < 0 ? candidates[candidates.length - 1] : candidates[0]) : dir < 0 ? tops[0] : tops[tops.length - 1];
208
+ el.scrollTo({ top: Math.max(0, target - 12), behavior: "smooth" });
209
+ }, [scrollRef]);
210
+
211
+ // 窗口变了:把锚点那一行放回原处(浏览器自己的滚动锚定已经做了时什么都不做),再接着做没做完的导航。
212
+ // 只有量锚点时那次窗口变化的提交才放回(中途接上一页的提交不消耗它)。
213
+ useLayoutEffect(() => {
214
+ const a = anchorRef.current;
215
+ const el = scrollRef.current;
216
+ if (a && a.win === win) {
217
+ anchorRef.current = null;
218
+ const node = el?.querySelector<HTMLElement>(`[data-tkey="${CSS.escape(a.key)}"]`);
219
+ if (el && node) { const delta = node.getBoundingClientRect().top - a.top; if (Math.abs(delta) > 1) el.scrollTop += delta; }
220
+ }
221
+ const nav = navAfterRevealRef.current;
222
+ if (nav && !anchorRef.current) { navAfterRevealRef.current = 0; scrollToUserMsg(nav); }
223
+ }, [win, scrollToUserMsg, scrollRef]);
224
+
225
+ // 跟随时:内容进来、窗口丢掉上面的行,都停在最新一行。布局 effect,绘制前落位,不会跳一下。
226
+ useLayoutEffect(() => {
227
+ if (pinned.current && scrollRef.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
228
+ }, [messages, running, startIdx, endIdx, win, scrollRef]);
229
+ // 滚动区自己变大变小(输入框变高、出现提示条)时也贴底;内容变化不管,展开底部的工具卡片不会把它的顶部顶出视野。
230
+ useEffect(() => {
231
+ const el = scrollRef.current;
232
+ if (!el || typeof ResizeObserver === "undefined") return;
233
+ const ro = new ResizeObserver(() => { if (pinned.current) el.scrollTop = el.scrollHeight; });
234
+ ro.observe(el);
235
+ return () => ro.disconnect();
236
+ }, [remountKey, scrollRef]);
237
+
238
+ // 换了(例如换了对话):回到最新、重新跟随。挂上时不做(挂上时本来就是跟随)。
239
+ const lastReset = useRef(resetKey);
240
+ useEffect(() => {
241
+ if (Object.is(lastReset.current, resetKey)) return;
242
+ lastReset.current = resetKey;
243
+ pinned.current = true;
244
+ setAtBottom(true);
245
+ setWin({ start: null, end: null });
246
+ if (scrollRef.current) scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
247
+ }, [resetKey, scrollRef]);
248
+
249
+ /** 宿主自己定位了(例如分支任务定位到分支处):不再贴底跟随。 */
250
+ const unpin = useCallback(() => { pinned.current = false; setAtBottom(false); }, []);
251
+
252
+ return { shown, hiddenAbove, hiddenBelow, loadingOlder, atBottom, onScroll, revealEarlier, revealLater, scrollToBottom, scrollToUserMsg, unpin, topSentinelRef, bottomSentinelRef };
253
+ }
@@ -1,47 +0,0 @@
1
- "use client";
2
-
3
- import { useEffect, useRef } from "react";
4
-
5
- /**
6
- * Re-check something that settles elsewhere (a browser-side login approval, a
7
- * server job) — without an unbounded polling loop:
8
- * · while `active`, call `check` every `intervalMs`, but only for `windowMs`
9
- * after it became active (or after the last wake);
10
- * · when the user comes back — the tab visible again, the window focused,
11
- * back online — check once right away and open a fresh window. That is when
12
- * a login approved in another tab is most likely to have settled.
13
- * Nothing runs while the tab is hidden, and nothing at all once `active` is false.
14
- */
15
- export function useBoundedPoll(active: boolean, check: () => void, intervalMs: number, windowMs: number): void {
16
- const latest = useRef(check);
17
- latest.current = check;
18
- useEffect(() => {
19
- if (!active) return;
20
- let timer: ReturnType<typeof setInterval> | undefined;
21
- let deadline = 0;
22
- const stop = () => { if (timer) { clearInterval(timer); timer = undefined; } };
23
- const start = () => {
24
- deadline = Date.now() + windowMs;
25
- if (timer) return;
26
- timer = setInterval(() => {
27
- if (Date.now() > deadline) { stop(); return; }
28
- if (document.visibilityState !== "hidden") latest.current();
29
- }, intervalMs);
30
- };
31
- const wake = () => {
32
- if (document.visibilityState === "hidden") return;
33
- latest.current();
34
- start();
35
- };
36
- start();
37
- window.addEventListener("focus", wake);
38
- window.addEventListener("online", wake);
39
- document.addEventListener("visibilitychange", wake);
40
- return () => {
41
- stop();
42
- window.removeEventListener("focus", wake);
43
- window.removeEventListener("online", wake);
44
- document.removeEventListener("visibilitychange", wake);
45
- };
46
- }, [active, intervalMs, windowMs]);
47
- }
@@ -1,88 +0,0 @@
1
- /* Orchestrator — icon set (ported from the design's icons.js) */
2
- import type { SVGProps } from "react";
3
-
4
- type P = SVGProps<SVGSVGElement>;
5
-
6
- function S(children: React.ReactNode, props?: P) {
7
- return (
8
- <svg
9
- viewBox="0 0 24 24"
10
- fill="none"
11
- stroke="currentColor"
12
- strokeWidth={1.7}
13
- strokeLinecap="round"
14
- strokeLinejoin="round"
15
- width="1em"
16
- height="1em"
17
- style={{ flex: "none" }}
18
- {...props}
19
- >
20
- {children}
21
- </svg>
22
- );
23
- }
24
-
25
- export const Icon = {
26
- plus: (p?: P) => S(<><line x1={12} y1={5} x2={12} y2={19} /><line x1={5} y1={12} x2={19} y2={12} /></>, p),
27
- 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),
28
- 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),
29
- 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),
30
- 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),
31
- 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),
32
- send: (p?: P) => S(<><path d="M7 11l5-5 5 5" /><line x1={12} y1={6} x2={12} y2={18} /></>, p),
33
- 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),
34
- clock: (p?: P) => S(<><circle cx={12} cy={12} r={9} /><path d="M12 7v5l3 2" /></>, p),
35
- clear: (p?: P) => S(<><path d="M3 12a9 9 0 1 0 3-6.7" /><path d="M3 4v4h4" /></>, p),
36
- 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),
37
- chevDown: (p?: P) => S(<path d="M6 9l6 6 6-6" />, p),
38
- chevUp: (p?: P) => S(<path d="M6 15l6-6 6 6" />, p),
39
- chevRight: (p?: P) => S(<path d="M9 6l6 6-6 6" />, p),
40
- toBottom: (p?: P) => S(<><path d="M7 7l5 5 5-5" /><line x1={5} y1={17} x2={19} y2={17} /></>, p),
41
- x: (p?: P) => S(<><line x1={6} y1={6} x2={18} y2={18} /><line x1={6} y1={18} x2={18} y2={6} /></>, p),
42
- 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),
43
- check: (p?: P) => S(<path d="M5 12l5 5 9-11" />, p),
44
- 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),
45
- 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),
46
- 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),
47
- 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),
48
- 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),
49
- flag: (p?: P) => S(<path d="M5 21V4h12l-2 4 2 4H5" />, p),
50
- bolt: (p?: P) => S(<path d="M13 2L4 14h7l-1 8 9-12h-7z" />, p),
51
- 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),
52
- 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),
53
- 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),
54
- 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),
55
- play: (p?: P) => S(<path d="M7 5l11 7-11 7z" />, p),
56
- stop: (p?: P) => S(<rect x={6} y={6} width={12} height={12} rx={2} />, p),
57
- 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),
58
- search: (p?: P) => S(<><circle cx={11} cy={11} r={7} /><line x1={21} y1={21} x2={16.65} y2={16.65} /></>, p),
59
- 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),
60
- 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),
61
- 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),
62
- 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),
63
- };
64
-
65
- // ---- agent brand marks ----
66
- // Logos, not line icons: filled paths in `currentColor` (the stroked `S()` set
67
- // above would smear these at avatar size). Keyed by agent driver id so any UI
68
- // that knows a task's `agent` can stamp the right mark; unknown/legacy ids get
69
- // no entry and callers fall back to a generic glyph.
70
-
71
- function Brand(children: React.ReactNode, props?: P) {
72
- return (
73
- <svg viewBox="0 0 24 24" fill="currentColor" stroke="none" width="1em" height="1em" style={{ flex: "none" }} aria-hidden {...props}>
74
- {children}
75
- </svg>
76
- );
77
- }
78
-
79
- export const AgentMark: Record<string, (p?: P) => React.ReactElement> = {
80
- claude: (p?: P) => Brand(
81
- <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" />,
82
- p,
83
- ),
84
- codex: (p?: P) => Brand(
85
- <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" />,
86
- p,
87
- ),
88
- };