@xgspz/cli-ui 0.0.0-stage → 0.6.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +165 -0
- package/README.md +517 -2
- package/dist/cli-ui-embed-vIXVNs6k.js +50295 -0
- package/dist/cli-ui-index-R24x28OX.js +11209 -0
- package/dist/cli-ui-jszip.min-DUghzByz.js +2324 -0
- package/dist/cli-ui-jszip.min-DfH6rdQD.js +26 -0
- package/dist/cli-ui-xlsx-CP0lJhff.js +93 -0
- package/dist/cli-ui.css +32 -0
- package/dist/cli-ui.js +17 -0
- package/package.json +60 -4
- package/src/board/BrowserPane.tsx +969 -0
- package/src/board/ChangesPane.tsx +185 -0
- package/src/board/CliBoard.tsx +484 -0
- package/src/board/FileViewer.tsx +721 -0
- package/src/board/FilesPane.tsx +262 -0
- package/src/board/HtmlReadingPreview.tsx +52 -0
- package/src/board/MarkdownPreview.tsx +134 -0
- package/src/board/PanelControls.tsx +40 -0
- package/src/board/blocks.tsx +97 -0
- package/src/board/context.ts +65 -0
- package/src/board/diffLines.ts +46 -0
- package/src/board/fileKind.ts +250 -0
- package/src/board/fileOps.ts +85 -0
- package/src/board/fileScope.ts +32 -0
- package/src/board/fileSync.ts +351 -0
- package/src/board/fileSyncCache.ts +250 -0
- package/src/board/fileSyncProcessor.ts +72 -0
- package/src/board/fileSyncQueue.ts +56 -0
- package/src/board/fileSyncTransport.ts +85 -0
- package/src/board/fileWatch.ts +211 -0
- package/src/board/folderDownload.ts +211 -0
- package/src/board/frameQuote.ts +52 -0
- package/src/board/htmlReading.ts +148 -0
- package/src/board/panels.ts +33 -0
- package/src/board/textLoad.ts +76 -0
- package/src/board/useBoardUpload.tsx +51 -0
- package/src/board/useBoardVisibility.ts +35 -0
- package/src/board/workspace.ts +200 -0
- package/src/board/zip.ts +85 -0
- package/src/chat/CliChat.tsx +960 -0
- package/src/chat/Composer.tsx +239 -0
- package/src/chat/GroupChat.tsx +113 -0
- package/src/chat/RunControls.tsx +196 -0
- package/src/chat/configuration.ts +15 -0
- package/src/chat/draft.ts +49 -0
- package/src/chat/export.ts +34 -0
- package/src/chat/queueEdit.ts +27 -0
- package/src/chat/useExecutionConfig.ts +93 -0
- package/src/components.tsx +13 -0
- package/src/core/blocks.ts +205 -0
- package/src/core/callbacks.ts +8 -0
- package/src/core/client.ts +822 -0
- package/src/core/config.ts +252 -0
- package/src/core/conversation.ts +581 -0
- package/src/core/documentUpload.ts +196 -0
- package/src/core/execution.ts +24 -0
- package/src/core/id.ts +15 -0
- package/src/core/identity.ts +21 -0
- package/src/core/provider.tsx +141 -0
- package/src/core/sha256.ts +21 -0
- package/src/core/sse.ts +51 -0
- package/src/core/store.ts +1109 -0
- package/src/core/subagents.ts +78 -0
- package/src/core/textDecode.ts +24 -0
- package/src/core/types.ts +507 -0
- package/src/core/xlsx.ts +142 -0
- package/src/elements.tsx +376 -0
- package/src/embed.ts +56 -0
- package/src/index.ts +56 -0
- package/src/layout/CliSplit.tsx +53 -0
- package/src/layout/split.ts +251 -0
- package/src/msglist/CliMsgList.tsx +422 -0
- package/src/operator/branchFlow.ts +35 -0
- package/src/operator/index.ts +9 -0
- package/src/operator/liveStream.ts +1 -0
- package/src/operator/node/AgentConnect.tsx +619 -0
- package/src/operator/node/AppearancePanel.tsx +35 -0
- package/src/operator/node/BranchHistory.tsx +67 -0
- package/src/operator/node/BranchNavigator.tsx +36 -0
- package/src/operator/node/BrowserPane.tsx +128 -0
- package/src/operator/node/BrowserPopup.tsx +22 -0
- package/src/operator/node/Chat.tsx +538 -0
- package/src/operator/node/CommandPalette.tsx +213 -0
- package/src/operator/node/Composer.tsx +316 -0
- package/src/operator/node/InsightsView.tsx +831 -0
- package/src/operator/node/Layout.tsx +192 -0
- package/src/operator/node/Markdown.tsx +61 -0
- package/src/operator/node/Modal.tsx +179 -0
- package/src/operator/node/NeedsYouMenu.tsx +54 -0
- package/src/operator/node/OnboardingWizard.tsx +248 -0
- package/src/operator/node/Orchestrator.tsx +644 -0
- package/src/operator/node/ProjectLanding.tsx +86 -0
- package/src/operator/node/ProjectsColumn.tsx +137 -0
- package/src/operator/node/ProviderAuth.tsx +559 -0
- package/src/operator/node/RunControls.tsx +2 -0
- package/src/operator/node/RunSwitch.tsx +113 -0
- package/src/operator/node/Services.tsx +245 -0
- package/src/operator/node/SessionRail.tsx +299 -0
- package/src/operator/node/SessionView.tsx +292 -0
- package/src/operator/node/SettingsView.tsx +690 -0
- package/src/operator/node/TaskBoard.tsx +352 -0
- package/src/operator/node/TaskChanges.tsx +526 -0
- package/src/operator/node/TaskHero.tsx +50 -0
- package/src/operator/node/TasksColumn.tsx +200 -0
- package/src/operator/node/Terminal.tsx +143 -0
- package/src/operator/node/Transcript.tsx +485 -0
- package/src/operator/node/ViewFile.tsx +79 -0
- package/src/operator/node/Welcome.tsx +101 -0
- package/src/operator/node/WorktreePrune.tsx +333 -0
- package/src/operator/node/agents.ts +58 -0
- package/src/operator/node/api.ts +57 -0
- package/src/operator/node/boardBackend.ts +63 -0
- package/src/operator/node/boardEvents.ts +40 -0
- package/src/operator/node/boardInteraction.ts +15 -0
- package/src/operator/node/boardState.ts +74 -0
- package/src/operator/node/boardWorkspace.ts +15 -0
- package/src/operator/node/boundedPoll.ts +47 -0
- package/src/operator/node/branchFlow.ts +1 -0
- package/src/operator/node/composerDraft.ts +78 -0
- package/src/operator/node/fileLinks.ts +81 -0
- package/src/operator/node/format.ts +292 -0
- package/src/operator/node/github.tsx +232 -0
- package/src/operator/node/icons.tsx +88 -0
- package/src/operator/node/liveStream.ts +323 -0
- package/src/operator/node/modals.tsx +766 -0
- package/src/operator/node/navHistory.ts +70 -0
- package/src/operator/node/persist.ts +26 -0
- package/src/operator/node/protocol/agents/optIn.ts +9 -0
- package/src/operator/node/protocol/agents/types.ts +394 -0
- package/src/operator/node/protocol/approvalFailure.ts +77 -0
- package/src/operator/node/protocol/authFailure.ts +66 -0
- package/src/operator/node/protocol/customModels.ts +23 -0
- package/src/operator/node/protocol/events.ts +13 -0
- package/src/operator/node/protocol/features.ts +70 -0
- package/src/operator/node/protocol/internalUsage.ts +1 -0
- package/src/operator/node/protocol/promptLimits.ts +52 -0
- package/src/operator/node/protocol/storageRetention.ts +13 -0
- package/src/operator/node/protocol/types.ts +335 -0
- package/src/operator/node/protocol/usageLimit.ts +72 -0
- package/src/operator/node/shared.tsx +89 -0
- package/src/operator/node/transcriptModel.ts +129 -0
- package/src/operator/node/types.ts +305 -0
- package/src/operator/node/useGlobalEvents.ts +97 -0
- package/src/operator/node/useOrchestrator.ts +697 -0
- package/src/operator/node/usePrefs.ts +114 -0
- package/src/operator/node/useRecaps.ts +102 -0
- package/src/operator/node/useTaskStream.ts +286 -0
- package/src/operator.base.css +1952 -0
- package/src/operator.css +1952 -0
- package/src/styles.css +579 -0
- package/src/ui/ErrorBoundary.tsx +38 -0
- package/src/ui/Markdown.tsx +60 -0
- package/src/ui/Popover.tsx +95 -0
- package/src/ui/QueuedMessages.tsx +110 -0
- package/src/ui/ReferenceCard.tsx +28 -0
- package/src/ui/SubagentChat.tsx +233 -0
- package/src/ui/TextBlock.tsx +51 -0
- package/src/ui/Transcript.tsx +435 -0
- package/src/ui/UploadProgress.tsx +46 -0
- package/src/ui/WorkspaceDrawer.tsx +64 -0
- package/src/ui/WorkspaceTabs.tsx +60 -0
- package/src/ui/fileLinks.ts +52 -0
- package/src/ui/format.ts +103 -0
- package/src/ui/icons.tsx +92 -0
- package/src/ui/reference.ts +40 -0
- package/src/ui/shared.tsx +38 -0
- package/src/ui/transcriptModel.ts +151 -0
- package/src/ui/useChatScrollbar.ts +29 -0
- package/src/ui/useComposerAutosize.ts +54 -0
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
// cli-msglist:消息对话列表。界面取自 Operator 的任务栏(TasksColumn 的列表视图,不做看板),
|
|
2
|
+
// 一张卡片是 center 里的一个对话任务(mode=conversation)。对话任务里开了几个对话时,卡片下面按对话展开,
|
|
3
|
+
// 点哪个对话选哪个(选中值是对话键 "job#task");卡片上的「+」在这个对话任务里新开一个对话(同一个工作区)。
|
|
4
|
+
import { useEffect, useId, useLayoutEffect, useMemo, useRef, useState, type CSSProperties } from "react";
|
|
5
|
+
import { CliErrorBoundary } from "../ui/ErrorBoundary";
|
|
6
|
+
import type { CliEvent, JobEntry } from "../core/store";
|
|
7
|
+
import type { CliStoreOptions, CliStore } from "../core/store";
|
|
8
|
+
import { useCliState, useCliStore, useCliStoreContext, useComponentEvents, withOwnProvider, type ResultHandlers } from "../core/provider";
|
|
9
|
+
import { convKey, NEW_TASK, parseConvKey, TERMINAL_JOB } from "../core/conversation";
|
|
10
|
+
import type { ConversationInfo } from "../core/types";
|
|
11
|
+
import { Icon } from "../ui/icons";
|
|
12
|
+
import { agentLabel, relTime, toMs } from "../ui/format";
|
|
13
|
+
import { AgentBadge, ErrNote, Skel, StatusDot, type DotCls } from "../ui/shared";
|
|
14
|
+
import { resolveMsgListConfig, type CliMsgListConfig, type ResolvedMsgListConfig } from "../core/config";
|
|
15
|
+
import { useBoardVisibility } from "../board/useBoardVisibility";
|
|
16
|
+
|
|
17
|
+
/** onTurnEnd / onConversationEnd:浏览器里从实时流算出的结果通知,不依赖 center 的 callback.url。 */
|
|
18
|
+
export interface CliMsgListProps extends CliStoreOptions, ResultHandlers {
|
|
19
|
+
/**
|
|
20
|
+
* 列表显示哪些对话(center 的 job_id),由宿主按自己的用户 / 业务给。
|
|
21
|
+
* 不给就是空列表(只显示本页新建的对话):组件不会去 center 列出这个产品线令牌下的全部对话。
|
|
22
|
+
*/
|
|
23
|
+
jobIds?: string[];
|
|
24
|
+
/** 受控的选中项(对话键:job_id,或 "job_id#task_id");不给就用共享仓库里的选中(和 cli-chat 联动)。 */
|
|
25
|
+
selectedId?: string | null;
|
|
26
|
+
/**
|
|
27
|
+
* 选了一个对话。id 是对话键:只有一个对话的对话任务是 job_id;对话任务里的某个对话是 "job_id#task_id";
|
|
28
|
+
* 点了「+」是 "job_id#new"(在这个对话任务里新开对话)。ref 是拆开的 jobId / taskId(新开时 taskId 为 "new")。
|
|
29
|
+
*/
|
|
30
|
+
onSelect?: (id: string, ref: { jobId: string; taskId: string | null }) => void;
|
|
31
|
+
/** 点「新对话」:缺省清空选中,cli-chat 会显示空白的新对话。 */
|
|
32
|
+
onNew?: () => void;
|
|
33
|
+
/** 顶部名称与色块(如业务 / 项目名)。 */
|
|
34
|
+
title?: string;
|
|
35
|
+
color?: string;
|
|
36
|
+
/**
|
|
37
|
+
* 顶部的项目说明(只显示)。要让智能体读到,创建时用 newConversation.workspace.context,之后用 store.appendContext;
|
|
38
|
+
* onEditContext 给了就可以点开(宿主自己做编辑框)。
|
|
39
|
+
*/
|
|
40
|
+
context?: string;
|
|
41
|
+
onEditContext?: () => void;
|
|
42
|
+
onCollapse?: () => void;
|
|
43
|
+
/**
|
|
44
|
+
* @deprecated 0.6.14 起列表不再定时轮询,此参数仅保留兼容、不再安排定时请求。
|
|
45
|
+
* 打开着的对话由实时流驱动;其余未结束的对话在页面回到前台、窗口重新获得焦点、网络恢复时重新拉取,也可用 store.refreshAll()。
|
|
46
|
+
*/
|
|
47
|
+
pollMs?: number;
|
|
48
|
+
theme?: "light" | "dark";
|
|
49
|
+
className?: string;
|
|
50
|
+
style?: CSSProperties;
|
|
51
|
+
width?: number | string;
|
|
52
|
+
/** 界面配置:顶部栏、搜索、卡片内容、分组标题与空状态文案,见 core/config.ts。 */
|
|
53
|
+
config?: CliMsgListConfig;
|
|
54
|
+
onEvent?: (event: CliEvent) => void;
|
|
55
|
+
|
|
56
|
+
store?: CliStore;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function CliMsgList(props: CliMsgListProps) {
|
|
60
|
+
const ctx = useCliStoreContext();
|
|
61
|
+
return <CliErrorBoundary component="cli-msglist" theme={props.theme}>{withOwnProvider(props, <MsgListInner {...props} inProvider={!!ctx} />, !!ctx)}</CliErrorBoundary>;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const SEARCH_MIN = 6;
|
|
65
|
+
const BATCH_ACTIVE = new Set(["dispatching", "running", "verifying"]);
|
|
66
|
+
// An orphaned task can retain a fresh, authoritative human-wait observation
|
|
67
|
+
// while the center is reconciling its execution state. Do not call it running.
|
|
68
|
+
const BATCH_AWAITING = new Set([...BATCH_ACTIVE, "orphaned"]);
|
|
69
|
+
|
|
70
|
+
interface CardView {
|
|
71
|
+
id: string;
|
|
72
|
+
title: string;
|
|
73
|
+
desc: string;
|
|
74
|
+
cls: DotCls;
|
|
75
|
+
label: string;
|
|
76
|
+
running: boolean;
|
|
77
|
+
awaiting: boolean;
|
|
78
|
+
group: "needs" | "active" | "done" | "cancelled";
|
|
79
|
+
/** 带状态的活动(打开 card.status 时显示)。 */
|
|
80
|
+
activity: string;
|
|
81
|
+
/** 只有时间(缺省显示这个)。 */
|
|
82
|
+
when: string;
|
|
83
|
+
agent: string | null;
|
|
84
|
+
turns: number;
|
|
85
|
+
updatedAt: number;
|
|
86
|
+
/** 一次性任务(含续派,宿主把它的 job_id 放进了列表):只能查看,不能在里面新开对话。 */
|
|
87
|
+
batch: boolean;
|
|
88
|
+
loading: boolean;
|
|
89
|
+
error: string | null;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function cardView(id: string, entry: JobEntry | undefined): CardView {
|
|
93
|
+
const job = entry?.job;
|
|
94
|
+
const conv = job?.conversation;
|
|
95
|
+
const updatedAt = toMs(conv?.last_activity_at) ?? 0;
|
|
96
|
+
const batch = !!job && job.mode !== "conversation";
|
|
97
|
+
const base = { id, title: job?.title ?? id, desc: entry?.firstInput ?? "", when: updatedAt ? relTime(updatedAt) : "", agent: job?.tasks?.[0]?.agent ?? conv?.last_turn?.agent ?? null, turns: conv?.turns ?? 0, updatedAt, batch, loading: !job && !entry?.error, error: entry?.error ?? null };
|
|
98
|
+
const status = job?.status ?? "queued";
|
|
99
|
+
if (TERMINAL_JOB.has(status)) {
|
|
100
|
+
const cancelled = status === "cancelled";
|
|
101
|
+
const failed = status === "failed";
|
|
102
|
+
const label = cancelled ? "已取消" : failed ? "已失败" : "已完成";
|
|
103
|
+
return { ...base, cls: cancelled || failed ? "x" : "g", label, running: false, awaiting: false, group: cancelled ? "cancelled" : "done", activity: updatedAt ? `${label} · ${relTime(updatedAt)}` : label };
|
|
104
|
+
}
|
|
105
|
+
// A browser handoff/input card keeps its turn running while requiring a human.
|
|
106
|
+
if (batch) {
|
|
107
|
+
const running = (job?.tasks ?? []).some((task) => BATCH_ACTIVE.has(task.status));
|
|
108
|
+
const awaiting = (job?.tasks ?? []).some((task) => BATCH_AWAITING.has(task.status) && task.awaiting_input === true);
|
|
109
|
+
if (awaiting) return { ...base, cls: "a", label: "需要你处理", running, awaiting, group: "needs", activity: "等你处理" };
|
|
110
|
+
return { ...base, cls: "a", label: "进行中", running, awaiting: false, group: "active", activity: running ? "运行中 · 工作中" : status === "queued" ? "等待执行节点" : "进行中" };
|
|
111
|
+
}
|
|
112
|
+
// 开了几个对话:任何一个在跑就算在跑,任何一个停在提问上就要你处理(关掉的对话不算)。
|
|
113
|
+
const open = (conv?.conversations ?? []).filter((c) => !c.closed);
|
|
114
|
+
const many = (conv?.conversations?.length ?? 0) > 1;
|
|
115
|
+
const running = many ? open.some((c) => c.running_turn) : !!conv?.running_turn;
|
|
116
|
+
// 上一轮以提问结束、后面没有新消息:等你回答。
|
|
117
|
+
const awaiting = many ? open.some((c) => c.awaiting_input === true || (!c.running_turn && !c.queued_turns && !!c.last_turn?.asked)) : conv?.awaiting_input === true || (!running && !conv?.queued_turns && !!conv?.last_turn?.asked);
|
|
118
|
+
if (awaiting) return { ...base, cls: "a", label: "需要你处理", running, awaiting, group: "needs", activity: updatedAt ? `等你处理 · ${relTime(updatedAt)}` : "等你处理" };
|
|
119
|
+
if (running) return { ...base, cls: "a", label: "进行中", running, awaiting, group: "active", activity: "运行中 · 工作中" };
|
|
120
|
+
const waiting = conv?.queued_turns ? `${conv.queued_turns} 条排队中` : status === "queued" ? "等待执行节点" : updatedAt ? relTime(updatedAt) : "未开始";
|
|
121
|
+
return { ...base, cls: "a", label: "进行中", running, awaiting, group: "active", activity: waiting };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** 对话任务里的一个对话(对话任务开了几个对话时,卡片下面的一行)。 */
|
|
125
|
+
interface ConvRowView {
|
|
126
|
+
key: string;
|
|
127
|
+
title: string;
|
|
128
|
+
cls: DotCls;
|
|
129
|
+
label: string;
|
|
130
|
+
running: boolean;
|
|
131
|
+
awaiting: boolean;
|
|
132
|
+
activity: string;
|
|
133
|
+
turns: number;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** 一次性任务里子任务的状态 → 行的样子。 */
|
|
137
|
+
const BATCH_ROW: Record<string, { cls: DotCls; label: string; running?: boolean }> = {
|
|
138
|
+
blocked: { cls: "r", label: "等待前置" }, queued: { cls: "r", label: "排队中" },
|
|
139
|
+
dispatching: { cls: "a", label: "运行中", running: true }, running: { cls: "a", label: "运行中", running: true }, verifying: { cls: "a", label: "核对中", running: true },
|
|
140
|
+
orphaned: { cls: "a", label: "等待节点" }, stalled: { cls: "a", label: "等待节点" }, paused: { cls: "r", label: "已暂停" },
|
|
141
|
+
succeeded: { cls: "g", label: "已完成" }, failed: { cls: "x", label: "失败" }, cancelled: { cls: "x", label: "已取消" },
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* 卡片下面的行:对话任务里的对话(只有一个时返回 [],整张卡片就是它);
|
|
146
|
+
* 一次性任务(宿主把它的 job_id 放进了列表)有几个子任务时,每个子任务一行——点哪行,对话和看板就看那个子任务。
|
|
147
|
+
*/
|
|
148
|
+
export function convRows(jobId: string, entry: JobEntry | undefined): ConvRowView[] {
|
|
149
|
+
const job = entry?.job;
|
|
150
|
+
if (job && job.mode !== "conversation") {
|
|
151
|
+
const tasks = [...(job.tasks ?? [])].sort((a, b) => (a.seq ?? 0) - (b.seq ?? 0));
|
|
152
|
+
if (tasks.length <= 1) return [];
|
|
153
|
+
return tasks.map((t) => {
|
|
154
|
+
const look = BATCH_ROW[t.status] ?? { cls: "a" as DotCls, label: t.status };
|
|
155
|
+
if (BATCH_AWAITING.has(t.status) && t.awaiting_input === true) return { key: convKey(jobId, t.id), title: t.title, cls: "a" as DotCls, label: "需要你处理", running: !!look.running, awaiting: true, activity: "等你处理", turns: 0 };
|
|
156
|
+
return { key: convKey(jobId, t.id), title: t.title, cls: look.cls, label: look.label, running: !!look.running, awaiting: false, activity: look.label, turns: 0 };
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
const list = entry?.job?.conversation?.conversations ?? [];
|
|
160
|
+
if (list.length <= 1) return [];
|
|
161
|
+
const jobEnded = TERMINAL_JOB.has(entry?.job?.status ?? "");
|
|
162
|
+
return list.map((c: ConversationInfo) => {
|
|
163
|
+
const base = { key: convKey(jobId, c.task_id), title: c.title, turns: c.turns };
|
|
164
|
+
if (c.closed || jobEnded) return { ...base, cls: "g" as DotCls, label: "已结束", running: false, awaiting: false, activity: "已结束" };
|
|
165
|
+
const running = c.running_turn;
|
|
166
|
+
const awaiting = c.awaiting_input === true || (!running && !c.queued_turns && !!c.last_turn?.asked);
|
|
167
|
+
if (awaiting) return { ...base, cls: "a" as DotCls, label: "需要你处理", running, awaiting, activity: "等你处理" };
|
|
168
|
+
if (running) return { ...base, cls: "a" as DotCls, label: "进行中", running, awaiting, activity: "运行中" };
|
|
169
|
+
return { ...base, cls: "a" as DotCls, label: "进行中", running, awaiting, activity: c.queued_turns ? `${c.queued_turns} 条排队中` : "空闲" };
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function MsgCard({ v, selected, onSelect, show, onNewInJob }: { v: CardView; selected: boolean; onSelect: () => void; show: ResolvedMsgListConfig["card"]; onNewInJob?: () => void }) {
|
|
174
|
+
if (v.loading) return <CardSkeleton i={v.id.length} />;
|
|
175
|
+
const activity = show.status ? v.activity : v.when;
|
|
176
|
+
const foot = (show.activity && !!activity) || (show.turns && v.turns > 0);
|
|
177
|
+
const card = (
|
|
178
|
+
<button className={`task ${selected ? "sel" : ""} ${v.awaiting && show.status ? "awaiting" : ""}`} onClick={onSelect} title={v.error ?? undefined}>
|
|
179
|
+
<div className="task-top">
|
|
180
|
+
{show.status && <StatusDot cls={v.cls} running={v.running} awaiting={v.awaiting} title={v.label} />}
|
|
181
|
+
<span className="ttitle">{v.title}</span>
|
|
182
|
+
{show.status && <span className={`slabel ${v.awaiting ? "await" : ""}`}>{v.label}</span>}
|
|
183
|
+
{show.agent && v.agent && <AgentBadge label={agentLabel(v.agent)} />}
|
|
184
|
+
</div>
|
|
185
|
+
{show.description && v.desc && <div className="tdesc">{v.desc}</div>}
|
|
186
|
+
{v.error && <div className="blocked-chip">{Icon.x()} {v.error}</div>}
|
|
187
|
+
{foot && <div className="task-foot">
|
|
188
|
+
{show.activity && activity && <span className="activity">{show.status && (v.awaiting ? <span style={{ color: "var(--blue)" }}>●</span> : v.running ? <span style={{ color: "var(--amber)" }}>●</span> : null)}{activity}</span>}
|
|
189
|
+
<span className="spacer" />
|
|
190
|
+
{show.turns && v.turns > 0 && <span className="activity">{v.turns} 条消息</span>}
|
|
191
|
+
</div>}
|
|
192
|
+
</button>
|
|
193
|
+
);
|
|
194
|
+
if (!onNewInJob) return card;
|
|
195
|
+
// 「+」不能放进卡片按钮里(按钮不能嵌套):叠在卡片右下角。
|
|
196
|
+
return (
|
|
197
|
+
<div className="cli-job-card">
|
|
198
|
+
{card}
|
|
199
|
+
<button className="icon-btn cli-new-in-job" onClick={onNewInJob} title="在这个任务里新开一个对话(共用同一个工作区)" aria-label="在这个任务里新开一个对话">{Icon.plus()}</button>
|
|
200
|
+
</div>
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function ConvRow({ v, selected, following, onSelect, show }: { v: ConvRowView; selected: boolean; following?: boolean; onSelect: () => void; show: ResolvedMsgListConfig["card"] }) {
|
|
205
|
+
return (
|
|
206
|
+
<button className={`cli-conv-row ${selected ? "sel" : ""} ${following ? "following" : ""} ${v.awaiting && show.status ? "awaiting" : ""}`} onClick={onSelect}
|
|
207
|
+
title={following ? "对话和看板正在看这个子任务(跟着最近开始执行的子任务)" : undefined}>
|
|
208
|
+
{show.status && <StatusDot cls={v.cls} running={v.running} awaiting={v.awaiting} title={v.label} />}
|
|
209
|
+
<span className="cli-conv-title">{v.title}</span>
|
|
210
|
+
{show.activity && show.status && <span className="cli-conv-act">{v.activity}</span>}
|
|
211
|
+
{show.turns && v.turns > 0 && <span className="cli-conv-act">{v.turns} 条</span>}
|
|
212
|
+
</button>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function CardSkeleton({ i = 0 }: { i?: number }) {
|
|
217
|
+
const widths = [58, 42, 66, 38];
|
|
218
|
+
return (
|
|
219
|
+
<div className="task" style={{ cursor: "default" }} aria-hidden>
|
|
220
|
+
<div className="task-top"><Skel w={9} h={9} r="50%" /><Skel w={`${widths[i % widths.length]}%`} h={12} /><span style={{ flex: 1 }} /><Skel w={34} h={10} /></div>
|
|
221
|
+
<div className="task-foot"><Skel w={120} h={9} /></div>
|
|
222
|
+
</div>
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
type GroupProps = {
|
|
227
|
+
label: string | false; cards: CardView[]; selected: string | null; onSelect: (id: string) => void; show: ResolvedMsgListConfig["card"];
|
|
228
|
+
rows: Record<string, ConvRowView[]>; firstTask: Record<string, string | undefined>; onNewInJob?: (jobId: string) => void;
|
|
229
|
+
/** 选中一次性任务(不指定子任务)时,对话和看板实际在看的子任务:那一行标出来,三个组件看的是哪个一眼对得上。 */
|
|
230
|
+
following?: string | null;
|
|
231
|
+
accent?: boolean; collapsible?: boolean; collapsed?: boolean; onToggle?: () => void;
|
|
232
|
+
/** 不分组:只有卡片,没有组标题。 */
|
|
233
|
+
plain?: boolean;
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
function Group({ label, cards, selected, onSelect, show, rows, firstTask, onNewInJob, accent, collapsible, collapsed, onToggle, plain, following }: GroupProps) {
|
|
237
|
+
if (!cards.length || label === false) return null;
|
|
238
|
+
const sel = selected ? parseConvKey(selected) : null;
|
|
239
|
+
const list = cards.map((v) => {
|
|
240
|
+
const convs = rows[v.id] ?? [];
|
|
241
|
+
// 选中的是这个对话任务:只有 job_id 指它的第一个对话。
|
|
242
|
+
// 一次性任务:只有 job_id = 跟着最近开始执行的子任务(卡片本身选中),不落在哪一行上。
|
|
243
|
+
const selectedKey = sel?.jobId === v.id ? (v.batch ? (sel.taskId ? convKey(v.id, sel.taskId) : null) : convKey(v.id, sel.taskId ?? firstTask[v.id])) : null;
|
|
244
|
+
const newing = sel?.jobId === v.id && sel.taskId === NEW_TASK;
|
|
245
|
+
const card = (
|
|
246
|
+
<MsgCard
|
|
247
|
+
key={v.id} v={v} show={show}
|
|
248
|
+
// 只有一个对话:整张卡片就是它;有几个对话:卡片是标题,选中落在下面的行上。
|
|
249
|
+
selected={(!convs.length || v.batch) && !newing && sel?.jobId === v.id && (!v.batch || !sel?.taskId)}
|
|
250
|
+
onSelect={() => onSelect(convs.length && !v.batch ? convs.find((c) => c.key === selectedKey)?.key ?? convs[0].key : v.id)}
|
|
251
|
+
onNewInJob={onNewInJob && !v.batch && v.group !== "done" && v.group !== "cancelled" ? () => onNewInJob(v.id) : undefined}
|
|
252
|
+
/>
|
|
253
|
+
);
|
|
254
|
+
if (!convs.length && !newing) return card;
|
|
255
|
+
return (
|
|
256
|
+
<div key={v.id} className="cli-job-group">
|
|
257
|
+
{card}
|
|
258
|
+
<div className="cli-conv-rows">
|
|
259
|
+
{convs.map((c) => <ConvRow key={c.key} v={c} selected={c.key === selectedKey} following={!selectedKey && v.batch && sel?.jobId === v.id && c.key === (following ? convKey(v.id, following) : null)} onSelect={() => onSelect(c.key)} show={show} />)}
|
|
260
|
+
{newing && <div className="cli-conv-row sel"><span className="cli-conv-title">新对话 · 发第一条消息后开始</span></div>}
|
|
261
|
+
</div>
|
|
262
|
+
</div>
|
|
263
|
+
);
|
|
264
|
+
});
|
|
265
|
+
if (plain) return <>{list}</>;
|
|
266
|
+
if (collapsible) {
|
|
267
|
+
return (
|
|
268
|
+
<>
|
|
269
|
+
<button className={`task-group-h tgh-btn ${collapsed ? "is-collapsed" : ""}`} onClick={onToggle} title={`${collapsed ? "展开" : "折叠"}「${label}」`}>
|
|
270
|
+
{Icon.chevDown({ className: "tgh-chev" })}
|
|
271
|
+
{label} <span className="gcount">{cards.length}</span><span className="gline" />
|
|
272
|
+
</button>
|
|
273
|
+
{!collapsed && list}
|
|
274
|
+
</>
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
return (
|
|
278
|
+
<>
|
|
279
|
+
<div className={`task-group-h ${accent ? "needs-you" : ""}`}>{label} <span className="gcount">{cards.length}</span><span className="gline" /></div>
|
|
280
|
+
{list}
|
|
281
|
+
</>
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** 回到前台 / 获得焦点 / 网络恢复时重新拉列表,相邻这么久以内只拉一次。 */
|
|
286
|
+
const LIST_REFRESH_GAP_MS = 5_000;
|
|
287
|
+
|
|
288
|
+
function MsgListInner({ jobIds, selectedId, onSelect, onNew, title = "对话", color = "#b8613f", context, onEditContext, onCollapse, config, theme = "light", className, style, width, onEvent, onTurnEnd, onConversationEnd, onCallback, inProvider }: CliMsgListProps & { inProvider: boolean }) {
|
|
289
|
+
useComponentEvents(onEvent, { onTurnEnd, onConversationEnd, onCallback }, inProvider);
|
|
290
|
+
const configKey = JSON.stringify(config ?? {});
|
|
291
|
+
const cfg = useMemo(() => resolveMsgListConfig(config), [configKey]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
292
|
+
const h = cfg.header;
|
|
293
|
+
const store = useCliStore();
|
|
294
|
+
const rootRef = useRef<HTMLDivElement>(null);
|
|
295
|
+
const owner = useId();
|
|
296
|
+
const visible = useBoardVisibility(rootRef, true);
|
|
297
|
+
const expanded = visible && !className?.split(/\s+/).includes("collapsed");
|
|
298
|
+
useLayoutEffect(() => { store.reportListPresence(owner, expanded); }, [store, owner, expanded]);
|
|
299
|
+
useLayoutEffect(() => () => { store.reportListPresence(owner, false); }, [store, owner]);
|
|
300
|
+
const ids = useCliState((s) => s.jobIds);
|
|
301
|
+
const jobs = useCliState((s) => s.jobs);
|
|
302
|
+
const shared = useCliState((s) => s.selected);
|
|
303
|
+
const listLoading = useCliState((s) => s.listLoading);
|
|
304
|
+
const listError = useCliState((s) => s.listError);
|
|
305
|
+
const selected = selectedId !== undefined ? selectedId : shared;
|
|
306
|
+
const following = useCliState((s) => (selected && !parseConvKey(selected).taskId ? s.convs[selected]?.task?.id ?? null : null));
|
|
307
|
+
const [query, setQuery] = useState("");
|
|
308
|
+
const [doneCollapsed, setDoneCollapsed] = useState(false);
|
|
309
|
+
const [cancelledCollapsed, setCancelledCollapsed] = useState(true);
|
|
310
|
+
const [discovered, setDiscovered] = useState(false);
|
|
311
|
+
|
|
312
|
+
const idsKey = jobIds?.join("\n");
|
|
313
|
+
useEffect(() => { if (jobIds) store.setJobIds(jobIds); }, [store, idsKey]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
314
|
+
// 宿主选中了列表里还没有的对话(例如旁边独立的 cli-chat 刚新建出来的):加进列表,不用两个组件共享任何东西。
|
|
315
|
+
// 放在 setJobIds 之后,宿主的 jobIds 更新后也会再补一次。
|
|
316
|
+
useEffect(() => {
|
|
317
|
+
const jobId = selected ? parseConvKey(selected).jobId : null;
|
|
318
|
+
if (jobId && !ids.includes(jobId)) store.addJob(jobId);
|
|
319
|
+
}, [selected, ids, store]);
|
|
320
|
+
|
|
321
|
+
// 首次:拉一遍列表里已有的(宿主给的 jobIds)。
|
|
322
|
+
useEffect(() => {
|
|
323
|
+
void store.refreshAll({ all: true }).finally(() => setDiscovered(true));
|
|
324
|
+
}, [store]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
325
|
+
|
|
326
|
+
// 不定时轮询:打开着的对话由实时流更新卡片(store 的 noticeTransitions);其余未结束的,
|
|
327
|
+
// 在页面回到前台、窗口重新获得焦点、网络恢复时重新拉一遍(相邻 5 秒内的合并为一次)。
|
|
328
|
+
useEffect(() => {
|
|
329
|
+
let last = Date.now();
|
|
330
|
+
const refresh = () => {
|
|
331
|
+
if (document.visibilityState === "hidden" || Date.now() - last < LIST_REFRESH_GAP_MS) return;
|
|
332
|
+
last = Date.now();
|
|
333
|
+
void store.refreshAll();
|
|
334
|
+
};
|
|
335
|
+
const onVisibility = () => { if (document.visibilityState === "visible") refresh(); };
|
|
336
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
337
|
+
window.addEventListener("focus", refresh);
|
|
338
|
+
window.addEventListener("online", refresh);
|
|
339
|
+
return () => {
|
|
340
|
+
document.removeEventListener("visibilitychange", onVisibility);
|
|
341
|
+
window.removeEventListener("focus", refresh);
|
|
342
|
+
window.removeEventListener("online", refresh);
|
|
343
|
+
};
|
|
344
|
+
}, [store]);
|
|
345
|
+
|
|
346
|
+
// 新加进来的对话(别的组件新建的 / 接续出来的)还没数据:补拉一次。
|
|
347
|
+
useEffect(() => { for (const id of ids) if (!jobs[id]) void store.refreshJob(id); }, [ids, jobs, store]);
|
|
348
|
+
|
|
349
|
+
const cards = useMemo(() => ids.map((id) => cardView(id, jobs[id])), [ids, jobs]);
|
|
350
|
+
const q = query.trim().toLowerCase();
|
|
351
|
+
const shown = q ? cards.filter((c) => c.title.toLowerCase().includes(q) || c.desc.toLowerCase().includes(q)
|
|
352
|
+
|| (jobs[c.id]?.job?.conversation?.conversations ?? []).some((conv) => conv.title.toLowerCase().includes(q))) : cards;
|
|
353
|
+
const byRecent = (a: CardView, b: CardView) => b.updatedAt - a.updatedAt;
|
|
354
|
+
const groups = {
|
|
355
|
+
needs: shown.filter((c) => c.group === "needs").sort(byRecent),
|
|
356
|
+
active: shown.filter((c) => c.group === "active").sort(byRecent),
|
|
357
|
+
done: shown.filter((c) => c.group === "done").sort(byRecent),
|
|
358
|
+
cancelled: shown.filter((c) => c.group === "cancelled").sort(byRecent),
|
|
359
|
+
};
|
|
360
|
+
const select = (id: string) => { if (selectedId === undefined) store.select(id); onSelect?.(id, parseConvKey(id)); };
|
|
361
|
+
const newInJob = cfg.card.newInJob ? (jobId: string) => select(convKey(jobId, NEW_TASK)) : undefined;
|
|
362
|
+
const rows = useMemo(() => Object.fromEntries(ids.map((id) => [id, convRows(id, jobs[id])])), [ids, jobs]);
|
|
363
|
+
const firstTask = useMemo(() => Object.fromEntries(ids.map((id) => [id, jobs[id]?.job?.conversation?.conversations?.[0]?.task_id ?? jobs[id]?.job?.tasks?.[0]?.id])), [ids, jobs]);
|
|
364
|
+
const groupProps = { selected, onSelect: select, show: cfg.card, rows, firstTask, onNewInJob: newInJob, following };
|
|
365
|
+
const startNew = () => { if (onNew) onNew(); else store.select(null); };
|
|
366
|
+
const firstLoad = !discovered && !cards.length;
|
|
367
|
+
const showRow = h.show && (h.title || h.newButton || h.refresh || !!onCollapse);
|
|
368
|
+
// 只显示:有文字、或者宿主给了编辑入口时才出现(发给智能体的项目背景见 newConversation.workspace.context 和 store.appendContext)。
|
|
369
|
+
const showContext = h.show && h.context && (!!context || !!onEditContext);
|
|
370
|
+
const showBanner = showRow || showContext;
|
|
371
|
+
|
|
372
|
+
return (
|
|
373
|
+
<div ref={rootRef} className={`cli-ui cli-msglist ${className ?? ""}`} data-theme={theme} style={{ ...style, ...(width !== undefined ? { width } : null) }}>
|
|
374
|
+
<div className="col col-tasks">
|
|
375
|
+
{showBanner && <div className="proj-banner">
|
|
376
|
+
{showRow && <div className="pb-row">
|
|
377
|
+
{h.title ? (
|
|
378
|
+
<span className="pb-home" style={{ cursor: "default" }}>
|
|
379
|
+
<span className="pb-pic" style={{ background: color }}>{title[0]}</span>
|
|
380
|
+
<span className="pb-name">{title}</span>
|
|
381
|
+
</span>
|
|
382
|
+
) : <span style={{ flex: 1 }} />}
|
|
383
|
+
{h.newButton && <button className="btn btn-line btn-sm" onClick={startNew}>{Icon.plus()} {h.newButtonText}</button>}
|
|
384
|
+
{h.refresh && <button className="icon-btn" onClick={() => void store.refreshAll({ all: true })} title="刷新列表" aria-label="刷新列表" disabled={listLoading}>{Icon.restore()}</button>}
|
|
385
|
+
{onCollapse && <button className="icon-btn" onClick={onCollapse} title="隐藏对话列表">{Icon.chevRight({ style: { transform: "rotate(180deg)" } })}</button>}
|
|
386
|
+
</div>}
|
|
387
|
+
{showContext && (
|
|
388
|
+
<button className="pb-ctx" onClick={onEditContext} style={onEditContext ? undefined : { cursor: "default" }}>
|
|
389
|
+
<div className={`ctx-txt ${context ? "" : "empty-ctx"}`}>{context || "添加项目背景 —— 简介、范围与规范"}</div>
|
|
390
|
+
{onEditContext && <div className="ctx-edit">{Icon.edit()} 上下文</div>}
|
|
391
|
+
</button>
|
|
392
|
+
)}
|
|
393
|
+
</div>}
|
|
394
|
+
{(cfg.search === true || (cfg.search === "auto" && cards.length >= SEARCH_MIN)) && (
|
|
395
|
+
<div className="search-bar">
|
|
396
|
+
<span className="search-ic">{Icon.search()}</span>
|
|
397
|
+
<input className="search-input" value={query} placeholder={cfg.searchPlaceholder} spellCheck={false} onChange={(e) => setQuery(e.target.value)} onKeyDown={(e) => { if (e.key === "Escape") setQuery(""); }} />
|
|
398
|
+
{query && <button className="search-clear" title="清除搜索" onClick={() => setQuery("")}>{Icon.x()}</button>}
|
|
399
|
+
</div>
|
|
400
|
+
)}
|
|
401
|
+
<div className="scroll">
|
|
402
|
+
<div className="task-scroll">
|
|
403
|
+
{listError && <ErrNote onRetry={() => void store.refreshAll({ all: true })}>{listError}</ErrNote>}
|
|
404
|
+
{firstLoad ? [0, 1, 2].map((i) => <CardSkeleton key={i} i={i} />) : (
|
|
405
|
+
<>
|
|
406
|
+
{cards.length === 0 && <div className="empty" style={{ padding: "30px 16px" }}><div className="e-t">{cfg.emptyTitle}</div>{cfg.emptyDescription && <div className="e-s">{cfg.emptyDescription}</div>}</div>}
|
|
407
|
+
{q && shown.length === 0 && <div className="search-empty">没有匹配“{query.trim()}”的对话。</div>}
|
|
408
|
+
{!cfg.grouped && <Group label="" cards={[...shown].sort(byRecent)} {...groupProps} plain />}
|
|
409
|
+
{cfg.grouped && <>
|
|
410
|
+
<Group label={cfg.groups.needs} cards={groups.needs} {...groupProps} accent />
|
|
411
|
+
<Group label={cfg.groups.active} cards={groups.active} {...groupProps} />
|
|
412
|
+
<Group label={cfg.groups.done} cards={groups.done} {...groupProps} collapsible collapsed={doneCollapsed && !q} onToggle={() => setDoneCollapsed((c) => !c)} />
|
|
413
|
+
<Group label={cfg.groups.cancelled} cards={groups.cancelled} {...groupProps} collapsible collapsed={cancelledCollapsed && !q} onToggle={() => setCancelledCollapsed((c) => !c)} />
|
|
414
|
+
</>}
|
|
415
|
+
</>
|
|
416
|
+
)}
|
|
417
|
+
</div>
|
|
418
|
+
</div>
|
|
419
|
+
</div>
|
|
420
|
+
</div>
|
|
421
|
+
);
|
|
422
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Msg } from "../core/types";
|
|
2
|
+
export interface BranchTask { id: string; project_id: string; title: string; agent: string; status: string; created_at?: number; updated_at?: number; parent_task_id?: string | null; fork_group_id?: string | null; fork_commit?: string | null; fork_source_title?: string | null; fork_context?: string | null; work_branch?: string; running?: number }
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
export type BranchStep<T extends BranchTask = BranchTask> = { source: T | null; child: T };
|
|
6
|
+
|
|
7
|
+
/** The selected path, oldest source first. Missing/deleted sources keep a boundary. */
|
|
8
|
+
export function branchAncestry<T extends BranchTask>(task: T, tasks: T[]): BranchStep<T>[] {
|
|
9
|
+
const byId = new Map(tasks.filter((t) => t.project_id === task.project_id).map((t) => [t.id, t]));
|
|
10
|
+
const visited = new Set([task.id]);
|
|
11
|
+
const steps: BranchStep<T>[] = [];
|
|
12
|
+
let child = task;
|
|
13
|
+
while (child.parent_task_id && !visited.has(child.parent_task_id)) {
|
|
14
|
+
visited.add(child.parent_task_id);
|
|
15
|
+
const source = byId.get(child.parent_task_id) ?? null;
|
|
16
|
+
steps.unshift({ source, child });
|
|
17
|
+
if (!source) break;
|
|
18
|
+
child = source;
|
|
19
|
+
}
|
|
20
|
+
return steps;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Outgoing paths only; a source is never presented as its own child branch. */
|
|
24
|
+
export function branchOptions<T extends BranchTask>(sourceId: string, projectId: string, tasks: T[], selected?: T): T[] {
|
|
25
|
+
const local = tasks.filter((t) => t.project_id === projectId);
|
|
26
|
+
const direct = local.filter((t) => t.parent_task_id === sourceId);
|
|
27
|
+
const groups = new Set([...direct, ...(selected ? [selected] : [])].map((t) => t.fork_group_id).filter(Boolean));
|
|
28
|
+
return local.filter((t) => t.id !== sourceId && (t.parent_task_id === sourceId || !!t.fork_group_id && groups.has(t.fork_group_id)));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// A parent may have been resumed since the fork. Its later messages belong to
|
|
32
|
+
// that parent, not above the historical point where this child branched off.
|
|
33
|
+
export function messagesAtFork(messages: Msg[], child: BranchTask): Msg[] {
|
|
34
|
+
return messages.filter((m) => m.role !== "queued" && (!child.created_at || !m.ts || m.ts <= child.created_at));
|
|
35
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { default as OperatorWorkspace } from "./node/Orchestrator";
|
|
2
|
+
export type { TaskRow, ProjectRow, AgentsBundle } from "./node/types";
|
|
3
|
+
|
|
4
|
+
export { default as OperatorFilePage } from "./node/ViewFile";
|
|
5
|
+
export { default as OperatorBrowserPage } from "./node/BrowserPopup";
|
|
6
|
+
|
|
7
|
+
export type { OperatorBoardOptions, OperatorBoardTab } from "./node/boardInteraction";
|
|
8
|
+
|
|
9
|
+
export { createOperatorWorkspace } from "./node/boardWorkspace";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./node/liveStream";
|