@steerable/agent-shell-web 0.6.27 → 0.6.29
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/package.json +4 -4
- package/src/components/AgentSidebar.tsx +27 -27
- package/src/components/BrandLockup.tsx +20 -4
- package/src/components/ChatHeader.tsx +5 -15
- package/src/components/LocalLlmSettingsModal.tsx +5 -5
- package/src/components/TerminalPanel.tsx +1 -1
- package/src/components/chat/ApprovalModal.tsx +5 -5
- package/src/components/chat/AskUserQuestionMenu.tsx +13 -13
- package/src/components/chat/AssistantMessage.tsx +122 -36
- package/src/components/chat/ChatInput.tsx +34 -26
- package/src/components/chat/ExecPolicyPicker.tsx +4 -4
- package/src/components/chat/ExecutedActionsCard.tsx +398 -29
- package/src/components/chat/FilePathCode.tsx +2 -1
- package/src/components/chat/LocalChatPanel.tsx +42 -11
- package/src/components/chat/Markdown.tsx +15 -14
- package/src/components/chat/MessageList.tsx +23 -5
- package/src/components/chat/ModelPicker.tsx +6 -6
- package/src/components/chat/OrchestrationChildrenCard.tsx +69 -5
- package/src/components/chat/SessionTodoList.tsx +234 -0
- package/src/components/chat/SessionTreeModal.tsx +3 -3
- package/src/components/chat/SuggestedReplies.tsx +14 -5
- package/src/components/chat/TaskPanelModal.tsx +3 -3
- package/src/components/chat/TaskProcessPanel.tsx +67 -21
- package/src/components/chat/TurnFilesCard.tsx +2 -2
- package/src/components/chat/TurnProcessGroup.tsx +252 -99
- package/src/components/chat/UserMessage.tsx +7 -7
- package/src/components/chat/executed-actions-model.ts +469 -0
- package/src/components/chat/next-steps-tags.ts +28 -0
- package/src/components/chat/orchestration-children-model.ts +120 -0
- package/src/components/chat/process-status.ts +125 -53
- package/src/components/chat/todo-list-model.ts +167 -0
- package/src/components/chat/turn-timeline.ts +123 -11
- package/src/components/settings/AgentsSettingsPanel.tsx +2 -2
- package/src/components/settings/AppearanceSettingsPanel.tsx +3 -3
- package/src/components/settings/DiagnoseSettingsPanel.tsx +2 -2
- package/src/components/settings/InsightsSettingsPanel.tsx +4 -4
- package/src/components/settings/LlmSettingsPanel.tsx +10 -10
- package/src/components/settings/McpSettingsPanel.tsx +3 -3
- package/src/components/settings/ModelIdCombobox.tsx +2 -2
- package/src/components/settings/SecuritySettingsPanel.tsx +1 -1
- package/src/components/settings/SettingsSaveButton.tsx +1 -1
- package/src/components/settings/SkillsSettingsPanel.tsx +2 -2
- package/src/components/settings/TelemetrySettingsPanel.tsx +2 -2
- package/src/components/settings/UsagePanel.tsx +14 -10
- package/src/components/settings/WebSearchSettingsPanel.tsx +2 -2
- package/src/layouts/AgentLayout.tsx +3 -3
- package/src/lib/chat-transport.ts +68 -12
- package/src/lib/local-api.ts +11 -0
- package/src/lib/show-thinking-content.ts +4 -4
- package/src/pages/AgentPage.tsx +82 -11
- package/src/pages/ChatPanelPreviewPage.tsx +26 -4
- package/src/pages/SettingsPage.tsx +39 -39
- package/src/styles/index.css +12 -2
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
import { PlanTodoList } from './PlanTodoList';
|
|
21
21
|
import { FilePathCode } from './FilePathCode';
|
|
22
22
|
import { looksLikeFilePath } from './path-mentions';
|
|
23
|
+
import { stripNextStepsTags } from './next-steps-tags';
|
|
23
24
|
|
|
24
25
|
/**
|
|
25
26
|
* Markdown renderer shared by `UserMessage` and `AssistantMessage`.
|
|
@@ -59,25 +60,25 @@ const components: Components = {
|
|
|
59
60
|
h1: (props: ComponentPropsWithoutRef<'h1'>) => (
|
|
60
61
|
<h1
|
|
61
62
|
{...props}
|
|
62
|
-
className="mb-
|
|
63
|
+
className="mb-0.5 mt-1.5 text-xs font-semibold text-agent-foreground first:mt-0"
|
|
63
64
|
/>
|
|
64
65
|
),
|
|
65
66
|
h2: (props: ComponentPropsWithoutRef<'h2'>) => (
|
|
66
67
|
<h2
|
|
67
68
|
{...props}
|
|
68
|
-
className="mb-
|
|
69
|
+
className="mb-0.5 mt-1.5 text-xs font-semibold text-agent-foreground first:mt-0"
|
|
69
70
|
/>
|
|
70
71
|
),
|
|
71
72
|
h3: (props: ComponentPropsWithoutRef<'h3'>) => (
|
|
72
73
|
<h3
|
|
73
74
|
{...props}
|
|
74
|
-
className="mb-
|
|
75
|
+
className="mb-0.5 mt-1.5 text-xs font-semibold text-agent-foreground first:mt-0"
|
|
75
76
|
/>
|
|
76
77
|
),
|
|
77
78
|
h4: (props: ComponentPropsWithoutRef<'h4'>) => (
|
|
78
79
|
<h4
|
|
79
80
|
{...props}
|
|
80
|
-
className="mb-
|
|
81
|
+
className="mb-0.5 mt-1.5 text-xs font-semibold text-agent-foreground first:mt-0"
|
|
81
82
|
/>
|
|
82
83
|
),
|
|
83
84
|
|
|
@@ -85,16 +86,16 @@ const components: Components = {
|
|
|
85
86
|
<p
|
|
86
87
|
style={{ whiteSpace: 'pre-line' }}
|
|
87
88
|
{...props}
|
|
88
|
-
className="my-
|
|
89
|
+
className="my-0.5 leading-relaxed"
|
|
89
90
|
/>
|
|
90
91
|
),
|
|
91
92
|
|
|
92
93
|
// ── Lists ─────────────────────────────────────────────────────────────
|
|
93
94
|
ul: (props: ComponentPropsWithoutRef<'ul'>) => (
|
|
94
|
-
<ul {...props} className="my-1
|
|
95
|
+
<ul {...props} className="my-1 list-disc space-y-0.5 pl-4" />
|
|
95
96
|
),
|
|
96
97
|
ol: (props: ComponentPropsWithoutRef<'ol'>) => (
|
|
97
|
-
<ol {...props} className="my-1
|
|
98
|
+
<ol {...props} className="my-1 list-decimal space-y-0.5 pl-4" />
|
|
98
99
|
),
|
|
99
100
|
li: (props: ComponentPropsWithoutRef<'li'>) => (
|
|
100
101
|
<li
|
|
@@ -115,18 +116,18 @@ const components: Components = {
|
|
|
115
116
|
blockquote: (props: ComponentPropsWithoutRef<'blockquote'>) => (
|
|
116
117
|
<blockquote
|
|
117
118
|
{...props}
|
|
118
|
-
className="my-1
|
|
119
|
+
className="my-1 border-l-2 border-agent-border bg-agent-muted/40 px-2 py-0.5 text-xs text-agent-muted-foreground [&>p]:my-0"
|
|
119
120
|
/>
|
|
120
121
|
),
|
|
121
122
|
|
|
122
123
|
// ── Horizontal rule ──────────────────────────────────────────────────
|
|
123
124
|
hr: (props: ComponentPropsWithoutRef<'hr'>) => (
|
|
124
|
-
<hr {...props} className="my-
|
|
125
|
+
<hr {...props} className="my-2 border-agent-border" />
|
|
125
126
|
),
|
|
126
127
|
|
|
127
128
|
// ── Tables ───────────────────────────────────────────────────────────
|
|
128
129
|
table: (props: ComponentPropsWithoutRef<'table'>) => (
|
|
129
|
-
<div className="my-
|
|
130
|
+
<div className="my-1.5 w-full overflow-x-auto rounded-md border border-agent-border">
|
|
130
131
|
<table {...props} className="w-full border-collapse text-[12px]" />
|
|
131
132
|
</div>
|
|
132
133
|
),
|
|
@@ -136,13 +137,13 @@ const components: Components = {
|
|
|
136
137
|
th: (props: ComponentPropsWithoutRef<'th'>) => (
|
|
137
138
|
<th
|
|
138
139
|
{...props}
|
|
139
|
-
className="border-b border-agent-border px-2 py-1
|
|
140
|
+
className="border-b border-agent-border px-2 py-1 text-left font-medium text-agent-foreground"
|
|
140
141
|
/>
|
|
141
142
|
),
|
|
142
143
|
td: (props: ComponentPropsWithoutRef<'td'>) => (
|
|
143
144
|
<td
|
|
144
145
|
{...props}
|
|
145
|
-
className="border-b border-agent-border px-2 py-1
|
|
146
|
+
className="border-b border-agent-border px-2 py-1 align-top break-words"
|
|
146
147
|
style={{ maxWidth: '300px' }}
|
|
147
148
|
/>
|
|
148
149
|
),
|
|
@@ -159,7 +160,7 @@ const components: Components = {
|
|
|
159
160
|
return (
|
|
160
161
|
<pre
|
|
161
162
|
{...props}
|
|
162
|
-
className="my-
|
|
163
|
+
className="my-1.5 overflow-x-auto rounded-md border border-agent-border bg-agent-muted/60 p-2 text-[12px] leading-relaxed text-agent-foreground"
|
|
163
164
|
/>
|
|
164
165
|
);
|
|
165
166
|
},
|
|
@@ -453,7 +454,7 @@ export function Markdown({
|
|
|
453
454
|
rehypePlugins={[rehypeRaw]}
|
|
454
455
|
components={finalComponents}
|
|
455
456
|
>
|
|
456
|
-
{children}
|
|
457
|
+
{stripNextStepsTags(children)}
|
|
457
458
|
</ReactMarkdown>
|
|
458
459
|
);
|
|
459
460
|
}
|
|
@@ -15,11 +15,13 @@ import { InterruptedTurnCard } from './InterruptedTurnCard';
|
|
|
15
15
|
import { TaskOutcomeCards } from './TaskOutcomeCards';
|
|
16
16
|
import { SuggestedReplies } from './SuggestedReplies';
|
|
17
17
|
import type { ExecutedAction } from './ExecutedActionsCard';
|
|
18
|
+
import type { InspectTaskInput } from './executed-actions-model';
|
|
18
19
|
import type { ChildInfo } from './OrchestrationChildrenCard';
|
|
19
20
|
import type { ChatMode } from './ChatInput';
|
|
20
21
|
import type { TurnBlock } from './turn-timeline';
|
|
21
22
|
import type { TurnFile } from './turn-files';
|
|
22
23
|
import { inferDurationMs, readPersistedDurationMs } from './elapsed';
|
|
24
|
+
import type { LlmSpeedSnapshot } from './process-status';
|
|
23
25
|
|
|
24
26
|
/**
|
|
25
27
|
* MessageList — scrolling viewport that renders user/assistant message
|
|
@@ -80,6 +82,10 @@ interface MessageListProps {
|
|
|
80
82
|
currentTurnStartedAtMs?: number;
|
|
81
83
|
/** Frozen duration keyed by assistant message id (live freeze + history). */
|
|
82
84
|
durationByMessageId?: Record<string, number>;
|
|
85
|
+
/** Live model-request speed for the in-flight assistant turn. */
|
|
86
|
+
currentLlmSpeed?: LlmSpeedSnapshot;
|
|
87
|
+
/** Frozen model-request speed keyed by assistant message id. */
|
|
88
|
+
llmSpeedByMessageId?: Record<string, LlmSpeedSnapshot>;
|
|
83
89
|
/**
|
|
84
90
|
* 回合产物文件列表,按落库消息 id 键控(live 回合在 message_id 事件时
|
|
85
91
|
* 归档;历史回合从 messageMetadata.turnFiles 水合)。
|
|
@@ -129,7 +135,7 @@ interface MessageListProps {
|
|
|
129
135
|
*/
|
|
130
136
|
finishedTasks?: LocalTask[];
|
|
131
137
|
/** 点「查看过程」:在右侧栏打开该任务的推理过程。 */
|
|
132
|
-
onInspectTask?: (task:
|
|
138
|
+
onInspectTask?: (task: InspectTaskInput) => void;
|
|
133
139
|
/** 点「忽略」:隐藏这条终态通知(本次挂载内,不落库)。 */
|
|
134
140
|
onDismissFinishedTask?: (taskId: string) => void;
|
|
135
141
|
/** 分享当前对话(截图)。只画在最近一条助手消息的时间戳行上。 */
|
|
@@ -155,6 +161,8 @@ export function MessageList({
|
|
|
155
161
|
currentTurnTimeline,
|
|
156
162
|
currentTurnStartedAtMs,
|
|
157
163
|
durationByMessageId,
|
|
164
|
+
currentLlmSpeed,
|
|
165
|
+
llmSpeedByMessageId,
|
|
158
166
|
turnFilesByMessageId,
|
|
159
167
|
currentTurnFiles,
|
|
160
168
|
currentTurnChildren,
|
|
@@ -241,12 +249,12 @@ export function MessageList({
|
|
|
241
249
|
<div className="relative flex-1 overflow-hidden">
|
|
242
250
|
<div
|
|
243
251
|
ref={containerRef}
|
|
244
|
-
className="h-full overflow-y-auto overflow-anchor-none px-3
|
|
252
|
+
className="h-full overflow-y-auto overflow-anchor-none px-3 pt-3 pb-6"
|
|
245
253
|
>
|
|
246
254
|
{visibleMessages.length === 0 ? (
|
|
247
255
|
emptyState ?? null
|
|
248
256
|
) : (
|
|
249
|
-
<div className="mx-auto w-full space-y-
|
|
257
|
+
<div className="mx-auto w-full max-w-3xl space-y-4">
|
|
250
258
|
{visibleMessages.map((message, index) => {
|
|
251
259
|
const isLast = index === visibleMessages.length - 1;
|
|
252
260
|
if (message.role === 'user') {
|
|
@@ -299,10 +307,12 @@ export function MessageList({
|
|
|
299
307
|
typeof message.messageMetadata === 'string'
|
|
300
308
|
? message.messageMetadata
|
|
301
309
|
: undefined;
|
|
310
|
+
let previousUser: ChatMessage | undefined;
|
|
302
311
|
let previousUserCreatedAt: string | undefined;
|
|
303
312
|
for (let i = index - 1; i >= 0; i -= 1) {
|
|
304
313
|
if (visibleMessages[i].role === 'user') {
|
|
305
|
-
|
|
314
|
+
previousUser = visibleMessages[i];
|
|
315
|
+
previousUserCreatedAt = previousUser.createdAt;
|
|
306
316
|
break;
|
|
307
317
|
}
|
|
308
318
|
}
|
|
@@ -311,6 +321,11 @@ export function MessageList({
|
|
|
311
321
|
: durationByMessageId?.[message.id]
|
|
312
322
|
?? readPersistedDurationMs(metadataJson)
|
|
313
323
|
?? inferDurationMs(previousUserCreatedAt, message.createdAt);
|
|
324
|
+
const persistedSpeed = llmSpeedByMessageId?.[message.id];
|
|
325
|
+
const isSpeedTail = isLast && currentLlmSpeed !== undefined;
|
|
326
|
+
const llmSpeed =
|
|
327
|
+
persistedSpeed
|
|
328
|
+
?? (isStreamingTail || isSpeedTail ? currentLlmSpeed : undefined);
|
|
314
329
|
|
|
315
330
|
return (
|
|
316
331
|
<div key={message.id}>
|
|
@@ -324,13 +339,16 @@ export function MessageList({
|
|
|
324
339
|
executedActions={actions}
|
|
325
340
|
timeline={turnTimeline}
|
|
326
341
|
orchestrationChildren={childList}
|
|
342
|
+
previousUser={previousUser}
|
|
327
343
|
currentRound={isStreamingTail ? currentRound : undefined}
|
|
328
344
|
isPlanMode={isPlanMode}
|
|
329
345
|
onRegenerate={onRegenerate}
|
|
330
346
|
startedAtMs={isStreamingTail ? currentTurnStartedAtMs : undefined}
|
|
331
347
|
durationMs={durationMs}
|
|
348
|
+
llmSpeed={llmSpeed}
|
|
332
349
|
turnFiles={turnFiles}
|
|
333
350
|
onShare={message.id === lastAssistantId ? onShare : undefined}
|
|
351
|
+
onInspectTask={onInspectTask}
|
|
334
352
|
/>
|
|
335
353
|
{!isStreaming &&
|
|
336
354
|
message.id === lastAssistantId &&
|
|
@@ -376,7 +394,7 @@ export function MessageList({
|
|
|
376
394
|
setIsAtBottom(true);
|
|
377
395
|
scrollToBottom('smooth');
|
|
378
396
|
}}
|
|
379
|
-
className="absolute bottom-
|
|
397
|
+
className="absolute bottom-2 right-2 flex h-7 w-7 items-center justify-center rounded-full border border-agent-border bg-agent-canvas text-agent-foreground shadow-md transition-colors hover:bg-agent-foreground/5"
|
|
380
398
|
title="回到底部"
|
|
381
399
|
aria-label="回到底部"
|
|
382
400
|
>
|
|
@@ -117,7 +117,7 @@ export function ModelPicker({
|
|
|
117
117
|
if (next && status !== 'live' && !loading) void refresh();
|
|
118
118
|
}}
|
|
119
119
|
disabled={disabled}
|
|
120
|
-
className="inline-flex h-
|
|
120
|
+
className="inline-flex h-6 min-w-0 max-w-[160px] items-center gap-1 rounded-full border border-agent-border bg-agent-canvas px-1.5 text-[12px] leading-[1.45] text-agent-foreground transition-colors hover:bg-agent-foreground/5 disabled:cursor-not-allowed disabled:opacity-70 @sm:max-w-[220px]"
|
|
121
121
|
title={
|
|
122
122
|
status === 'offline'
|
|
123
123
|
? `模型目录不可用${catalog?.error ? `:${catalog.error}` : ''}(仍可手动指定模型)`
|
|
@@ -165,7 +165,7 @@ export function ModelPicker({
|
|
|
165
165
|
setModelMenuOpen(false);
|
|
166
166
|
}}
|
|
167
167
|
className={[
|
|
168
|
-
'flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-
|
|
168
|
+
'flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-[12px] leading-[1.45] transition-colors',
|
|
169
169
|
model == null
|
|
170
170
|
? 'bg-agent-foreground/10 text-agent-foreground'
|
|
171
171
|
: 'text-agent-muted-foreground hover:bg-agent-foreground/5 hover:text-agent-foreground',
|
|
@@ -192,7 +192,7 @@ export function ModelPicker({
|
|
|
192
192
|
setModelMenuOpen(false);
|
|
193
193
|
}}
|
|
194
194
|
className={[
|
|
195
|
-
'flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-
|
|
195
|
+
'flex w-full items-center gap-2 rounded px-2 py-1.5 text-left text-[12px] leading-[1.45] transition-colors',
|
|
196
196
|
isActive
|
|
197
197
|
? 'bg-agent-foreground/10 text-agent-foreground'
|
|
198
198
|
: 'text-agent-muted-foreground hover:bg-agent-foreground/5 hover:text-agent-foreground',
|
|
@@ -232,7 +232,7 @@ export function ModelPicker({
|
|
|
232
232
|
setModelMenuOpen(false);
|
|
233
233
|
}}
|
|
234
234
|
disabled={disabled}
|
|
235
|
-
className="inline-flex h-
|
|
235
|
+
className="inline-flex h-6 items-center gap-1 rounded-full border border-agent-border bg-agent-canvas px-1.5 text-[12px] leading-[1.45] text-agent-foreground transition-colors hover:bg-agent-foreground/5 disabled:cursor-not-allowed disabled:opacity-70"
|
|
236
236
|
title="推理档位(随每轮请求下发;模型不支持会直接报错)"
|
|
237
237
|
aria-haspopup="menu"
|
|
238
238
|
aria-expanded={effortMenuOpen}
|
|
@@ -253,7 +253,7 @@ export function ModelPicker({
|
|
|
253
253
|
setEffortMenuOpen(false);
|
|
254
254
|
}}
|
|
255
255
|
className={[
|
|
256
|
-
'flex w-full rounded px-2 py-1.5 text-left text-
|
|
256
|
+
'flex w-full rounded px-2 py-1.5 text-left text-[12px] leading-[1.45] transition-colors',
|
|
257
257
|
reasoningEffort == null
|
|
258
258
|
? 'bg-agent-foreground/10 text-agent-foreground'
|
|
259
259
|
: 'text-agent-muted-foreground hover:bg-agent-foreground/5 hover:text-agent-foreground',
|
|
@@ -271,7 +271,7 @@ export function ModelPicker({
|
|
|
271
271
|
setEffortMenuOpen(false);
|
|
272
272
|
}}
|
|
273
273
|
className={[
|
|
274
|
-
'flex w-full rounded px-2 py-1.5 text-left text-
|
|
274
|
+
'flex w-full rounded px-2 py-1.5 text-left text-[12px] leading-[1.45] transition-colors',
|
|
275
275
|
reasoningEffort === level
|
|
276
276
|
? 'bg-agent-foreground/10 text-agent-foreground'
|
|
277
277
|
: 'text-agent-muted-foreground hover:bg-agent-foreground/5 hover:text-agent-foreground',
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
import { useMemo } from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
OrchestrationPlanCard,
|
|
4
|
+
ORCHESTRATION_DOT_CLASS,
|
|
5
|
+
ORCHESTRATION_STATUS_LABEL,
|
|
6
|
+
} from '@steerable/agent-ui/cards';
|
|
3
7
|
import {
|
|
4
8
|
childrenToCardModel,
|
|
9
|
+
findAgentForProfile,
|
|
10
|
+
shouldShowOrchestrationBoard,
|
|
5
11
|
type ChildInfo,
|
|
6
12
|
} from './orchestration-children-model';
|
|
13
|
+
import type { LocalChatAgent } from '@/lib/local-api';
|
|
7
14
|
|
|
8
15
|
/**
|
|
9
16
|
* OrchestrationChildrenCard — the desktop surface of the framework's P3.1
|
|
@@ -13,25 +20,82 @@ import {
|
|
|
13
20
|
* payload is synthesized from spawn events, so there is no separate "plan"
|
|
14
21
|
* step: the plan IS the set of spawned children.
|
|
15
22
|
*
|
|
23
|
+
* Rows are overridden (`renderTaskRow`) to add the agent's colored initial
|
|
24
|
+
* next to its display name, matching the `@提及` chip in the composer and in
|
|
25
|
+
* user bubbles. Status labels and dot colors come from the card's own
|
|
26
|
+
* exported tables so the two surfaces cannot drift.
|
|
27
|
+
*
|
|
16
28
|
* The mapping logic lives in `./orchestration-children-model` (pure,
|
|
17
29
|
* runtime-dependency-free) so node-side tests do not pull the agent-ui
|
|
18
|
-
* dist build.
|
|
30
|
+
* dist build. A single delegate child is not rendered here: the tool row
|
|
31
|
+
* already shows 「委派 ·」.
|
|
32
|
+
*
|
|
33
|
+
* **Currently unmounted.** Nothing renders this card: every child is a
|
|
34
|
+
* `delegate_subagent` call, and that tool row carries the same progress
|
|
35
|
+
* (执行中 → 已完成) plus the expandable task brief and the child's report.
|
|
36
|
+
* `ChildInfo` and the event fold stay in use — they feed the turn's agent
|
|
37
|
+
* badges — so putting the board back is one JSX line in `AssistantMessage`.
|
|
19
38
|
*/
|
|
20
39
|
|
|
21
40
|
export type { ChildInfo } from './orchestration-children-model';
|
|
22
41
|
|
|
23
|
-
export function OrchestrationChildrenCard({
|
|
42
|
+
export function OrchestrationChildrenCard({
|
|
43
|
+
children,
|
|
44
|
+
agents = [],
|
|
45
|
+
}: {
|
|
46
|
+
children: ChildInfo[];
|
|
47
|
+
agents?: ReadonlyArray<Pick<LocalChatAgent, 'id' | 'slug' | 'name' | 'color'>>;
|
|
48
|
+
}) {
|
|
24
49
|
const { payload, taskStatuses } = useMemo(
|
|
25
50
|
() => childrenToCardModel(children),
|
|
26
51
|
[children],
|
|
27
52
|
);
|
|
53
|
+
const delegateOnly = children.every((child) => Boolean(child.profile));
|
|
28
54
|
|
|
29
|
-
if (children
|
|
55
|
+
if (!shouldShowOrchestrationBoard(children)) return null;
|
|
30
56
|
return (
|
|
31
57
|
<OrchestrationPlanCard
|
|
32
58
|
payload={payload}
|
|
33
59
|
taskStatuses={taskStatuses}
|
|
34
|
-
|
|
60
|
+
headerLabel={delegateOnly ? '子代理' : undefined}
|
|
61
|
+
hideMode={delegateOnly}
|
|
62
|
+
renderTaskRow={(task, status) => {
|
|
63
|
+
// 没有匹配的智能体:编排六件套的子代理(只有 lineage id)或被删掉的
|
|
64
|
+
// 智能体,显示画像原文。
|
|
65
|
+
const agent = findAgentForProfile(task.agentId, agents);
|
|
66
|
+
const displayName = agent?.name ?? task.agentId;
|
|
67
|
+
return (
|
|
68
|
+
<li className="flex items-start gap-2 text-sm">
|
|
69
|
+
<span
|
|
70
|
+
className={`mt-1.5 h-2 w-2 flex-shrink-0 rounded-full ${ORCHESTRATION_DOT_CLASS[status]}`}
|
|
71
|
+
aria-hidden
|
|
72
|
+
/>
|
|
73
|
+
<div className="min-w-0 flex-1">
|
|
74
|
+
<div className="flex items-center gap-2">
|
|
75
|
+
<span
|
|
76
|
+
data-agent-color-dot=""
|
|
77
|
+
className="inline-flex h-3.5 w-3.5 shrink-0 items-center justify-center rounded-full text-[8px] font-semibold text-white"
|
|
78
|
+
style={{ backgroundColor: agent?.color || '#7c3aed' }}
|
|
79
|
+
aria-hidden
|
|
80
|
+
>
|
|
81
|
+
{displayName.trim()[0]?.toUpperCase() ?? 'A'}
|
|
82
|
+
</span>
|
|
83
|
+
<span className="font-medium text-[var(--agent-foreground,#111827)]">
|
|
84
|
+
{displayName}
|
|
85
|
+
</span>
|
|
86
|
+
<span className="text-xs text-[var(--agent-muted-foreground,#6b7280)]">
|
|
87
|
+
{ORCHESTRATION_STATUS_LABEL[status]}
|
|
88
|
+
</span>
|
|
89
|
+
</div>
|
|
90
|
+
{task.prompt ? (
|
|
91
|
+
<p className="mt-0.5 line-clamp-2 text-xs text-[var(--agent-muted-foreground,#6b7280)]">
|
|
92
|
+
{task.prompt}
|
|
93
|
+
</p>
|
|
94
|
+
) : null}
|
|
95
|
+
</div>
|
|
96
|
+
</li>
|
|
97
|
+
);
|
|
98
|
+
}}
|
|
35
99
|
defaultExpanded
|
|
36
100
|
/>
|
|
37
101
|
);
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
LuCircle,
|
|
4
|
+
LuCircleCheck,
|
|
5
|
+
LuCircleDot,
|
|
6
|
+
LuChevronDown,
|
|
7
|
+
LuListChecks,
|
|
8
|
+
LuLoaderCircle,
|
|
9
|
+
LuX,
|
|
10
|
+
} from 'react-icons/lu';
|
|
11
|
+
import {
|
|
12
|
+
summarizeTodos,
|
|
13
|
+
type SessionTodo,
|
|
14
|
+
type TodoStatus,
|
|
15
|
+
} from './todo-list-model';
|
|
16
|
+
|
|
17
|
+
function StatusIcon({
|
|
18
|
+
status,
|
|
19
|
+
className,
|
|
20
|
+
}: {
|
|
21
|
+
status: TodoStatus;
|
|
22
|
+
className?: string;
|
|
23
|
+
}) {
|
|
24
|
+
const cls = ['h-3.5 w-3.5 shrink-0', className].filter(Boolean).join(' ');
|
|
25
|
+
if (status === 'completed') {
|
|
26
|
+
return <LuCircleCheck className={`${cls} text-emerald-600 dark:text-emerald-400`} />;
|
|
27
|
+
}
|
|
28
|
+
if (status === 'in_progress') {
|
|
29
|
+
return <LuCircleDot className={`${cls} text-blue-600 dark:text-blue-400`} />;
|
|
30
|
+
}
|
|
31
|
+
return <LuCircle className={`${cls} text-agent-muted-foreground/70`} />;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function currentTodoStep(todos: SessionTodo[]): SessionTodo | null {
|
|
35
|
+
return (
|
|
36
|
+
todos.find((todo) => todo.status === 'in_progress') ??
|
|
37
|
+
todos.find((todo) => todo.status === 'pending') ??
|
|
38
|
+
todos[todos.length - 1] ??
|
|
39
|
+
null
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function todoProgressCopy(todos: SessionTodo[]): string {
|
|
44
|
+
const summary = summarizeTodos(todos);
|
|
45
|
+
if (summary.total === 0) return '';
|
|
46
|
+
if (summary.completed === summary.total) {
|
|
47
|
+
return `${summary.completed}/${summary.total} 已完成`;
|
|
48
|
+
}
|
|
49
|
+
const currentIndex = todos.findIndex((todo) => todo.status === 'in_progress');
|
|
50
|
+
const step =
|
|
51
|
+
currentIndex >= 0 ? currentIndex + 1 : Math.min(summary.completed + 1, summary.total);
|
|
52
|
+
return `执行到 ${step}/${summary.total}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function TodoItems({
|
|
56
|
+
todos,
|
|
57
|
+
compact = false,
|
|
58
|
+
}: {
|
|
59
|
+
todos: SessionTodo[];
|
|
60
|
+
compact?: boolean;
|
|
61
|
+
}) {
|
|
62
|
+
return (
|
|
63
|
+
<ol
|
|
64
|
+
className={`m-0 list-none ${compact ? 'max-h-48 space-y-1 overflow-y-auto px-2.5 py-1.5' : 'space-y-1 px-2.5 py-1.5'}`}
|
|
65
|
+
>
|
|
66
|
+
{todos.map((todo) => {
|
|
67
|
+
const done = todo.status === 'completed';
|
|
68
|
+
const active = todo.status === 'in_progress';
|
|
69
|
+
return (
|
|
70
|
+
<li
|
|
71
|
+
key={todo.id}
|
|
72
|
+
data-testid={`todo-item-${todo.id}`}
|
|
73
|
+
data-status={todo.status}
|
|
74
|
+
className="flex items-start gap-2 text-[11px] leading-relaxed"
|
|
75
|
+
>
|
|
76
|
+
<StatusIcon status={todo.status} className="mt-[2px]" />
|
|
77
|
+
<span
|
|
78
|
+
className={
|
|
79
|
+
done
|
|
80
|
+
? 'text-agent-muted-foreground line-through'
|
|
81
|
+
: active
|
|
82
|
+
? 'font-medium text-agent-foreground'
|
|
83
|
+
: 'text-agent-muted-foreground/85'
|
|
84
|
+
}
|
|
85
|
+
>
|
|
86
|
+
{todo.content}
|
|
87
|
+
</span>
|
|
88
|
+
</li>
|
|
89
|
+
);
|
|
90
|
+
})}
|
|
91
|
+
</ol>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function SessionTodoList({ todos }: { todos: SessionTodo[] }) {
|
|
96
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
97
|
+
const current = useMemo(() => currentTodoStep(todos), [todos]);
|
|
98
|
+
const progressLabel = useMemo(() => todoProgressCopy(todos), [todos]);
|
|
99
|
+
|
|
100
|
+
const hasActive = useMemo(
|
|
101
|
+
() => todos.some((t) => t.status === 'in_progress'),
|
|
102
|
+
[todos],
|
|
103
|
+
);
|
|
104
|
+
const isAllCompleted = useMemo(() => {
|
|
105
|
+
const summary = summarizeTodos(todos);
|
|
106
|
+
return summary.total > 0 && summary.completed === summary.total;
|
|
107
|
+
}, [todos]);
|
|
108
|
+
|
|
109
|
+
// 执行中默认展开,全部完成默认折叠;允许用户手动点击收起或展开
|
|
110
|
+
const [userToggled, setUserToggled] = useState<boolean | null>(null);
|
|
111
|
+
const expanded = userToggled ?? hasActive;
|
|
112
|
+
|
|
113
|
+
// 当任务全部执行完毕时,自动收起为紧凑胶囊
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
if (isAllCompleted) {
|
|
116
|
+
setUserToggled(false);
|
|
117
|
+
}
|
|
118
|
+
}, [isAllCompleted]);
|
|
119
|
+
|
|
120
|
+
// 当新出现执行中的任务时,恢复默认展开
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
if (hasActive) {
|
|
123
|
+
setUserToggled(null);
|
|
124
|
+
}
|
|
125
|
+
}, [hasActive]);
|
|
126
|
+
|
|
127
|
+
// 任务全部完成后,如果展开了浮层,点击外部区域时自动收起
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
if (!expanded || !isAllCompleted) return;
|
|
130
|
+
const handlePointerDown = (event: PointerEvent) => {
|
|
131
|
+
if (
|
|
132
|
+
containerRef.current &&
|
|
133
|
+
!containerRef.current.contains(event.target as Node)
|
|
134
|
+
) {
|
|
135
|
+
setUserToggled(false);
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
document.addEventListener('pointerdown', handlePointerDown);
|
|
139
|
+
return () => document.removeEventListener('pointerdown', handlePointerDown);
|
|
140
|
+
}, [expanded, isAllCompleted]);
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<div
|
|
144
|
+
ref={containerRef}
|
|
145
|
+
className="relative inline-flex items-center"
|
|
146
|
+
data-testid="session-todo-list"
|
|
147
|
+
>
|
|
148
|
+
<button
|
|
149
|
+
type="button"
|
|
150
|
+
onClick={() => setUserToggled(!expanded)}
|
|
151
|
+
className={[
|
|
152
|
+
'flex h-6 max-w-[240px] sm:max-w-[320px] items-center gap-1.5 rounded-full px-2 text-left text-[11px] transition-all duration-200 select-none shadow-2xs',
|
|
153
|
+
isAllCompleted
|
|
154
|
+
? 'border border-emerald-500/25 bg-emerald-500/[0.04] text-emerald-700 dark:text-emerald-300 hover:bg-emerald-500/[0.08]'
|
|
155
|
+
: hasActive
|
|
156
|
+
? 'border border-blue-500/30 bg-blue-500/[0.06] text-blue-700 dark:text-blue-300 hover:bg-blue-500/10'
|
|
157
|
+
: 'border border-agent-border/80 bg-agent-canvas text-agent-muted-foreground hover:bg-agent-foreground/5 hover:text-agent-foreground',
|
|
158
|
+
].join(' ')}
|
|
159
|
+
aria-expanded={expanded}
|
|
160
|
+
>
|
|
161
|
+
{isAllCompleted ? (
|
|
162
|
+
<LuCircleCheck className="h-3.5 w-3.5 shrink-0 text-emerald-600 dark:text-emerald-400" />
|
|
163
|
+
) : hasActive ? (
|
|
164
|
+
<LuLoaderCircle className="h-3.5 w-3.5 shrink-0 animate-spin text-blue-600 dark:text-blue-400" />
|
|
165
|
+
) : current ? (
|
|
166
|
+
<StatusIcon status={current.status} />
|
|
167
|
+
) : (
|
|
168
|
+
<LuListChecks className="h-3.5 w-3.5 shrink-0 text-agent-foreground" />
|
|
169
|
+
)}
|
|
170
|
+
<span className="min-w-0 flex-1 truncate font-medium">
|
|
171
|
+
{isAllCompleted ? '任务清单' : (current?.content ?? '任务清单')}
|
|
172
|
+
</span>
|
|
173
|
+
{progressLabel ? (
|
|
174
|
+
<span
|
|
175
|
+
className={`shrink-0 rounded-full px-1.5 py-0.2 text-[10px] font-medium ${
|
|
176
|
+
isAllCompleted
|
|
177
|
+
? 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400'
|
|
178
|
+
: hasActive
|
|
179
|
+
? 'bg-blue-500/15 text-blue-700 dark:text-blue-300'
|
|
180
|
+
: 'bg-agent-muted text-agent-muted-foreground'
|
|
181
|
+
}`}
|
|
182
|
+
data-testid="todo-progress"
|
|
183
|
+
>
|
|
184
|
+
{progressLabel}
|
|
185
|
+
</span>
|
|
186
|
+
) : null}
|
|
187
|
+
<LuChevronDown
|
|
188
|
+
className={`h-3 w-3 shrink-0 text-current transition-transform duration-200 ${
|
|
189
|
+
expanded ? 'rotate-180' : ''
|
|
190
|
+
}`}
|
|
191
|
+
/>
|
|
192
|
+
</button>
|
|
193
|
+
|
|
194
|
+
{expanded && (
|
|
195
|
+
<div
|
|
196
|
+
className="absolute bottom-full right-0 mb-1.5 w-72 sm:w-80 overflow-hidden rounded-agent-lg border border-agent-border bg-agent-canvas/98 backdrop-blur-md shadow-xl z-40 animate-in fade-in slide-in-from-bottom-1 duration-150"
|
|
197
|
+
>
|
|
198
|
+
<div className="flex items-center justify-between border-b border-agent-border/60 bg-agent-muted/30 px-2.5 py-1.5">
|
|
199
|
+
<div className="flex items-center gap-1.5 text-[11px] font-medium text-agent-foreground">
|
|
200
|
+
<LuListChecks className="h-3.5 w-3.5 text-agent-foreground" />
|
|
201
|
+
<span>任务清单</span>
|
|
202
|
+
</div>
|
|
203
|
+
<div className="flex items-center gap-1.5">
|
|
204
|
+
{progressLabel && (
|
|
205
|
+
<span
|
|
206
|
+
className={`rounded-full px-1.5 py-0.2 text-[10px] font-medium ${
|
|
207
|
+
isAllCompleted
|
|
208
|
+
? 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400'
|
|
209
|
+
: 'bg-blue-500/10 text-blue-600 dark:text-blue-400'
|
|
210
|
+
}`}
|
|
211
|
+
>
|
|
212
|
+
{progressLabel}
|
|
213
|
+
</span>
|
|
214
|
+
)}
|
|
215
|
+
<button
|
|
216
|
+
type="button"
|
|
217
|
+
onClick={() => setUserToggled(false)}
|
|
218
|
+
className="flex h-5 w-5 items-center justify-center rounded text-agent-muted-foreground transition-colors hover:bg-agent-foreground/10 hover:text-agent-foreground"
|
|
219
|
+
title="收起清单"
|
|
220
|
+
aria-label="收起清单"
|
|
221
|
+
>
|
|
222
|
+
<LuX className="h-3 w-3" />
|
|
223
|
+
</button>
|
|
224
|
+
</div>
|
|
225
|
+
</div>
|
|
226
|
+
<TodoItems todos={todos} compact />
|
|
227
|
+
</div>
|
|
228
|
+
)}
|
|
229
|
+
</div>
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export default SessionTodoList;
|
|
234
|
+
|
|
@@ -136,9 +136,9 @@ export function SessionTreeModal({ chatId, onClose, onBranchSwitched }: SessionT
|
|
|
136
136
|
data-session-tree-modal
|
|
137
137
|
>
|
|
138
138
|
<div className="flex max-h-[85vh] w-full max-w-lg flex-col overflow-hidden rounded-agent-lg border border-agent-border bg-agent-canvas shadow-xl">
|
|
139
|
-
<div className="flex items-center gap-2 border-b border-agent-border px-
|
|
139
|
+
<div className="flex items-center gap-2 border-b border-agent-border px-3 py-2">
|
|
140
140
|
<LuGitBranch className="h-4 w-4 shrink-0 text-agent-muted-foreground" />
|
|
141
|
-
<span className="min-w-0 flex-1 truncate text-
|
|
141
|
+
<span className="min-w-0 flex-1 truncate text-xs font-medium text-agent-foreground">
|
|
142
142
|
会话分支树
|
|
143
143
|
{data && data.nodeCount > 1 && (
|
|
144
144
|
<span className="ml-1.5 text-xs font-normal text-agent-muted-foreground">
|
|
@@ -206,7 +206,7 @@ export function SessionTreeModal({ chatId, onClose, onBranchSwitched }: SessionT
|
|
|
206
206
|
</div>
|
|
207
207
|
|
|
208
208
|
{!loading && !isEmpty && (
|
|
209
|
-
<div className="border-t border-agent-border px-
|
|
209
|
+
<div className="border-t border-agent-border px-3 py-1.5 text-[10px] text-agent-muted-foreground/80">
|
|
210
210
|
{data?.truncated
|
|
211
211
|
? '分支过多,树已被截断(仅显示部分节点)。'
|
|
212
212
|
: '点击切换分支;↑↓ 移动,Enter 切换,Esc 关闭。'}
|