@spzxg/cli-ui 0.6.2 → 0.6.6
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 +49 -0
- package/README.md +165 -19
- package/dist/cli-ui-embed-BAqonuoD.js +45952 -0
- package/dist/{cli-ui-index-BY5i3wId.js → cli-ui-index-b8wF7wrN.js} +2 -2
- package/dist/{cli-ui-jszip.min-m01fexHW.js → cli-ui-jszip.min-B0uLrCHg.js} +2 -2
- package/dist/{cli-ui-jszip.min-BZsUwI-s.js → cli-ui-jszip.min-ggP8OokP.js} +1 -1
- package/dist/{cli-ui-xlsx-CpWicJLT.js → cli-ui-xlsx-DEDdHrJJ.js} +1 -1
- package/dist/cli-ui.css +32 -1
- package/dist/cli-ui.js +1 -1
- package/package.json +17 -10
- package/src/board/BrowserPane.tsx +346 -115
- package/src/board/ChangesPane.tsx +2 -2
- package/src/board/CliBoard.tsx +79 -50
- package/src/board/FileViewer.tsx +54 -31
- package/src/board/FilesPane.tsx +32 -15
- package/src/board/PanelControls.tsx +28 -0
- package/src/board/blocks.tsx +5 -5
- package/src/board/context.ts +65 -0
- package/src/board/fileOps.ts +26 -3
- package/src/board/fileWatch.ts +94 -0
- package/src/board/folderDownload.ts +30 -14
- package/src/board/frameQuote.ts +2 -2
- package/src/board/panels.ts +33 -0
- package/src/board/textLoad.ts +9 -7
- package/src/board/workspace.ts +53 -14
- package/src/chat/CliChat.tsx +93 -40
- package/src/chat/Composer.tsx +44 -42
- package/src/chat/RunControls.tsx +99 -0
- package/src/chat/configuration.ts +15 -0
- package/src/chat/draft.ts +41 -0
- package/src/chat/export.ts +2 -2
- package/src/components.tsx +13 -0
- package/src/core/blocks.ts +17 -6
- package/src/core/callbacks.ts +8 -0
- package/src/core/client.ts +103 -19
- package/src/core/conversation.ts +4 -3
- package/src/core/id.ts +15 -0
- package/src/core/identity.ts +33 -0
- package/src/core/provider.tsx +31 -14
- package/src/core/store.ts +151 -53
- package/src/core/types.ts +64 -9
- package/src/elements.tsx +135 -29
- package/src/embed.ts +6 -8
- package/src/index.ts +24 -17
- package/src/layout/CliSplit.tsx +1 -1
- package/src/layout/split.ts +67 -30
- package/src/msglist/CliMsgList.tsx +19 -13
- 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 +624 -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 +517 -0
- package/src/operator/node/CommandPalette.tsx +213 -0
- package/src/operator/node/Composer.tsx +287 -0
- package/src/operator/node/InsightsView.tsx +831 -0
- package/src/operator/node/Layout.tsx +190 -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 +250 -0
- package/src/operator/node/Orchestrator.tsx +634 -0
- package/src/operator/node/ProjectLanding.tsx +86 -0
- package/src/operator/node/ProjectsColumn.tsx +137 -0
- package/src/operator/node/ProviderAuth.tsx +561 -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 +260 -0
- package/src/operator/node/SessionView.tsx +288 -0
- package/src/operator/node/SettingsView.tsx +690 -0
- package/src/operator/node/TaskBoard.tsx +352 -0
- package/src/operator/node/TaskChanges.tsx +530 -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 +468 -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 +40 -0
- package/src/operator/node/boardBackend.ts +82 -0
- package/src/operator/node/boardEvents.ts +22 -0
- package/src/operator/node/boardInteraction.ts +15 -0
- package/src/operator/node/boardState.ts +57 -0
- package/src/operator/node/boardWorkspace.ts +12 -0
- package/src/operator/node/branchFlow.ts +1 -0
- package/src/operator/node/composerDraft.ts +52 -0
- package/src/operator/node/fileLinks.ts +81 -0
- package/src/operator/node/format.ts +290 -0
- package/src/operator/node/github.tsx +234 -0
- package/src/operator/node/icons.tsx +88 -0
- package/src/operator/node/liveStream.ts +270 -0
- package/src/operator/node/modals.tsx +720 -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 +332 -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 +296 -0
- package/src/operator/node/useGlobalEvents.ts +97 -0
- package/src/operator/node/useOrchestrator.ts +696 -0
- package/src/operator/node/usePrefs.ts +114 -0
- package/src/operator/node/useRecaps.ts +91 -0
- package/src/operator/node/useTaskStream.ts +280 -0
- package/src/operator.base.css +1806 -0
- package/src/operator.css +194 -16
- package/src/styles.css +49 -1
- package/src/ui/Markdown.tsx +7 -2
- package/src/ui/Popover.tsx +82 -0
- package/src/ui/TextBlock.tsx +3 -3
- package/src/ui/Transcript.tsx +17 -10
- package/src/ui/fileLinks.ts +6 -0
- package/src/ui/format.ts +1 -1
- package/src/ui/shared.tsx +1 -1
- package/src/ui/transcriptModel.ts +2 -2
- package/dist/cli-ui-embed-6IJ_jRVh.js +0 -41223
package/src/elements.tsx
CHANGED
|
@@ -28,34 +28,111 @@
|
|
|
28
28
|
* 渲染在 light DOM 里(不用 Shadow DOM),样式靠页面引入 styles.css,作用域都在 .cli-ui 内。
|
|
29
29
|
*/
|
|
30
30
|
import { createRoot, type Root } from "react-dom/client";
|
|
31
|
-
import { CliMsgList } from "./
|
|
32
|
-
import {
|
|
33
|
-
import {
|
|
34
|
-
import {
|
|
35
|
-
import {
|
|
36
|
-
import {
|
|
37
|
-
import type
|
|
38
|
-
import { attachSplit, type SplitHandle } from "./layout/split.js";
|
|
31
|
+
import { CliMsgList, CliChat, CliBoard } from "./components";
|
|
32
|
+
import type { BoardTab } from "./board/CliBoard";
|
|
33
|
+
import { CliBrowserView, CliFilePreview } from "./board/blocks";
|
|
34
|
+
import { createCliStore, cloneWorkspaceEvent, type CliEvent, type CliStore } from "./core/store";
|
|
35
|
+
import { convKey } from "./core/conversation";
|
|
36
|
+
import type { CliWorkspace } from "./board/workspace";
|
|
37
|
+
import { attachSplit, type SplitHandle } from "./layout/split";
|
|
39
38
|
|
|
40
|
-
|
|
39
|
+
type StoreOptions = ReturnType<typeof storeOptions>;
|
|
40
|
+
type GroupMember = { el: HTMLElement; refresh: () => void };
|
|
41
|
+
type StoreBucket = { store: CliStore; options: StoreOptions; members: Set<GroupMember> };
|
|
42
|
+
type StoreGroup = { buckets: Map<string, StoreBucket>; preferred: StoreBucket | null; owner: GroupMember | null; configured: boolean; members: Set<GroupMember> };
|
|
43
|
+
type GroupBinding = { groupName: string; group: StoreGroup; bucket: StoreBucket };
|
|
44
|
+
const groups = new Map<string, StoreGroup>();
|
|
45
|
+
const STORE_ATTRS = ["base-url", "token", "line", "namespace", "new-conversation"] as const;
|
|
46
|
+
const identity = (options: StoreOptions) => JSON.stringify(options);
|
|
47
|
+
const configured = (el: HTMLElement) => STORE_ATTRS.some(name => el.hasAttribute(name));
|
|
48
|
+
|
|
49
|
+
const memberships = new WeakMap<GroupMember, { groupName: string; group: StoreGroup }>();
|
|
50
|
+
function notifyGroup(group: StoreGroup, except?: GroupMember) {
|
|
51
|
+
const peers = [...group.members];
|
|
52
|
+
queueMicrotask(() => { for (const peer of peers) if (peer !== except) peer.refresh(); });
|
|
53
|
+
}
|
|
54
|
+
function leaveGroup(member: GroupMember) {
|
|
55
|
+
const membership = memberships.get(member);
|
|
56
|
+
if (!membership) return;
|
|
57
|
+
const { groupName, group } = membership;
|
|
58
|
+
group.members.delete(member);
|
|
59
|
+
for (const [key, bucket] of group.buckets) {
|
|
60
|
+
bucket.members.delete(member);
|
|
61
|
+
if (!bucket.members.size && bucket !== group.preferred) group.buckets.delete(key);
|
|
62
|
+
}
|
|
63
|
+
if (group.owner === member) group.owner = null;
|
|
64
|
+
memberships.delete(member);
|
|
65
|
+
if (!group.members.size) groups.delete(groupName); else notifyGroup(group);
|
|
66
|
+
}
|
|
67
|
+
function releaseGroup(binding: GroupBinding, member: GroupMember, retainOwner = false) {
|
|
68
|
+
const { group, bucket } = binding;
|
|
69
|
+
bucket.members.delete(member);
|
|
70
|
+
if (!retainOwner && group.owner === member) group.owner = null;
|
|
71
|
+
if (!bucket.members.size && bucket !== group.preferred) group.buckets.delete(identity(bucket.options));
|
|
72
|
+
notifyGroup(group, member);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function inheritedOptions(el: HTMLElement, base: StoreOptions | null): StoreOptions {
|
|
76
|
+
if (!base) return storeOptions(el);
|
|
77
|
+
return {
|
|
78
|
+
baseUrl: el.hasAttribute("base-url") ? el.getAttribute("base-url")! : base.baseUrl,
|
|
79
|
+
token: el.hasAttribute("token") ? el.getAttribute("token")! : base.token,
|
|
80
|
+
line: el.hasAttribute("line") ? el.getAttribute("line")! : base.line,
|
|
81
|
+
namespace: el.hasAttribute("namespace") ? el.getAttribute("namespace")! : base.namespace,
|
|
82
|
+
newConversation: el.hasAttribute("new-conversation") ? jsonAttr(el, "new-conversation") : base.newConversation,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
41
85
|
|
|
42
86
|
// 服务端渲染(Next.js 等)时没有 HTMLElement:模块照样能导入,defineCliElements() 在服务端什么都不做。
|
|
43
87
|
const Base = (typeof HTMLElement === "undefined" ? class {} : HTMLElement) as typeof HTMLElement;
|
|
44
88
|
|
|
45
|
-
function storeFor(el: HTMLElement):
|
|
46
|
-
const
|
|
47
|
-
if (!
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
groups.set(key, store);
|
|
89
|
+
function storeFor(el: HTMLElement, member: GroupMember, previous?: GroupBinding): GroupBinding | undefined {
|
|
90
|
+
const groupName = el.getAttribute("group");
|
|
91
|
+
if (!groupName) { leaveGroup(member); return undefined; }
|
|
92
|
+
if (memberships.get(member)?.groupName !== groupName) leaveGroup(member);
|
|
93
|
+
let group = groups.get(groupName);
|
|
94
|
+
if (!group) {
|
|
95
|
+
group = { buckets: new Map(), preferred: null, owner: null, configured: false, members: new Set() };
|
|
96
|
+
groups.set(groupName, group);
|
|
54
97
|
}
|
|
55
|
-
|
|
98
|
+
group.members.add(member);
|
|
99
|
+
memberships.set(member, { groupName, group });
|
|
100
|
+
const ownsDefaults = group.owner === member || (!group.configured && configured(el));
|
|
101
|
+
const matches = [...group.buckets.values()].filter(item => [...item.members].some(peer => configured(peer.el))).filter(item => {
|
|
102
|
+
const values = storeOptions(el);
|
|
103
|
+
return STORE_ATTRS.every(name => {
|
|
104
|
+
if (!el.hasAttribute(name)) return true;
|
|
105
|
+
const key = ({ "base-url": "baseUrl", token: "token", line: "line", namespace: "namespace", "new-conversation": "newConversation" } as const)[name as typeof STORE_ATTRS[number]];
|
|
106
|
+
return JSON.stringify(values[key as keyof StoreOptions]) === JSON.stringify(item.options[key as keyof StoreOptions]);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
if (!ownsDefaults && matches.length > 1) throw new Error("同一 group 有多个连接配置,请明确设置 base-url、token、line 或 namespace,避免混用会话。");
|
|
110
|
+
const options = inheritedOptions(el, ownsDefaults ? null : matches[0]?.options ?? group.preferred?.options ?? null);
|
|
111
|
+
const key = identity(options);
|
|
112
|
+
let bucket = group.buckets.get(key);
|
|
113
|
+
const newBucket = !bucket;
|
|
114
|
+
if (!bucket) {
|
|
115
|
+
bucket = { store: createCliStore(options), options, members: new Set() };
|
|
116
|
+
group.buckets.set(key, bucket);
|
|
117
|
+
}
|
|
118
|
+
bucket.members.add(member);
|
|
119
|
+
if (newBucket) notifyGroup(group, member);
|
|
120
|
+
if (!group.preferred || ownsDefaults) {
|
|
121
|
+
const changed = group.preferred !== bucket;
|
|
122
|
+
group.preferred = bucket;
|
|
123
|
+
if (ownsDefaults) { group.configured = true; group.owner = member; }
|
|
124
|
+
if (changed) {
|
|
125
|
+
// Config may arrive after a sibling with only group was connected. Move
|
|
126
|
+
// inheriting siblings together; explicitly different credentials remain isolated.
|
|
127
|
+
notifyGroup(group, member);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const binding = { groupName, group, bucket };
|
|
131
|
+
if (previous && (previous.group !== group || previous.bucket !== bucket)) releaseGroup(previous, member, previous.group === group);
|
|
132
|
+
return binding;
|
|
56
133
|
}
|
|
57
134
|
|
|
58
|
-
const ATTRS = ["base-url", "token", "line", "namespace", "group", "selected-id", "job-id", "task-id", "title", "color", "theme", "context", "read-only", "config", "new-conversation", "tabs", "default-tab", "file-open", "path", "no-quote", "hidden"];
|
|
135
|
+
const ATTRS = ["base-url", "token", "line", "namespace", "group", "selected-id", "job-id", "task-id", "title", "color", "theme", "context", "read-only", "config", "new-conversation", "tabs", "available-tabs", "default-tab", "file-open", "path", "no-quote", "hidden", "watch-files", "passive-context"];
|
|
59
136
|
|
|
60
137
|
function jsonAttr(el: HTMLElement, name: string): object | undefined {
|
|
61
138
|
const raw = el.getAttribute(name);
|
|
@@ -77,6 +154,9 @@ abstract class CliElement extends Base {
|
|
|
77
154
|
static observedAttributes = ATTRS;
|
|
78
155
|
private root: Root | null = null;
|
|
79
156
|
private ownStore: CliStore | undefined;
|
|
157
|
+
private ownStoreKey: string | undefined;
|
|
158
|
+
private groupBinding: GroupBinding | undefined;
|
|
159
|
+
private groupMember: GroupMember = { el: this, refresh: () => { if (this.root) this.render(); } };
|
|
80
160
|
private configValue: object | undefined;
|
|
81
161
|
|
|
82
162
|
/** 界面配置(对象)。优先于 config 属性里的 JSON。 */
|
|
@@ -90,7 +170,12 @@ abstract class CliElement extends Base {
|
|
|
90
170
|
|
|
91
171
|
/** 这个元素用的仓库(同一个 group 的元素共用一个)。高级用法:store.on(...)、store.select(...)。 */
|
|
92
172
|
get store(): CliStore {
|
|
93
|
-
|
|
173
|
+
const binding = storeFor(this, this.groupMember, this.groupBinding);
|
|
174
|
+
if (binding) { this.groupBinding = binding; return binding.bucket.store; }
|
|
175
|
+
if (this.groupBinding) { releaseGroup(this.groupBinding, this.groupMember); this.groupBinding = undefined; }
|
|
176
|
+
const options = storeOptions(this), key = identity(options);
|
|
177
|
+
if (!this.ownStore || this.ownStoreKey !== key) { this.ownStore = createCliStore(options); this.ownStoreKey = key; }
|
|
178
|
+
return this.ownStore;
|
|
94
179
|
}
|
|
95
180
|
/** 元素自己写的对话(job-id + task-id);没写时 null(跟着这一组的选中走)。 */
|
|
96
181
|
private ownKey(): string | null {
|
|
@@ -129,7 +214,12 @@ abstract class CliElement extends Base {
|
|
|
129
214
|
}
|
|
130
215
|
disconnectedCallback() {
|
|
131
216
|
// 移动节点时会先断开再连上:推到下一轮再卸载。
|
|
132
|
-
queueMicrotask(() => { if (!this.isConnected) {
|
|
217
|
+
queueMicrotask(() => { if (!this.isConnected) {
|
|
218
|
+
this.root?.unmount(); this.root = null;
|
|
219
|
+
leaveGroup(this.groupMember); this.groupBinding = undefined;
|
|
220
|
+
this.ownStore = undefined; this.ownStoreKey = undefined;
|
|
221
|
+
this.appliedSelection = null; this.selectionStore = null;
|
|
222
|
+
} });
|
|
133
223
|
}
|
|
134
224
|
attributeChangedCallback() { this.syncHidden(); if (this.root) this.render(); }
|
|
135
225
|
private syncHidden() {
|
|
@@ -138,12 +228,15 @@ abstract class CliElement extends Base {
|
|
|
138
228
|
}
|
|
139
229
|
|
|
140
230
|
private appliedSelection: string | null = null;
|
|
231
|
+
private selectionStore: CliStore | null = null;
|
|
141
232
|
/** selected-id:这一组一起看哪个对话。属性变了才切(列表点击换了选中,不会被属性拉回去)。隐藏的元素也生效。 */
|
|
142
233
|
private applySelection() {
|
|
143
234
|
const selection = this.getAttribute("selected-id");
|
|
144
|
-
|
|
235
|
+
const store = this.store;
|
|
236
|
+
if (selection !== null && (selection !== this.appliedSelection || store !== this.selectionStore)) {
|
|
145
237
|
this.appliedSelection = selection;
|
|
146
|
-
this.
|
|
238
|
+
this.selectionStore = store;
|
|
239
|
+
store.select(selection || null);
|
|
147
240
|
}
|
|
148
241
|
}
|
|
149
242
|
protected common() {
|
|
@@ -153,7 +246,12 @@ abstract class CliElement extends Base {
|
|
|
153
246
|
title: this.getAttribute("title") ?? undefined,
|
|
154
247
|
color: this.getAttribute("color") ?? undefined,
|
|
155
248
|
theme: (this.getAttribute("theme") as "light" | "dark" | null) ?? undefined,
|
|
156
|
-
onEvent: (event: unknown) =>
|
|
249
|
+
onEvent: (event: unknown) => {
|
|
250
|
+
this.dispatchEvent(new CustomEvent("cli-event", { detail: cloneWorkspaceEvent(event as CliEvent), bubbles: true }));
|
|
251
|
+
const type=(event as {type?:string}).type;
|
|
252
|
+
const name=type==='files_changed'?'cli-files-change':type==='files_watch'?'cli-files-watch':type==='board_context'?'cli-board-context':null;
|
|
253
|
+
if(name)this.dispatchEvent(new CustomEvent(name,{detail:event,bubbles:true}));
|
|
254
|
+
},
|
|
157
255
|
onTurnEnd: (payload: unknown) => this.dispatchEvent(new CustomEvent("cli-turn-end", { detail: payload, bubbles: true })),
|
|
158
256
|
onConversationEnd: (payload: unknown) => this.dispatchEvent(new CustomEvent("cli-conversation-end", { detail: payload, bubbles: true })),
|
|
159
257
|
onCallback: (payload: unknown) => this.dispatchEvent(new CustomEvent("cli-callback", { detail: payload, bubbles: true })),
|
|
@@ -164,8 +262,14 @@ abstract class CliElement extends Base {
|
|
|
164
262
|
protected abstract element(): React.ReactNode;
|
|
165
263
|
// 隐藏的元素什么都不渲染:不轮询、不占连接;workspace() / appendContext() 照样能用。
|
|
166
264
|
private render() {
|
|
167
|
-
|
|
168
|
-
|
|
265
|
+
try {
|
|
266
|
+
this.applySelection();
|
|
267
|
+
this.root?.render(this.hasAttribute("hidden") ? null : this.element());
|
|
268
|
+
} catch (error) {
|
|
269
|
+
// Configuration errors must unmount the previous connection, never leave
|
|
270
|
+
// another account's old interface interactive under new attributes.
|
|
271
|
+
this.root?.render(this.hasAttribute("hidden") ? null : <div className="cli-ui cli-error" role="alert">{error instanceof Error ? error.message : "组件连接配置无效"}</div>);
|
|
272
|
+
}
|
|
169
273
|
}
|
|
170
274
|
}
|
|
171
275
|
|
|
@@ -183,7 +287,7 @@ class CliChatElement extends CliElement {
|
|
|
183
287
|
const jobId = this.getAttribute("job-id");
|
|
184
288
|
const taskId = this.getAttribute("task-id");
|
|
185
289
|
const fileOpen = this.getAttribute("file-open") as "auto" | "board" | "modal" | "link" | null;
|
|
186
|
-
return <CliChat {...this.common()} {...(jobId !== null ? { jobId: jobId || null } : {})} {...(taskId ? { taskId } : {})} readOnly={this.hasAttribute("read-only")} {...(fileOpen ? { fileOpen } : {})} />;
|
|
290
|
+
return <CliChat {...this.common()} {...(jobId !== null ? { jobId: jobId || null } : {})} {...(taskId ? { taskId } : {})} passiveContext={this.getAttribute("passive-context")!=="false"} readOnly={this.hasAttribute("read-only")} {...(fileOpen ? { fileOpen } : {})} />;
|
|
187
291
|
}
|
|
188
292
|
}
|
|
189
293
|
|
|
@@ -191,12 +295,14 @@ class CliBoardElement extends CliElement {
|
|
|
191
295
|
protected element() {
|
|
192
296
|
const jobId = this.getAttribute("job-id");
|
|
193
297
|
const tabs = this.getAttribute("tabs")?.split(",").map((t) => t.trim()).filter(Boolean) as BoardTab[] | undefined;
|
|
298
|
+
const availableTabs = this.getAttribute("available-tabs")?.split(",").map(t=>t.trim()).filter(Boolean) as BoardTab[] | undefined;
|
|
194
299
|
const defaultTab = (this.getAttribute("default-tab") as BoardTab | null) ?? undefined;
|
|
195
300
|
// 结果事件只由 <cli-chat> 发(同上);看板只发 cli-event。
|
|
196
301
|
const { onTurnEnd: _a, onConversationEnd: _b, onCallback: _c, title: _t, color: _k, config: _g, ...common } = this.common();
|
|
197
302
|
const taskId = this.getAttribute("task-id");
|
|
198
|
-
return <CliBoard {...common} {...(jobId !== null ? { jobId: jobId || null } : {})} {...(taskId ? { taskId } : {})} tabs={tabs
|
|
303
|
+
return <CliBoard {...common} {...(jobId !== null ? { jobId: jobId || null } : {})} {...(taskId ? { taskId } : {})} tabs={this.hasAttribute("tabs") ? tabs ?? [] : undefined} availableTabs={availableTabs} defaultTab={defaultTab} readOnly={this.hasAttribute("read-only")}
|
|
199
304
|
// 同一个 group 里有对话:看板里「引用到对话」带进那个对话的下一条消息;no-quote 关掉。
|
|
305
|
+
watchFiles={this.getAttribute("watch-files")!=="false"}
|
|
200
306
|
quoteToChat={this.hasAttribute("group") && !this.hasAttribute("no-quote")} />;
|
|
201
307
|
}
|
|
202
308
|
}
|
package/src/embed.ts
CHANGED
|
@@ -14,14 +14,12 @@
|
|
|
14
14
|
import "./styles.css";
|
|
15
15
|
import { createElement } from "react";
|
|
16
16
|
import { createRoot } from "react-dom/client";
|
|
17
|
-
import { defineCliElements } from "./elements
|
|
18
|
-
import { CliChat, type CliChatProps } from "./
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
22
|
-
import {
|
|
23
|
-
import { createCliStore } from "./core/store.js";
|
|
24
|
-
import { createCenterClient } from "./core/client.js";
|
|
17
|
+
import { defineCliElements } from "./elements";
|
|
18
|
+
import { CliChat, CliMsgList, CliBoard, type CliChatProps, type CliMsgListProps, type CliBoardProps } from "./components";
|
|
19
|
+
import { CliBrowserView, CliFilePreview, type CliBrowserViewProps, type CliFilePreviewProps } from "./board/blocks";
|
|
20
|
+
import { CliCenterProvider } from "./core/provider";
|
|
21
|
+
import { createCliStore } from "./core/store";
|
|
22
|
+
import { createCenterClient } from "./core/client";
|
|
25
23
|
|
|
26
24
|
export interface Mounted<P> {
|
|
27
25
|
/** 合并更新参数(只传要改的)。 */
|
package/src/index.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
export { CliMsgList, CliChat, CliBoard, type CliMsgListProps, type CliChatProps, type CliBoardProps } from "./components";
|
|
1
2
|
/**
|
|
2
3
|
* @spzxg/cli-ui —— 任务中心(cli-task-center)对话任务的前端积木,数据全部来自 center 服务。
|
|
3
4
|
*
|
|
@@ -19,27 +20,33 @@
|
|
|
19
20
|
* import "@spzxg/cli-ui/styles.css";
|
|
20
21
|
* 非 React 页面用 Web Component:import { defineCliElements } from "@spzxg/cli-ui/elements"。
|
|
21
22
|
*/
|
|
22
|
-
export {
|
|
23
|
-
export {
|
|
24
|
-
export {
|
|
25
|
-
export {
|
|
26
|
-
export {
|
|
27
|
-
export {
|
|
28
|
-
export {
|
|
29
|
-
export {
|
|
30
|
-
export {
|
|
31
|
-
export { attachSplit, type SplitPane, type SplitOptions, type SplitHandle } from "./layout/split.js";
|
|
32
|
-
export { CliCenterProvider, useCliStore, useCliState, type CliCenterProviderProps, type ResultHandlers } from "./core/provider.js";
|
|
23
|
+
export { type BoardTab, type BoardExtraTab } from "./board/CliBoard";
|
|
24
|
+
export { downloadFolder, zipFolder, listFolder, FOLDER_MAX_FILES, FOLDER_MAX_BYTES, type FolderResult, type FolderZip, type FolderProgress } from "./board/folderDownload";
|
|
25
|
+
export { createWorkspace, WORKSPACE_TEXT_MAX, WORKSPACE_LIST_MAX, WORKSPACE_FILE_MAX, type CliWorkspace, type BoardView } from "./board/workspace";
|
|
26
|
+
export { CliFilePreview, CliBrowserView, type CliFilePreviewProps, type CliBrowserViewProps } from "./board/blocks";
|
|
27
|
+
export { FileViewer, downloadFile, type FileViewerProps, type FileAction } from "./board/FileViewer";
|
|
28
|
+
export { BrowserPane } from "./board/BrowserPane";
|
|
29
|
+
export { CliSplit, type CliSplitProps } from "./layout/CliSplit";
|
|
30
|
+
export { attachSplit, type SplitPane, type SplitOptions, type SplitHandle } from "./layout/split";
|
|
31
|
+
export { CliCenterProvider, useCliStore, useCliState, type CliCenterProviderProps, type ResultHandlers } from "./core/provider";
|
|
33
32
|
|
|
34
33
|
export {
|
|
35
34
|
resolveChatConfig, resolveMsgListConfig, DEFAULT_CHAT_CONFIG, DEFAULT_MSGLIST_CONFIG,
|
|
36
35
|
type CliChatConfig, type CliMsgListConfig, type ResolvedChatConfig, type ResolvedMsgListConfig,
|
|
37
|
-
} from "./core/config
|
|
38
|
-
export { createCliStore, CONTEXT_ITEM_MAX, type CliStore, type CliStoreOptions, type CliState, type CliEvent, type JobEntry, type OpenTarget, type BoardRef, type ContextItem } from "./core/store
|
|
39
|
-
export { createCenterClient, CenterError, boardFileUrl, type CenterClient, type CenterClientOptions, type StreamHandlers, type BoardBackend, type ConversationBackend } from "./core/client
|
|
40
|
-
export { viewMessages, isRunning, runningTurn, convKey, parseConvKey, NEW_TASK, type ConvState } from "./core/conversation
|
|
36
|
+
} from "./core/config";
|
|
37
|
+
export { createCliStore, CONTEXT_ITEM_MAX, type CliStore, type CliStoreOptions, type CliState, type CliEvent, type JobEntry, type OpenTarget, type BoardRef, type ContextItem } from "./core/store";
|
|
38
|
+
export { createCenterClient, CenterError, boardFileUrl, type CenterClient, type CenterClientOptions, type StreamHandlers, type BoardBackend, type ConversationBackend } from "./core/client";
|
|
39
|
+
export { viewMessages, isRunning, runningTurn, convKey, parseConvKey, NEW_TASK, type ConvState } from "./core/conversation";
|
|
41
40
|
export {
|
|
42
41
|
composeMessage, splitMessage, serializeBlock, readFileText, isLongText, ACCEPTED_EXTENSIONS, MAX_MESSAGE_CHARS,
|
|
43
42
|
type TextBlock, type MessagePart,
|
|
44
|
-
} from "./core/blocks
|
|
45
|
-
export type * from "./core/types
|
|
43
|
+
} from "./core/blocks";
|
|
44
|
+
export type * from "./core/types";
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
export { RunControls, type RunControlProps, type RunCapabilities, type RunTask } from "./chat/RunControls";
|
|
48
|
+
|
|
49
|
+
export { formatBoardContext, appendBoardContext, BOARD_HISTORY_MAX } from "./board/context";
|
|
50
|
+
export { subscribeWorkspaceFiles, type WorkspaceWatchOptions } from "./board/fileWatch";
|
|
51
|
+
export type { WorkspaceHandlers } from "./core/provider";
|
|
52
|
+
export type { BoardFile } from "./board/workspace";
|
package/src/layout/CliSplit.tsx
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* 三个组件在同一个 Provider 里照常联动(列表选中 → 对话和看板跟着切;对话里点文件 → 看板打开),分栏只管宽度。
|
|
13
13
|
*/
|
|
14
14
|
import { Children, useEffect, useRef, type CSSProperties, type ReactNode } from "react";
|
|
15
|
-
import { attachSplit, type SplitHandle, type SplitPane } from "./split
|
|
15
|
+
import { attachSplit, type SplitHandle, type SplitPane } from "./split";
|
|
16
16
|
|
|
17
17
|
export type CliSplitProps = {
|
|
18
18
|
children: ReactNode;
|
package/src/layout/split.ts
CHANGED
|
@@ -5,12 +5,12 @@
|
|
|
5
5
|
* attachSplit(container, { panes: [{ size: 300, min: 220, max: 480 }, { min: 360 }, { size: "42%", min: 320 }], storageKey: "strategy" })
|
|
6
6
|
*
|
|
7
7
|
* 规则:
|
|
8
|
-
* · 写了 size 的栏是定宽(px
|
|
8
|
+
* · 写了 size 的栏是定宽(px 或百分比,未手动调整的百分比随容器宽度变化);没写 size 的栏占剩下的宽度(只能有一个,没有就是最后一栏)。
|
|
9
9
|
* · 拖动条改它旁边定宽的那一栏;任何一栏都不会小于自己的 min,占剩余宽度的那栏也一样。
|
|
10
10
|
* · 双击拖动条:那一栏回到初始宽度。键盘:拖动条能聚焦,←/→ 每次 16px(按住 Shift 64px)。
|
|
11
11
|
* · 子元素根节点带 collapsed 类(cli-board 收起时):那一栏变成 40px 窄条(--cli-split-collapsed),旁边的拖动条隐藏;展开后回到原来的宽度。
|
|
12
12
|
* · 写了 storageKey:拖好的宽度记在 localStorage(cli-ui:split:<storageKey>),下次打开还是这个宽度。
|
|
13
|
-
* ·
|
|
13
|
+
* · 窗口变窄时定宽栏按超过 min 的空间比例让出宽度;各栏 min 之和能放下时,保证占剩余宽度的那栏不小于它的 min。
|
|
14
14
|
*/
|
|
15
15
|
export type SplitPane = { size?: number | `${number}%`; min?: number; max?: number };
|
|
16
16
|
export type SplitOptions = {
|
|
@@ -26,6 +26,8 @@ const STEP = 16;
|
|
|
26
26
|
|
|
27
27
|
export function attachSplit(container: HTMLElement, initial: SplitOptions = {}): SplitHandle {
|
|
28
28
|
let options = initial;
|
|
29
|
+
// Only user overrides are pixels. null keeps the configured responsive default.
|
|
30
|
+
// Old persisted numeric arrays remain valid user preferences.
|
|
29
31
|
let sizes: (number | null)[] = [];
|
|
30
32
|
let gutters: HTMLElement[] = [];
|
|
31
33
|
let panes: HTMLElement[] = [];
|
|
@@ -45,48 +47,79 @@ export function attachSplit(container: HTMLElement, initial: SplitOptions = {}):
|
|
|
45
47
|
};
|
|
46
48
|
|
|
47
49
|
const spec = (i: number): SplitPane => options.panes?.[i] ?? {};
|
|
48
|
-
const
|
|
50
|
+
const preferredFlexIndex = () => {
|
|
49
51
|
const i = panes.findIndex((_, n) => spec(n).size === undefined);
|
|
50
52
|
return i >= 0 ? i : panes.length - 1;
|
|
51
53
|
};
|
|
52
|
-
const minOf = (i: number) => spec(i).min ?? DEFAULT_MIN;
|
|
54
|
+
const minOf = (i: number) => Math.max(0, spec(i).min ?? DEFAULT_MIN);
|
|
55
|
+
const clamp = (i: number, w: number) => Math.round(Math.min(Math.max(minOf(i), spec(i).max ?? Infinity), Math.max(minOf(i), w)));
|
|
56
|
+
const contentWidth = () => {
|
|
57
|
+
const style = getComputedStyle(container);
|
|
58
|
+
return Math.max(0, container.clientWidth - (parseFloat(style.paddingLeft) || 0) - (parseFloat(style.paddingRight) || 0));
|
|
59
|
+
};
|
|
53
60
|
const initialSize = (i: number): number | null => {
|
|
54
|
-
if (i ===
|
|
61
|
+
if (i === preferredFlexIndex()) return null;
|
|
55
62
|
const s = spec(i).size;
|
|
56
|
-
if (typeof s === "string") return
|
|
57
|
-
return s ?? 320;
|
|
63
|
+
if (typeof s === "string") return clamp(i, (parseFloat(s) / 100) * (contentWidth() || 1200));
|
|
64
|
+
return clamp(i, s ?? 320);
|
|
58
65
|
};
|
|
59
66
|
const isCollapsed = (pane: HTMLElement) => !!pane.firstElementChild?.classList.contains("collapsed");
|
|
60
|
-
const
|
|
67
|
+
const isVisible = (pane: HTMLElement) => !pane.hidden && getComputedStyle(pane).display !== "none";
|
|
68
|
+
const flexIndex = () => {
|
|
69
|
+
const preferred = preferredFlexIndex();
|
|
70
|
+
if (panes[preferred] && isVisible(panes[preferred]) && !isCollapsed(panes[preferred])) return preferred;
|
|
71
|
+
// A host may hide whole panes on mobile. A remaining visible pane fills the
|
|
72
|
+
// space without turning its default width into a permanent pixel override.
|
|
73
|
+
for (let i = panes.length - 1; i >= 0; i--) if (isVisible(panes[i]) && !isCollapsed(panes[i])) return i;
|
|
74
|
+
return -1;
|
|
75
|
+
};
|
|
76
|
+
const desiredSizes = () => panes.map((_, i) => i === preferredFlexIndex() ? null : clamp(i, sizes[i] ?? initialSize(i)!));
|
|
61
77
|
|
|
62
78
|
/** 定宽栏放进容器:总宽超了就按比例收,给占剩余宽度的那栏留出它的 min。 */
|
|
63
|
-
function fitted(): (number | null)[] {
|
|
64
|
-
const
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
79
|
+
function fitted(flex: number): (number | null)[] {
|
|
80
|
+
const shown = desiredSizes();
|
|
81
|
+
const fixed = panes.flatMap((p, i) => i !== flex && isVisible(p) && !isCollapsed(p) ? [i] : []);
|
|
82
|
+
const collapsedW = panes.reduce((sum, p) => sum + (isVisible(p) && isCollapsed(p) ? p.getBoundingClientRect().width : 0), 0);
|
|
83
|
+
const gutterW = gutters.reduce((sum, g) => {
|
|
84
|
+
if (g.hidden) return sum;
|
|
85
|
+
const style = getComputedStyle(g);
|
|
86
|
+
const width = g.getBoundingClientRect().width || parseFloat(style.flexBasis) || 0;
|
|
87
|
+
return sum + Math.max(0, width + (parseFloat(style.marginLeft) || 0) + (parseFloat(style.marginRight) || 0));
|
|
88
|
+
}, 0);
|
|
89
|
+
const room = contentWidth() - (flex < 0 ? 0 : minOf(flex)) - collapsedW - gutterW;
|
|
90
|
+
const sum = fixed.reduce((total, i) => total + shown[i]!, 0);
|
|
91
|
+
if (!container.clientWidth || sum <= room) return shown;
|
|
92
|
+
const minimum = fixed.reduce((total, i) => total + minOf(i), 0);
|
|
93
|
+
// Scaling whole widths can hit one pane's minimum and still overflow the
|
|
94
|
+
// flexible pane. Allocate only the space above each minimum instead.
|
|
95
|
+
const ratio = sum > minimum ? Math.max(0, room - minimum) / (sum - minimum) : 0;
|
|
96
|
+
for (const i of fixed) shown[i] = minOf(i) + Math.floor((shown[i]! - minOf(i)) * ratio);
|
|
97
|
+
return shown;
|
|
73
98
|
}
|
|
74
99
|
|
|
75
100
|
function apply() {
|
|
76
101
|
const flex = flexIndex();
|
|
77
|
-
|
|
102
|
+
// Set narrow strips before measuring: their previous expanded width must
|
|
103
|
+
// not be subtracted from the available space on the first collapse frame.
|
|
104
|
+
for (const pane of panes) {
|
|
105
|
+
if (!isCollapsed(pane)) continue;
|
|
106
|
+
pane.style.flex = "0 0 var(--cli-split-collapsed, 40px)";
|
|
107
|
+
pane.style.width = "";
|
|
108
|
+
pane.style.minWidth = "";
|
|
109
|
+
}
|
|
110
|
+
gutters.forEach((g, i) => { g.hidden = !isVisible(panes[i]) || !isVisible(panes[i + 1]) || isCollapsed(panes[i]) || isCollapsed(panes[i + 1]); });
|
|
111
|
+
const shown = fitted(flex);
|
|
78
112
|
panes.forEach((pane, i) => {
|
|
79
113
|
pane.classList.add("cli-split-pane");
|
|
114
|
+
if (isCollapsed(pane)) return;
|
|
80
115
|
if (i === flex) { pane.style.flex = "1 1 0"; pane.style.width = ""; pane.style.minWidth = `${minOf(i)}px`; return; }
|
|
81
|
-
//
|
|
82
|
-
if (
|
|
116
|
+
// Hidden preferred flex panes keep their flex role for when they return.
|
|
117
|
+
if (shown[i] === null) { pane.style.flex = "1 1 0"; pane.style.width = ""; pane.style.minWidth = `${minOf(i)}px`; return; }
|
|
83
118
|
pane.style.flex = `0 0 ${shown[i]}px`;
|
|
84
119
|
pane.style.width = `${shown[i]}px`;
|
|
85
120
|
pane.style.minWidth = "0";
|
|
86
121
|
});
|
|
87
122
|
gutters.forEach((g, i) => {
|
|
88
|
-
const hidden = isCollapsed(panes[i]) || isCollapsed(panes[i + 1]);
|
|
89
|
-
g.hidden = hidden;
|
|
90
123
|
g.setAttribute("aria-valuenow", String(Math.round(panes[sideOf(i)]?.getBoundingClientRect().width ?? 0)));
|
|
91
124
|
});
|
|
92
125
|
}
|
|
@@ -95,18 +128,20 @@ export function attachSplit(container: HTMLElement, initial: SplitOptions = {}):
|
|
|
95
128
|
const sideOf = (gutter: number) => (gutter === flexIndex() ? gutter + 1 : gutter);
|
|
96
129
|
|
|
97
130
|
function resize(gutter: number, dx: number) {
|
|
131
|
+
if (gutters[gutter]?.hidden) return;
|
|
98
132
|
const target = sideOf(gutter);
|
|
99
133
|
const sign = target === gutter ? 1 : -1;
|
|
100
134
|
const flex = flexIndex();
|
|
135
|
+
if (flex < 0) return;
|
|
101
136
|
const current = panes[target].getBoundingClientRect().width;
|
|
102
137
|
// 占剩余宽度的那栏不能被挤到 min 以下。
|
|
103
138
|
const flexW = panes[flex].getBoundingClientRect().width;
|
|
104
|
-
const grow = Math.min(sign * dx, flexW - minOf(flex));
|
|
139
|
+
const grow = Math.min(sign * dx, Math.max(0, flexW - minOf(flex)));
|
|
105
140
|
sizes[target] = clamp(target, current + grow);
|
|
106
141
|
apply();
|
|
107
142
|
}
|
|
108
143
|
|
|
109
|
-
function finish() { save(); options.onResize?.(
|
|
144
|
+
function finish() { save(); options.onResize?.(desiredSizes()); }
|
|
110
145
|
|
|
111
146
|
function makeGutter(index: number) {
|
|
112
147
|
const g = document.createElement("div");
|
|
@@ -138,7 +173,7 @@ export function attachSplit(container: HTMLElement, initial: SplitOptions = {}):
|
|
|
138
173
|
});
|
|
139
174
|
g.addEventListener("dblclick", () => {
|
|
140
175
|
const target = sideOf(index);
|
|
141
|
-
sizes[target] =
|
|
176
|
+
sizes[target] = null;
|
|
142
177
|
apply();
|
|
143
178
|
finish();
|
|
144
179
|
});
|
|
@@ -153,20 +188,22 @@ export function attachSplit(container: HTMLElement, initial: SplitOptions = {}):
|
|
|
153
188
|
|
|
154
189
|
/** 子元素变了(宿主增删栏):重新找栏、重新插拖动条。 */
|
|
155
190
|
function rebuild() {
|
|
191
|
+
for (const p of panes) ro?.unobserve?.(p);
|
|
156
192
|
for (const g of gutters) g.remove();
|
|
157
193
|
panes = [...container.children].filter((c): c is HTMLElement => c instanceof HTMLElement && !c.classList.contains("cli-split-gutter"));
|
|
158
194
|
const saved = load();
|
|
159
|
-
const flex =
|
|
195
|
+
const flex = preferredFlexIndex();
|
|
160
196
|
sizes = panes.map((_, i) => {
|
|
161
197
|
if (i === flex) return null;
|
|
162
198
|
const keep = sizes[i];
|
|
163
|
-
if (typeof keep === "number") return keep;
|
|
199
|
+
if (typeof keep === "number" && Number.isFinite(keep)) return clamp(i, keep);
|
|
164
200
|
const s = saved && saved.length === panes.length ? saved[i] : null;
|
|
165
|
-
return typeof s === "number" ? clamp(i, s) :
|
|
201
|
+
return typeof s === "number" && Number.isFinite(s) ? clamp(i, s) : null;
|
|
166
202
|
});
|
|
167
203
|
gutters = panes.slice(0, -1).map((_, i) => makeGutter(i));
|
|
168
204
|
gutters.forEach((g, i) => panes[i].after(g));
|
|
169
205
|
apply();
|
|
206
|
+
for (const p of panes) ro?.observe(p);
|
|
170
207
|
}
|
|
171
208
|
|
|
172
209
|
// 只看栏的增删、栏的根节点换了、根节点的 class 变了(收起 / 展开);栏里面的内容(流式输出)不看,不然每个字都要重排。
|
|
@@ -199,7 +236,7 @@ export function attachSplit(container: HTMLElement, initial: SplitOptions = {}):
|
|
|
199
236
|
observer.disconnect(); rebuild(); observe();
|
|
200
237
|
},
|
|
201
238
|
reset() {
|
|
202
|
-
sizes = panes.map((
|
|
239
|
+
sizes = panes.map(() => null);
|
|
203
240
|
apply();
|
|
204
241
|
finish();
|
|
205
242
|
},
|
|
@@ -2,16 +2,16 @@
|
|
|
2
2
|
// 一张卡片是 center 里的一个对话任务(mode=conversation)。对话任务里开了几个对话时,卡片下面按对话展开,
|
|
3
3
|
// 点哪个对话选哪个(选中值是对话键 "job#task");卡片上的「+」在这个对话任务里新开一个对话(同一个工作区)。
|
|
4
4
|
import { useEffect, useMemo, 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
|
|
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
15
|
|
|
16
16
|
/** onTurnEnd / onConversationEnd:浏览器里从实时流算出的结果通知,不依赖 center 的 callback.url。 */
|
|
17
17
|
export interface CliMsgListProps extends CliStoreOptions, ResultHandlers {
|
|
@@ -59,6 +59,9 @@ export function CliMsgList(props: CliMsgListProps) {
|
|
|
59
59
|
|
|
60
60
|
const SEARCH_MIN = 6;
|
|
61
61
|
const BATCH_ACTIVE = new Set(["dispatching", "running", "verifying"]);
|
|
62
|
+
// An orphaned task can retain a fresh, authoritative human-wait observation
|
|
63
|
+
// while the center is reconciling its execution state. Do not call it running.
|
|
64
|
+
const BATCH_AWAITING = new Set([...BATCH_ACTIVE, "orphaned"]);
|
|
62
65
|
|
|
63
66
|
interface CardView {
|
|
64
67
|
id: string;
|
|
@@ -95,9 +98,11 @@ export function cardView(id: string, entry: JobEntry | undefined): CardView {
|
|
|
95
98
|
const label = cancelled ? "已取消" : failed ? "已失败" : "已完成";
|
|
96
99
|
return { ...base, cls: cancelled || failed ? "x" : "g", label, running: false, awaiting: false, group: cancelled ? "cancelled" : "done", activity: updatedAt ? `${label} · ${relTime(updatedAt)}` : label };
|
|
97
100
|
}
|
|
98
|
-
//
|
|
101
|
+
// A browser handoff/input card keeps its turn running while requiring a human.
|
|
99
102
|
if (batch) {
|
|
100
103
|
const running = (job?.tasks ?? []).some((task) => BATCH_ACTIVE.has(task.status));
|
|
104
|
+
const awaiting = (job?.tasks ?? []).some((task) => BATCH_AWAITING.has(task.status) && task.awaiting_input === true);
|
|
105
|
+
if (awaiting) return { ...base, cls: "a", label: "需要你处理", running, awaiting, group: "needs", activity: "等你处理" };
|
|
101
106
|
return { ...base, cls: "a", label: "进行中", running, awaiting: false, group: "active", activity: running ? "运行中 · 工作中" : status === "queued" ? "等待执行节点" : "进行中" };
|
|
102
107
|
}
|
|
103
108
|
// 开了几个对话:任何一个在跑就算在跑,任何一个停在提问上就要你处理(关掉的对话不算)。
|
|
@@ -105,7 +110,7 @@ export function cardView(id: string, entry: JobEntry | undefined): CardView {
|
|
|
105
110
|
const many = (conv?.conversations?.length ?? 0) > 1;
|
|
106
111
|
const running = many ? open.some((c) => c.running_turn) : !!conv?.running_turn;
|
|
107
112
|
// 上一轮以提问结束、后面没有新消息:等你回答。
|
|
108
|
-
const awaiting = many ? open.some((c) => !c.running_turn && !c.queued_turns && !!c.last_turn?.asked) : !running && !conv?.queued_turns && !!conv?.last_turn?.asked;
|
|
113
|
+
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);
|
|
109
114
|
if (awaiting) return { ...base, cls: "a", label: "需要你处理", running, awaiting, group: "needs", activity: updatedAt ? `等你处理 · ${relTime(updatedAt)}` : "等你处理" };
|
|
110
115
|
if (running) return { ...base, cls: "a", label: "进行中", running, awaiting, group: "active", activity: "运行中 · 工作中" };
|
|
111
116
|
const waiting = conv?.queued_turns ? `${conv.queued_turns} 条排队中` : status === "queued" ? "等待执行节点" : updatedAt ? relTime(updatedAt) : "未开始";
|
|
@@ -143,6 +148,7 @@ export function convRows(jobId: string, entry: JobEntry | undefined): ConvRowVie
|
|
|
143
148
|
if (tasks.length <= 1) return [];
|
|
144
149
|
return tasks.map((t) => {
|
|
145
150
|
const look = BATCH_ROW[t.status] ?? { cls: "a" as DotCls, label: t.status };
|
|
151
|
+
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 };
|
|
146
152
|
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 };
|
|
147
153
|
});
|
|
148
154
|
}
|
|
@@ -153,7 +159,7 @@ export function convRows(jobId: string, entry: JobEntry | undefined): ConvRowVie
|
|
|
153
159
|
const base = { key: convKey(jobId, c.task_id), title: c.title, turns: c.turns };
|
|
154
160
|
if (c.closed || jobEnded) return { ...base, cls: "g" as DotCls, label: "已结束", running: false, awaiting: false, activity: "已结束" };
|
|
155
161
|
const running = c.running_turn;
|
|
156
|
-
const awaiting = !running && !c.queued_turns && !!c.last_turn?.asked;
|
|
162
|
+
const awaiting = c.awaiting_input === true || (!running && !c.queued_turns && !!c.last_turn?.asked);
|
|
157
163
|
if (awaiting) return { ...base, cls: "a" as DotCls, label: "需要你处理", running, awaiting, activity: "等你处理" };
|
|
158
164
|
if (running) return { ...base, cls: "a" as DotCls, label: "进行中", running, awaiting, activity: "运行中" };
|
|
159
165
|
return { ...base, cls: "a" as DotCls, label: "进行中", running, awaiting, activity: c.queued_turns ? `${c.queued_turns} 条排队中` : "空闲" };
|