@spzxg/cli-ui 0.6.2 → 0.6.5
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 +26 -0
- package/README.md +137 -11
- package/dist/cli-ui-embed-BmLN2Jjg.js +43290 -0
- package/dist/{cli-ui-index-BY5i3wId.js → cli-ui-index-D4qfPHMd.js} +2 -2
- package/dist/{cli-ui-jszip.min-BZsUwI-s.js → cli-ui-jszip.min-DC9898pC.js} +1 -1
- package/dist/{cli-ui-jszip.min-m01fexHW.js → cli-ui-jszip.min-DpEl0G1z.js} +2 -2
- package/dist/{cli-ui-xlsx-CpWicJLT.js → cli-ui-xlsx-zs7oEtPj.js} +1 -1
- package/dist/cli-ui.css +32 -1
- package/dist/cli-ui.js +1 -1
- package/package.json +14 -8
- package/src/board/BrowserPane.tsx +160 -74
- package/src/board/ChangesPane.tsx +2 -2
- package/src/board/CliBoard.tsx +79 -50
- package/src/board/FileViewer.tsx +32 -22
- 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 +88 -0
- package/src/board/folderDownload.ts +30 -14
- package/src/board/panels.ts +33 -0
- package/src/board/textLoad.ts +2 -2
- package/src/board/workspace.ts +38 -9
- package/src/chat/CliChat.tsx +73 -30
- package/src/chat/Composer.tsx +41 -41
- 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 +2 -2
- package/src/core/callbacks.ts +8 -0
- package/src/core/client.ts +8 -6
- package/src/core/conversation.ts +3 -3
- package/src/core/id.ts +15 -0
- package/src/core/identity.ts +33 -0
- package/src/core/provider.tsx +24 -14
- package/src/core/store.ts +83 -26
- package/src/core/types.ts +34 -0
- 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/msglist/CliMsgList.tsx +10 -10
- 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 +463 -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 +80 -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 +279 -0
- package/src/operator.base.css +1798 -0
- package/src/operator.css +186 -16
- package/src/styles.css +20 -0
- package/src/ui/Markdown.tsx +7 -2
- package/src/ui/Popover.tsx +77 -0
- package/src/ui/TextBlock.tsx +3 -3
- package/src/ui/Transcript.tsx +10 -8
- 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
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, type ReactNode } from "react";
|
|
4
|
+
import type { Status } from "./protocol/types";
|
|
5
|
+
import { Icon } from "./icons";
|
|
6
|
+
import { isAwaiting, relTime } from "./format";
|
|
7
|
+
import { SEARCH_MIN, type ProjectRow, type TaskRow, type AgentsBundle, type TaskView } from "./types";
|
|
8
|
+
import { agentLabel } from "./agents";
|
|
9
|
+
import { StatusDot, PriPill, SearchBar, AgentBadge } from "./shared";
|
|
10
|
+
|
|
11
|
+
// The kanban alternative to the grouped task list (layout from the Claude
|
|
12
|
+
// Design "Operator — Board View" study, rendered with the app's own tokens).
|
|
13
|
+
// Columns are live views over the same task rows the list renders — cards
|
|
14
|
+
// update as sessions stream — and dragging a card re-statuses it and/or
|
|
15
|
+
// persists a new manual order.
|
|
16
|
+
type ColKey = "suggested" | "not_started" | "in_progress" | "awaiting" | "on_hold" | "done" | "cancelled";
|
|
17
|
+
|
|
18
|
+
const COL_ORDER: ColKey[] = ["suggested", "not_started", "in_progress", "awaiting", "on_hold", "done", "cancelled"];
|
|
19
|
+
|
|
20
|
+
// What lands on a task dropped into each column. `null` = the column rejects
|
|
21
|
+
// the drop (Suggested and Needs-input hold derived states you can't drag INTO —
|
|
22
|
+
// they still allow reordering their own cards). `{}` = position-only move.
|
|
23
|
+
type Patch = Partial<Pick<TaskRow, "status" | "suggested">> | null;
|
|
24
|
+
|
|
25
|
+
const COLS: Record<ColKey, {
|
|
26
|
+
label: string;
|
|
27
|
+
accent?: boolean; // Needs-input styling (blue header/rule)
|
|
28
|
+
derived?: boolean; // holds a derived state — badged, rejects foreign drops
|
|
29
|
+
mini?: boolean; // terminal column: compact rows, newest first, drops append
|
|
30
|
+
always: boolean; // On hold / Cancelled hide when empty (drop bays mid-drag)
|
|
31
|
+
member: (t: TaskRow) => boolean;
|
|
32
|
+
patchFor: (t: TaskRow) => Patch;
|
|
33
|
+
noDropWhy?: string; // reason line for the "can't drop here" callout
|
|
34
|
+
}> = {
|
|
35
|
+
suggested: {
|
|
36
|
+
label: "建议", derived: true, always: true,
|
|
37
|
+
member: (t) => !!t.suggested,
|
|
38
|
+
patchFor: (t) => (t.suggested ? {} : null),
|
|
39
|
+
noDropWhy: "「建议」是智能体提出工作的地方 —— 请把卡片接受出去,而不是拖进来。",
|
|
40
|
+
},
|
|
41
|
+
not_started: {
|
|
42
|
+
label: "未开始", always: true,
|
|
43
|
+
member: (t) => !t.suggested && t.status === "not_started",
|
|
44
|
+
patchFor: (t) => statusPatch(t, "not_started"),
|
|
45
|
+
},
|
|
46
|
+
in_progress: {
|
|
47
|
+
label: "进行中", always: true,
|
|
48
|
+
member: (t) => !t.suggested && t.status === "in_progress" && !isAwaiting(t),
|
|
49
|
+
// Dropping an awaiting card here is "I've dealt with it": the explicit
|
|
50
|
+
// status write clears the awaiting flag server-side, so patch even when
|
|
51
|
+
// the status string wouldn't change.
|
|
52
|
+
patchFor: (t) => (t.suggested ? { suggested: 0, status: "in_progress" } : t.status !== "in_progress" || isAwaiting(t) ? { status: "in_progress" } : {}),
|
|
53
|
+
},
|
|
54
|
+
awaiting: {
|
|
55
|
+
label: "需要输入", accent: true, derived: true, always: true,
|
|
56
|
+
member: (t) => !t.suggested && isAwaiting(t),
|
|
57
|
+
patchFor: (t) => (!t.suggested && isAwaiting(t) ? {} : null),
|
|
58
|
+
noDropWhy: "「需要输入」由会话状态推导而来 —— 智能体向你提问时会自动置为该状态。",
|
|
59
|
+
},
|
|
60
|
+
on_hold: {
|
|
61
|
+
label: "已搁置", always: false,
|
|
62
|
+
member: (t) => !t.suggested && t.status === "on_hold",
|
|
63
|
+
patchFor: (t) => statusPatch(t, "on_hold"),
|
|
64
|
+
},
|
|
65
|
+
done: {
|
|
66
|
+
label: "已完成", mini: true, always: true,
|
|
67
|
+
member: (t) => !t.suggested && t.status === "done",
|
|
68
|
+
patchFor: (t) => statusPatch(t, "done"),
|
|
69
|
+
},
|
|
70
|
+
cancelled: {
|
|
71
|
+
label: "已取消", mini: true, always: false,
|
|
72
|
+
member: (t) => !t.suggested && t.status === "cancelled",
|
|
73
|
+
patchFor: (t) => statusPatch(t, "cancelled"),
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// Dropping into a plain status column: accept a suggestion into the real list,
|
|
78
|
+
// change status when it differs, or (same column) just reorder.
|
|
79
|
+
function statusPatch(t: TaskRow, status: Status): Patch {
|
|
80
|
+
if (t.suggested) return { suggested: 0, status };
|
|
81
|
+
return t.status !== status ? { status } : {};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Terminal columns show at most this many rows before the "Show all" veil.
|
|
85
|
+
const MINI_CAP = 7;
|
|
86
|
+
|
|
87
|
+
// Day bucket for the Done column's group dividers (newest first).
|
|
88
|
+
function dayBucket(ts: number): string {
|
|
89
|
+
const day = (d: Date) => new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
|
|
90
|
+
const today = day(new Date());
|
|
91
|
+
if (ts >= today) return "今天";
|
|
92
|
+
if (ts >= today - 86_400_000) return "昨天";
|
|
93
|
+
return "更早";
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function BoardCard({ task, agents, selected, running, blockedBy, mini, dragging, canDrag, onSelect, onDragStart, onDragOverCard, onDropOnCard, onDragEnd, actions }: {
|
|
97
|
+
task: TaskRow; agents: AgentsBundle; selected: boolean; running: boolean; blockedBy?: string[];
|
|
98
|
+
mini?: boolean; dragging: boolean; canDrag: boolean;
|
|
99
|
+
onSelect: () => void; onDragStart: () => void; onDragOverCard: (e: React.DragEvent) => void;
|
|
100
|
+
onDropOnCard: (e: React.DragEvent) => void; onDragEnd: () => void; actions?: ReactNode;
|
|
101
|
+
}) {
|
|
102
|
+
const awaiting = isAwaiting(task);
|
|
103
|
+
const blocked = !!blockedBy?.length && !task.started;
|
|
104
|
+
const sessionCount = task.started ? task.generation : Math.max(0, task.generation - 1);
|
|
105
|
+
const activity = awaiting ? `waiting on you · ${relTime(task.updated_at)}`
|
|
106
|
+
: running ? "运行中 · 工作中"
|
|
107
|
+
: task.status === "done" ? `已完成 · ${relTime(task.updated_at)}`
|
|
108
|
+
: task.status === "cancelled" ? `已取消 · ${relTime(task.updated_at)}`
|
|
109
|
+
: task.status === "on_hold" ? `已搁置 · ${relTime(task.updated_at)}`
|
|
110
|
+
: task.started ? relTime(task.updated_at) : "未开始";
|
|
111
|
+
return (
|
|
112
|
+
<article
|
|
113
|
+
role="button" tabIndex={0}
|
|
114
|
+
className={`bcard ${mini ? "mini" : ""} ${selected ? "sel" : ""} ${awaiting ? "needs" : ""} ${running ? "working" : ""} ${dragging ? "dragging" : ""}`}
|
|
115
|
+
onClick={onSelect}
|
|
116
|
+
onKeyDown={(e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); onSelect(); } }}
|
|
117
|
+
draggable={canDrag}
|
|
118
|
+
onDragStart={(e) => { onDragStart(); e.dataTransfer.effectAllowed = "move"; }}
|
|
119
|
+
onDragOver={onDragOverCard}
|
|
120
|
+
onDrop={onDropOnCard}
|
|
121
|
+
onDragEnd={onDragEnd}
|
|
122
|
+
title={canDrag ? "拖动可移动 / 排序" : undefined}
|
|
123
|
+
>
|
|
124
|
+
<div className="bc-top">
|
|
125
|
+
<StatusDot status={task.status} running={running} awaiting={awaiting} />
|
|
126
|
+
<h3 className="bc-title">{task.title}</h3>
|
|
127
|
+
{!mini && <PriPill p={task.priority} />}
|
|
128
|
+
</div>
|
|
129
|
+
<div className="bc-meta">
|
|
130
|
+
<AgentBadge label={agentLabel(agents, task.agent)} multi={!mini && agents.agents.length > 1} />
|
|
131
|
+
<span className={`bc-act ${awaiting ? "need" : running ? "on" : ""}`}>{activity}</span>
|
|
132
|
+
</div>
|
|
133
|
+
{running && <div className="bc-bar"><i /></div>}
|
|
134
|
+
{actions}
|
|
135
|
+
{(blocked || sessionCount > 0) && !mini && (
|
|
136
|
+
<div className="bc-foot">
|
|
137
|
+
{blocked && (task.auto_start ? (
|
|
138
|
+
<span className="bc-chip autostart" title={`以下任务完成后自动开始:${blockedBy!.join("、")}`}>
|
|
139
|
+
{Icon.bolt()} {blockedBy!.length === 1 ? "1 个任务" : `${blockedBy!.length} 个任务`}完成后自动开始
|
|
140
|
+
</span>
|
|
141
|
+
) : (
|
|
142
|
+
<span className="bc-chip block" title={`以下任务完成前受阻:${blockedBy!.join("、")}`}>
|
|
143
|
+
{Icon.lock()} 受阻于 {blockedBy!.length === 1 ? "1 个任务" : `${blockedBy!.length} 个任务`}
|
|
144
|
+
</span>
|
|
145
|
+
))}
|
|
146
|
+
<span className="sp" />
|
|
147
|
+
{sessionCount > 0 && <span className="bc-sess" title={`${sessionCount} 个会话`}>{Icon.clock()} {sessionCount}</span>}
|
|
148
|
+
</div>
|
|
149
|
+
)}
|
|
150
|
+
</article>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function TaskBoard({ tasks, suggested, agents, selTaskId, running, blockedBy, canDrag, onSelect, onEditTask, onMove, onStartSuggestion, onAcceptSuggestion, onDismissSuggestion }: {
|
|
155
|
+
tasks: TaskRow[]; suggested: TaskRow[]; agents: AgentsBundle; selTaskId: string | null;
|
|
156
|
+
running: Set<string>; blockedBy: Map<string, string[]>;
|
|
157
|
+
// Dragging is disabled while a search filter is active: hidden cards would be
|
|
158
|
+
// silently dropped from the persisted order.
|
|
159
|
+
canDrag: boolean;
|
|
160
|
+
onSelect: (id: string) => void; onEditTask: (id: string) => void;
|
|
161
|
+
onMove: (id: string, patch: Partial<Pick<TaskRow, "status" | "suggested">>, orderedIds: string[]) => void;
|
|
162
|
+
onStartSuggestion: (id: string) => void; onAcceptSuggestion: (id: string) => void; onDismissSuggestion: (id: string) => void;
|
|
163
|
+
}) {
|
|
164
|
+
const [dragId, setDragId] = useState<string | null>(null);
|
|
165
|
+
const [over, setOver] = useState<{ col: ColKey; index: number } | null>(null);
|
|
166
|
+
// Terminal columns past MINI_CAP rows collapse under a veil until expanded.
|
|
167
|
+
const [showAll, setShowAll] = useState<Record<string, boolean>>({});
|
|
168
|
+
const all = [...suggested, ...tasks];
|
|
169
|
+
const dragTask = dragId ? all.find((x) => x.id === dragId) : undefined;
|
|
170
|
+
// Position-order membership per column — the source of truth for drag math.
|
|
171
|
+
const cols = new Map<ColKey, TaskRow[]>(COL_ORDER.map((k) => [k, all.filter(COLS[k].member)]));
|
|
172
|
+
const reset = () => { setDragId(null); setOver(null); };
|
|
173
|
+
|
|
174
|
+
const drop = (colKey: ColKey, index: number) => {
|
|
175
|
+
const t = dragId ? all.find((x) => x.id === dragId) : undefined;
|
|
176
|
+
reset();
|
|
177
|
+
if (!t) return;
|
|
178
|
+
const patch = COLS[colKey].patchFor(t);
|
|
179
|
+
if (patch === null) return; // column rejects this card
|
|
180
|
+
// Rebuild every column's id list with the card removed, insert it at the
|
|
181
|
+
// drop index (computed against the pre-removal list, so dragging downward
|
|
182
|
+
// lands after the hovered card — same feel as the projects sidebar), then
|
|
183
|
+
// flatten in column order into the project's new manual order.
|
|
184
|
+
const lists = new Map<ColKey, string[]>(
|
|
185
|
+
COL_ORDER.map((k) => [k, cols.get(k)!.map((x) => x.id).filter((id) => id !== t.id)])
|
|
186
|
+
);
|
|
187
|
+
const destination = lists.get(colKey)!;
|
|
188
|
+
destination.splice(Math.min(index, destination.length), 0, t.id);
|
|
189
|
+
const orderedIds = COL_ORDER.flatMap((k) => lists.get(k)!);
|
|
190
|
+
onMove(t.id, patch, orderedIds);
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
// On hold / Cancelled aren't part of the core flow: at rest they only appear
|
|
194
|
+
// when something is in them — but while a drag they'd accept is live, they
|
|
195
|
+
// materialise as empty drop bays so every status stays reachable without
|
|
196
|
+
// permanent column bloat. Bays are APPENDED after the resting columns:
|
|
197
|
+
// splicing them into their canonical slot would shift the columns to their
|
|
198
|
+
// right mid-drag, moving the drop target out from under the cursor.
|
|
199
|
+
const resting = COL_ORDER.filter((k) => COLS[k].always || cols.get(k)!.length > 0);
|
|
200
|
+
const bays = COL_ORDER.filter(
|
|
201
|
+
(k) => !resting.includes(k) && !!dragTask && COLS[k].patchFor(dragTask) !== null
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
return (
|
|
205
|
+
<div className="board">
|
|
206
|
+
{[...resting, ...bays].map((key) => {
|
|
207
|
+
const def = COLS[key];
|
|
208
|
+
const colTasks = cols.get(key)!;
|
|
209
|
+
const accepts = !!dragTask && def.patchFor(dragTask) !== null;
|
|
210
|
+
const reject = !!dragTask && !accepts;
|
|
211
|
+
const isOver = over?.col === key;
|
|
212
|
+
// Terminal columns render newest-first in compact rows; ordering within
|
|
213
|
+
// them is meaningless, so drops append and no insertion line is shown.
|
|
214
|
+
const display = def.mini ? [...colTasks].sort((a, b) => b.updated_at - a.updated_at) : colTasks;
|
|
215
|
+
const expanded = !!showAll[key];
|
|
216
|
+
const visible = def.mini && !expanded ? display.slice(0, MINI_CAP) : display;
|
|
217
|
+
const hidden = display.length - visible.length;
|
|
218
|
+
let lastDay: string | null = null;
|
|
219
|
+
return (
|
|
220
|
+
<div
|
|
221
|
+
key={key}
|
|
222
|
+
className={`bcol k-${key} ${accepts && isOver ? "drag-over" : ""} ${reject ? "reject" : ""}`}
|
|
223
|
+
onDragOver={(e) => { e.preventDefault(); e.dataTransfer.dropEffect = accepts ? "move" : "none"; if (dragId) setOver({ col: key, index: colTasks.length }); }}
|
|
224
|
+
onDragLeave={(e) => { if (isOver && !e.currentTarget.contains(e.relatedTarget as Node)) setOver(null); }}
|
|
225
|
+
onDrop={(e) => { e.preventDefault(); drop(key, colTasks.length); }}
|
|
226
|
+
>
|
|
227
|
+
<div className="bcol-h">
|
|
228
|
+
<span className={`cn ${def.accent ? "needs-you" : ""}`}>{key === "suggested" && Icon.spark()}{def.label}</span>
|
|
229
|
+
<span className={`ct ${def.accent ? "needs-you" : ""}`}>{colTasks.length}</span>
|
|
230
|
+
<span className="sp" />
|
|
231
|
+
{def.derived && <span className="derived" title="反映智能体 / 会话状态 —— 只能把卡片拖出,不能拖入">推导</span>}
|
|
232
|
+
</div>
|
|
233
|
+
<div className="bcol-rule" />
|
|
234
|
+
{reject && isOver && (
|
|
235
|
+
<div className="b-nodrop">
|
|
236
|
+
<span className="x">{Icon.x()}</span>
|
|
237
|
+
无法放置到此处
|
|
238
|
+
{def.noDropWhy && <small>{def.noDropWhy}</small>}
|
|
239
|
+
</div>
|
|
240
|
+
)}
|
|
241
|
+
<div className="bcol-body">
|
|
242
|
+
{visible.map((t, i) => {
|
|
243
|
+
const day = def.mini && key === "done" ? dayBucket(t.updated_at) : null;
|
|
244
|
+
const divider = day !== null && day !== lastDay ? <div className="b-day" key={`day-${day}`}>{day}<i /></div> : null;
|
|
245
|
+
lastDay = day;
|
|
246
|
+
return (
|
|
247
|
+
<div className="b-slot" key={t.id}>
|
|
248
|
+
{divider}
|
|
249
|
+
{accepts && isOver && !def.mini && over!.index === i && <div className="b-dropline" />}
|
|
250
|
+
<BoardCard
|
|
251
|
+
task={t}
|
|
252
|
+
agents={agents}
|
|
253
|
+
selected={t.id === selTaskId}
|
|
254
|
+
running={running.has(t.id)}
|
|
255
|
+
blockedBy={blockedBy.get(t.id)}
|
|
256
|
+
mini={def.mini}
|
|
257
|
+
dragging={dragId === t.id}
|
|
258
|
+
canDrag={canDrag}
|
|
259
|
+
onSelect={() => (t.suggested ? onEditTask(t.id) : onSelect(t.id))}
|
|
260
|
+
onDragStart={() => setDragId(t.id)}
|
|
261
|
+
onDragOverCard={(e) => { e.preventDefault(); e.stopPropagation(); e.dataTransfer.dropEffect = accepts ? "move" : "none"; if (dragId) setOver({ col: key, index: def.mini ? colTasks.length : i }); }}
|
|
262
|
+
onDropOnCard={(e) => { e.preventDefault(); e.stopPropagation(); drop(key, def.mini ? colTasks.length : i); }}
|
|
263
|
+
onDragEnd={reset}
|
|
264
|
+
actions={t.suggested ? (
|
|
265
|
+
<div className="bsug-acts" onClick={(e) => e.stopPropagation()}>
|
|
266
|
+
<button className="go" onClick={() => onStartSuggestion(t.id)}>{Icon.play()} 开始</button>
|
|
267
|
+
<button onClick={() => onAcceptSuggestion(t.id)} title="加入列表,稍后再开始">{Icon.plus()} 添加</button>
|
|
268
|
+
<button className="no" onClick={() => onDismissSuggestion(t.id)} title="忽略">{Icon.x()}</button>
|
|
269
|
+
</div>
|
|
270
|
+
) : undefined}
|
|
271
|
+
/>
|
|
272
|
+
</div>
|
|
273
|
+
);
|
|
274
|
+
})}
|
|
275
|
+
{accepts && isOver && !def.mini && over!.index >= visible.length && visible.length > 0 && <div className="b-dropline" />}
|
|
276
|
+
{hidden > 0 && (
|
|
277
|
+
<button className="b-showall" onClick={() => setShowAll((s) => ({ ...s, [key]: true }))}>查看全部 {display.length} 个 →</button>
|
|
278
|
+
)}
|
|
279
|
+
{def.mini && expanded && display.length > MINI_CAP && (
|
|
280
|
+
<button className="b-showall" onClick={() => setShowAll((s) => ({ ...s, [key]: false }))}>收起</button>
|
|
281
|
+
)}
|
|
282
|
+
{visible.length === 0 && (
|
|
283
|
+
accepts ? <div className="b-emptydrop">拖到这里</div> : <div className="bcol-empty">空</div>
|
|
284
|
+
)}
|
|
285
|
+
</div>
|
|
286
|
+
</div>
|
|
287
|
+
);
|
|
288
|
+
})}
|
|
289
|
+
</div>
|
|
290
|
+
);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Full-workspace board shell (desktop): owns everything right of the projects
|
|
294
|
+
// sidebar — header with the List/Board toggle, the board, and (via `children`)
|
|
295
|
+
// the slide-over session panel + drawers the composition root mounts on top.
|
|
296
|
+
export function BoardWorkspace({ project, agents, tasks, suggested, selTaskId, running, blockedBy, loading, onSetView, onMoveTask, onSelectTask, onNewTask, onEditContext, onShowSessions, onEditTask, onStartSuggestion, onAcceptSuggestion, onDismissSuggestion, children }: {
|
|
297
|
+
project: ProjectRow; agents: AgentsBundle; tasks: TaskRow[]; suggested: TaskRow[]; selTaskId: string | null;
|
|
298
|
+
running: Set<string>; blockedBy: Map<string, string[]>; loading?: boolean;
|
|
299
|
+
onSetView: (v: TaskView) => void;
|
|
300
|
+
onMoveTask: (id: string, patch: Partial<Pick<TaskRow, "status" | "suggested">>, orderedIds: string[]) => void;
|
|
301
|
+
onSelectTask: (id: string) => void; onNewTask: () => void; onEditContext: () => void; onShowSessions: () => void;
|
|
302
|
+
onEditTask: (id: string) => void;
|
|
303
|
+
onStartSuggestion: (id: string) => void; onAcceptSuggestion: (id: string) => void; onDismissSuggestion: (id: string) => void;
|
|
304
|
+
children?: ReactNode;
|
|
305
|
+
}) {
|
|
306
|
+
const [query, setQuery] = useState("");
|
|
307
|
+
const q = query.trim().toLowerCase();
|
|
308
|
+
const match = (t: TaskRow) => !q || t.title.toLowerCase().includes(q) || (t.description ?? "").toLowerCase().includes(q);
|
|
309
|
+
const shown = tasks.filter(match);
|
|
310
|
+
const shownSuggested = suggested.filter(match);
|
|
311
|
+
const total = tasks.length;
|
|
312
|
+
return (
|
|
313
|
+
<div className="col board-ws">
|
|
314
|
+
<div className="bws-h">
|
|
315
|
+
<span className="bws-pic" style={{ background: project.color }}>{project.name[0]}</span>
|
|
316
|
+
<h1 className="bws-name">{project.name}</h1>
|
|
317
|
+
<span className="bws-count">{total} 个任务</span>
|
|
318
|
+
<span className="spacer" />
|
|
319
|
+
{total + suggested.length >= SEARCH_MIN && (
|
|
320
|
+
<div className="bws-search"><SearchBar value={query} onChange={setQuery} placeholder="搜索任务…" /></div>
|
|
321
|
+
)}
|
|
322
|
+
<button className="btn btn-line btn-sm" onClick={onShowSessions} title="该项目下运行过的智能体会话">{Icon.clock()} 会话</button>
|
|
323
|
+
<button className="btn btn-line btn-sm" onClick={onEditContext} title="编辑项目上下文">{Icon.edit()} 上下文</button>
|
|
324
|
+
<div className="bseg" role="tablist" aria-label="任务布局">
|
|
325
|
+
<button role="tab" aria-selected={false} onClick={() => onSetView("list")}>{Icon.list()} 列表</button>
|
|
326
|
+
<button className="on" role="tab" aria-selected>{Icon.board()} 看板</button>
|
|
327
|
+
</div>
|
|
328
|
+
<button className="btn btn-accent btn-sm" onClick={onNewTask}>{Icon.plus()} 任务</button>
|
|
329
|
+
</div>
|
|
330
|
+
{q && shown.length === 0 && shownSuggested.length === 0 && <div className="search-empty">没有匹配“{query.trim()}”的任务。</div>}
|
|
331
|
+
{loading ? (
|
|
332
|
+
<div className="board board-loading">
|
|
333
|
+
{["建议", "未开始", "进行中"].map((label) => (
|
|
334
|
+
<div className="bcol" key={label}>
|
|
335
|
+
<div className="bcol-h"><span className="cn">{label}</span></div>
|
|
336
|
+
<div className="bcol-rule" />
|
|
337
|
+
<div className="bcol-body"><div className="bcard skel-card"><div className="skel" style={{ width: "80%" }} /><div className="skel" style={{ width: "45%", marginTop: 8 }} /></div></div>
|
|
338
|
+
</div>
|
|
339
|
+
))}
|
|
340
|
+
</div>
|
|
341
|
+
) : (
|
|
342
|
+
<TaskBoard
|
|
343
|
+
tasks={shown} suggested={shownSuggested} agents={agents} selTaskId={selTaskId}
|
|
344
|
+
running={running} blockedBy={blockedBy} canDrag={!q}
|
|
345
|
+
onSelect={onSelectTask} onEditTask={onEditTask} onMove={onMoveTask}
|
|
346
|
+
onStartSuggestion={onStartSuggestion} onAcceptSuggestion={onAcceptSuggestion} onDismissSuggestion={onDismissSuggestion}
|
|
347
|
+
/>
|
|
348
|
+
)}
|
|
349
|
+
{children}
|
|
350
|
+
</div>
|
|
351
|
+
);
|
|
352
|
+
}
|