@steerable/agent-shell-web 0.6.28 → 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 +3 -3
- package/src/components/BrandLockup.tsx +20 -4
- package/src/components/chat/AssistantMessage.tsx +83 -25
- package/src/components/chat/ChatInput.tsx +11 -3
- package/src/components/chat/ExecutedActionsCard.tsx +389 -27
- package/src/components/chat/LocalChatPanel.tsx +12 -16
- package/src/components/chat/MessageList.tsx +9 -4
- package/src/components/chat/OrchestrationChildrenCard.tsx +69 -5
- package/src/components/chat/SessionTodoList.tsx +117 -20
- package/src/components/chat/SuggestedReplies.tsx +11 -3
- package/src/components/chat/TaskProcessPanel.tsx +62 -17
- package/src/components/chat/TurnProcessGroup.tsx +27 -1
- package/src/components/chat/UserMessage.tsx +5 -5
- package/src/components/chat/executed-actions-model.ts +469 -0
- package/src/components/chat/orchestration-children-model.ts +120 -0
- package/src/components/chat/turn-timeline.ts +23 -0
- package/src/lib/local-api.ts +11 -0
- package/src/pages/AgentPage.tsx +27 -6
|
@@ -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
|
);
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { useMemo, useState } from 'react';
|
|
1
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
2
2
|
import {
|
|
3
3
|
LuCircle,
|
|
4
4
|
LuCircleCheck,
|
|
5
5
|
LuCircleDot,
|
|
6
6
|
LuChevronDown,
|
|
7
|
-
LuChevronRight,
|
|
8
7
|
LuListChecks,
|
|
8
|
+
LuLoaderCircle,
|
|
9
|
+
LuX,
|
|
9
10
|
} from 'react-icons/lu';
|
|
10
11
|
import {
|
|
11
12
|
summarizeTodos,
|
|
@@ -60,7 +61,7 @@ export function TodoItems({
|
|
|
60
61
|
}) {
|
|
61
62
|
return (
|
|
62
63
|
<ol
|
|
63
|
-
className={`m-0 list-none ${compact ? 'max-h-
|
|
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'}`}
|
|
64
65
|
>
|
|
65
66
|
{todos.map((todo) => {
|
|
66
67
|
const done = todo.status === 'completed';
|
|
@@ -70,7 +71,7 @@ export function TodoItems({
|
|
|
70
71
|
key={todo.id}
|
|
71
72
|
data-testid={`todo-item-${todo.id}`}
|
|
72
73
|
data-status={todo.status}
|
|
73
|
-
className="flex items-start gap-2 text-[
|
|
74
|
+
className="flex items-start gap-2 text-[11px] leading-relaxed"
|
|
74
75
|
>
|
|
75
76
|
<StatusIcon status={todo.status} className="mt-[2px]" />
|
|
76
77
|
<span
|
|
@@ -79,7 +80,7 @@ export function TodoItems({
|
|
|
79
80
|
? 'text-agent-muted-foreground line-through'
|
|
80
81
|
: active
|
|
81
82
|
? 'font-medium text-agent-foreground'
|
|
82
|
-
: 'text-agent-foreground'
|
|
83
|
+
: 'text-agent-muted-foreground/85'
|
|
83
84
|
}
|
|
84
85
|
>
|
|
85
86
|
{todo.content}
|
|
@@ -92,46 +93,142 @@ export function TodoItems({
|
|
|
92
93
|
}
|
|
93
94
|
|
|
94
95
|
export function SessionTodoList({ todos }: { todos: SessionTodo[] }) {
|
|
95
|
-
const
|
|
96
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
96
97
|
const current = useMemo(() => currentTodoStep(todos), [todos]);
|
|
97
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]);
|
|
98
141
|
|
|
99
142
|
return (
|
|
100
143
|
<div
|
|
101
|
-
|
|
144
|
+
ref={containerRef}
|
|
145
|
+
className="relative inline-flex items-center"
|
|
102
146
|
data-testid="session-todo-list"
|
|
103
147
|
>
|
|
104
148
|
<button
|
|
105
149
|
type="button"
|
|
106
|
-
onClick={() =>
|
|
107
|
-
className=
|
|
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(' ')}
|
|
108
159
|
aria-expanded={expanded}
|
|
109
160
|
>
|
|
110
|
-
{
|
|
111
|
-
<
|
|
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} />
|
|
112
167
|
) : (
|
|
113
|
-
<LuChevronRight className="h-3.5 w-3.5 shrink-0 text-agent-muted-foreground" />
|
|
114
|
-
)}
|
|
115
|
-
{expanded || !current ? (
|
|
116
168
|
<LuListChecks className="h-3.5 w-3.5 shrink-0 text-agent-foreground" />
|
|
117
|
-
) : (
|
|
118
|
-
<StatusIcon status={current.status} />
|
|
119
169
|
)}
|
|
120
|
-
<span className="min-w-0 flex-1 truncate
|
|
121
|
-
{
|
|
170
|
+
<span className="min-w-0 flex-1 truncate font-medium">
|
|
171
|
+
{isAllCompleted ? '任务清单' : (current?.content ?? '任务清单')}
|
|
122
172
|
</span>
|
|
123
173
|
{progressLabel ? (
|
|
124
174
|
<span
|
|
125
|
-
className=
|
|
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
|
+
}`}
|
|
126
182
|
data-testid="todo-progress"
|
|
127
183
|
>
|
|
128
184
|
{progressLabel}
|
|
129
185
|
</span>
|
|
130
186
|
) : null}
|
|
187
|
+
<LuChevronDown
|
|
188
|
+
className={`h-3 w-3 shrink-0 text-current transition-transform duration-200 ${
|
|
189
|
+
expanded ? 'rotate-180' : ''
|
|
190
|
+
}`}
|
|
191
|
+
/>
|
|
131
192
|
</button>
|
|
132
|
-
|
|
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
|
+
)}
|
|
133
229
|
</div>
|
|
134
230
|
);
|
|
135
231
|
}
|
|
136
232
|
|
|
137
233
|
export default SessionTodoList;
|
|
234
|
+
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { LuSparkles } from 'react-icons/lu';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* WorkBuddy-style follow-up chips: next-turn user inputs rendered under the
|
|
3
5
|
* latest assistant reply. Source is the model's judgment of `[next_steps]`
|
|
@@ -15,18 +17,23 @@ export function SuggestedReplies({
|
|
|
15
17
|
|
|
16
18
|
return (
|
|
17
19
|
<div
|
|
18
|
-
className="mx-auto flex w-full max-w-[var(--chat-input-box-width)] flex-
|
|
20
|
+
className="mx-auto mt-2.5 flex w-full max-w-[var(--chat-input-box-width)] flex-wrap items-center gap-1.5 px-1 pb-0.5"
|
|
19
21
|
data-testid="suggested-replies"
|
|
20
22
|
>
|
|
23
|
+
<div className="mr-0.5 inline-flex items-center gap-1 text-[11px] font-medium text-agent-muted-foreground/70 select-none">
|
|
24
|
+
<LuSparkles className="h-3 w-3 text-agent-muted-foreground/80" />
|
|
25
|
+
<span>建议回复</span>
|
|
26
|
+
</div>
|
|
21
27
|
{suggestions.map((text) => (
|
|
22
28
|
<button
|
|
23
29
|
key={text}
|
|
24
30
|
type="button"
|
|
25
31
|
data-testid="suggested-reply"
|
|
26
32
|
onClick={() => onSelect(text)}
|
|
27
|
-
|
|
33
|
+
title={text}
|
|
34
|
+
className="inline-flex max-w-[280px] sm:max-w-[340px] items-center rounded-full border border-agent-border/80 bg-agent-canvas px-2.5 py-1 text-left text-[12px] leading-[1.4] text-agent-muted-foreground transition-all duration-150 hover:border-agent-foreground/30 hover:bg-agent-foreground/5 hover:text-agent-foreground shadow-2xs active:scale-[0.98]"
|
|
28
35
|
>
|
|
29
|
-
{text}
|
|
36
|
+
<span className="truncate">{text}</span>
|
|
30
37
|
</button>
|
|
31
38
|
))}
|
|
32
39
|
</div>
|
|
@@ -34,3 +41,4 @@ export function SuggestedReplies({
|
|
|
34
41
|
}
|
|
35
42
|
|
|
36
43
|
export default SuggestedReplies;
|
|
44
|
+
|
|
@@ -1,18 +1,34 @@
|
|
|
1
1
|
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
2
2
|
import { LuArrowDown, LuLoaderCircle, LuTerminal } from 'react-icons/lu';
|
|
3
3
|
import { DockHeaderButton, DockPanelHeader } from '@/components/DockPanelHeader';
|
|
4
|
-
import { getTaskProcess } from '@/lib/local-api';
|
|
4
|
+
import { getChildProcess, getTaskProcess } from '@/lib/local-api';
|
|
5
5
|
import { getElectronBridge } from '@/lib/electron-bridge';
|
|
6
6
|
import { Markdown } from '@/components/chat/Markdown';
|
|
7
7
|
import { TurnProcessGroup } from '@/components/chat/TurnProcessGroup';
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
parseTurnBlocks,
|
|
10
|
+
timelineContentSignature,
|
|
11
|
+
type TurnBlock,
|
|
12
|
+
} from '@/components/chat/turn-timeline';
|
|
9
13
|
|
|
10
14
|
export interface InspectedTask {
|
|
11
15
|
id: string;
|
|
12
16
|
chatId: string;
|
|
13
17
|
title: string;
|
|
18
|
+
/**
|
|
19
|
+
* 子代理过程:按它自己的 durable record 读,而不是任务表。设了它就走
|
|
20
|
+
* `GET /child-process`,`live` 期间轮询(子回合边跑边写 record)。
|
|
21
|
+
*/
|
|
22
|
+
recordId?: string;
|
|
23
|
+
/** 目标仍在运行——决定轮询与标题状态。 */
|
|
24
|
+
live?: boolean;
|
|
14
25
|
}
|
|
15
26
|
|
|
27
|
+
const CHILD_POLL_INTERVAL_MS = 1500;
|
|
28
|
+
|
|
29
|
+
/** 连续这么多次轮询读到同一份内容就认为子代理跑完了(≈60s)。 */
|
|
30
|
+
const CHILD_IDLE_POLLS_UNTIL_DONE = 40;
|
|
31
|
+
|
|
16
32
|
const NEAR_BOTTOM_THRESHOLD_PX = 100;
|
|
17
33
|
|
|
18
34
|
/**
|
|
@@ -74,27 +90,46 @@ export function TaskProcessPanel({
|
|
|
74
90
|
|
|
75
91
|
// live 增量在 paint 前钉住底部。smooth scroll 会先露出一帧半截内容再往下
|
|
76
92
|
// 滑,Windows 经典滚动条就会上下跳。
|
|
77
|
-
const timelineSig = blocks
|
|
78
|
-
.map((block) =>
|
|
79
|
-
block.type === 'tools' ? `t${block.actions.length}` : `c${block.content.length}`,
|
|
80
|
-
)
|
|
81
|
-
.join('|');
|
|
93
|
+
const timelineSig = timelineContentSignature(blocks);
|
|
82
94
|
useLayoutEffect(() => {
|
|
83
95
|
if (!isAtBottomRef.current) return;
|
|
84
96
|
scrollToBottom('auto');
|
|
85
97
|
}, [timelineSig, live, scrollToBottom]);
|
|
86
98
|
|
|
99
|
+
const childRecordId = inspected.recordId;
|
|
100
|
+
|
|
87
101
|
useEffect(() => {
|
|
88
102
|
let cancelled = false;
|
|
103
|
+
let timer: number | undefined;
|
|
104
|
+
let lastSig: string | null = null;
|
|
105
|
+
let idlePolls = 0;
|
|
89
106
|
setLoading(true);
|
|
90
107
|
setError(null);
|
|
91
|
-
|
|
108
|
+
const load = async () => {
|
|
92
109
|
try {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
110
|
+
if (childRecordId) {
|
|
111
|
+
const snapshot = await getChildProcess(childRecordId);
|
|
112
|
+
if (cancelled) return;
|
|
113
|
+
const next = parseTurnBlocks(snapshot.timeline) ?? [];
|
|
114
|
+
const sig = timelineContentSignature(next);
|
|
115
|
+
idlePolls = sig === lastSig ? idlePolls + 1 : 0;
|
|
116
|
+
lastSig = sig;
|
|
117
|
+
setBlocks(next);
|
|
118
|
+
// 子代理跑完不会再有事件通知这个面板(它的 live 是点开那刻的快照),
|
|
119
|
+
// 所以用「record 不再变化」当结束信号:停轮询、标题转「已结束」。
|
|
120
|
+
if (timer !== undefined && idlePolls >= CHILD_IDLE_POLLS_UNTIL_DONE) {
|
|
121
|
+
window.clearInterval(timer);
|
|
122
|
+
timer = undefined;
|
|
123
|
+
setLive(false);
|
|
124
|
+
}
|
|
125
|
+
setStale(false);
|
|
126
|
+
} else {
|
|
127
|
+
const snapshot = await getTaskProcess(inspected.id);
|
|
128
|
+
if (cancelled) return;
|
|
129
|
+
setBlocks(parseTurnBlocks(snapshot.timeline) ?? []);
|
|
130
|
+
setLive(snapshot.live);
|
|
131
|
+
setStale(snapshot.stale);
|
|
132
|
+
}
|
|
98
133
|
} catch (err) {
|
|
99
134
|
if (cancelled) return;
|
|
100
135
|
setError(err instanceof Error ? err.message : String(err));
|
|
@@ -102,13 +137,22 @@ export function TaskProcessPanel({
|
|
|
102
137
|
} finally {
|
|
103
138
|
if (!cancelled) setLoading(false);
|
|
104
139
|
}
|
|
105
|
-
}
|
|
140
|
+
};
|
|
141
|
+
if (childRecordId) setLive(Boolean(inspected.live));
|
|
142
|
+
void load();
|
|
143
|
+
// 子代理没有 task-process 广播通道(它不是任务),运行中靠轮询它的
|
|
144
|
+
// record 追进度;终态打开只读一次。
|
|
145
|
+
if (childRecordId && inspected.live) {
|
|
146
|
+
timer = window.setInterval(() => void load(), CHILD_POLL_INTERVAL_MS);
|
|
147
|
+
}
|
|
106
148
|
return () => {
|
|
107
149
|
cancelled = true;
|
|
150
|
+
if (timer !== undefined) window.clearInterval(timer);
|
|
108
151
|
};
|
|
109
|
-
}, [inspected.id]);
|
|
152
|
+
}, [inspected.id, childRecordId, inspected.live]);
|
|
110
153
|
|
|
111
154
|
useEffect(() => {
|
|
155
|
+
if (childRecordId) return;
|
|
112
156
|
const bridge = getElectronBridge();
|
|
113
157
|
if (!bridge?.onTaskProcess) return;
|
|
114
158
|
return bridge.onTaskProcess((payload) => {
|
|
@@ -121,13 +165,14 @@ export function TaskProcessPanel({
|
|
|
121
165
|
setLive(payload.live);
|
|
122
166
|
if (payload.live) setStale(false);
|
|
123
167
|
});
|
|
124
|
-
}, [inspected.id]);
|
|
168
|
+
}, [inspected.id, childRecordId]);
|
|
125
169
|
|
|
126
170
|
const statusLabel = live
|
|
127
171
|
? '正在推理'
|
|
128
172
|
: stale
|
|
129
173
|
? '进程已中断(表上仍是运行中)'
|
|
130
174
|
: '已结束';
|
|
175
|
+
const panelTitle = `${childRecordId ? '子代理推理' : '后台推理'} · ${statusLabel}`;
|
|
131
176
|
|
|
132
177
|
return (
|
|
133
178
|
<div
|
|
@@ -135,7 +180,7 @@ export function TaskProcessPanel({
|
|
|
135
180
|
data-testid="task-process-panel"
|
|
136
181
|
>
|
|
137
182
|
<DockPanelHeader
|
|
138
|
-
title={
|
|
183
|
+
title={panelTitle}
|
|
139
184
|
onClose={onClose}
|
|
140
185
|
closeLabel="关闭过程面板"
|
|
141
186
|
actions={
|
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
} from '@/lib/show-thinking-content';
|
|
8
8
|
import { Markdown } from './Markdown';
|
|
9
9
|
import { ToolsFlow } from './ExecutedActionsCard';
|
|
10
|
+
import type { InspectTaskInput } from './executed-actions-model';
|
|
11
|
+
import type { ChildInfo } from './OrchestrationChildrenCard';
|
|
10
12
|
import {
|
|
11
13
|
estimateReasoningDurationMs,
|
|
12
14
|
processStatusLabel,
|
|
@@ -176,6 +178,8 @@ function ProcessBlockItems({
|
|
|
176
178
|
agents,
|
|
177
179
|
chats,
|
|
178
180
|
chatId,
|
|
181
|
+
onInspectTask,
|
|
182
|
+
orchestrationChildren,
|
|
179
183
|
thinkingDisplay,
|
|
180
184
|
keepFinishedThinkingOpen = false,
|
|
181
185
|
thinkingElapsedByIndex,
|
|
@@ -185,6 +189,8 @@ function ProcessBlockItems({
|
|
|
185
189
|
agents: LocalChatAgent[];
|
|
186
190
|
chats: LocalChat[];
|
|
187
191
|
chatId?: string | null;
|
|
192
|
+
onInspectTask?: (task: InspectTaskInput) => void;
|
|
193
|
+
orchestrationChildren?: ChildInfo[];
|
|
188
194
|
thinkingDisplay: ThinkingDisplayMode;
|
|
189
195
|
keepFinishedThinkingOpen?: boolean;
|
|
190
196
|
thinkingElapsedByIndex?: Array<number | undefined>;
|
|
@@ -218,7 +224,14 @@ function ProcessBlockItems({
|
|
|
218
224
|
if (block.type === 'tools') {
|
|
219
225
|
return (
|
|
220
226
|
<div key={`tools-${index}`} data-testid="turn-tools">
|
|
221
|
-
<ToolsFlow
|
|
227
|
+
<ToolsFlow
|
|
228
|
+
actions={block.actions}
|
|
229
|
+
compact
|
|
230
|
+
agents={agents}
|
|
231
|
+
chatId={chatId}
|
|
232
|
+
onInspectTask={onInspectTask}
|
|
233
|
+
orchestrationChildren={orchestrationChildren}
|
|
234
|
+
/>
|
|
222
235
|
</div>
|
|
223
236
|
);
|
|
224
237
|
}
|
|
@@ -245,6 +258,8 @@ function ProcessBlocks({
|
|
|
245
258
|
agents,
|
|
246
259
|
chats,
|
|
247
260
|
chatId,
|
|
261
|
+
onInspectTask,
|
|
262
|
+
orchestrationChildren,
|
|
248
263
|
thinkingDisplay,
|
|
249
264
|
keepFinishedThinkingOpen,
|
|
250
265
|
thinkingElapsedByIndex,
|
|
@@ -254,6 +269,8 @@ function ProcessBlocks({
|
|
|
254
269
|
agents: LocalChatAgent[];
|
|
255
270
|
chats: LocalChat[];
|
|
256
271
|
chatId?: string | null;
|
|
272
|
+
onInspectTask?: (task: InspectTaskInput) => void;
|
|
273
|
+
orchestrationChildren?: ChildInfo[];
|
|
257
274
|
thinkingDisplay: ThinkingDisplayMode;
|
|
258
275
|
keepFinishedThinkingOpen: boolean;
|
|
259
276
|
thinkingElapsedByIndex?: Array<number | undefined>;
|
|
@@ -266,6 +283,8 @@ function ProcessBlocks({
|
|
|
266
283
|
agents={agents}
|
|
267
284
|
chats={chats}
|
|
268
285
|
chatId={chatId}
|
|
286
|
+
onInspectTask={onInspectTask}
|
|
287
|
+
orchestrationChildren={orchestrationChildren}
|
|
269
288
|
thinkingDisplay={thinkingDisplay}
|
|
270
289
|
keepFinishedThinkingOpen={keepFinishedThinkingOpen}
|
|
271
290
|
thinkingElapsedByIndex={thinkingElapsedByIndex}
|
|
@@ -280,6 +299,8 @@ export function TurnProcessGroup({
|
|
|
280
299
|
agents,
|
|
281
300
|
chats,
|
|
282
301
|
chatId,
|
|
302
|
+
onInspectTask,
|
|
303
|
+
orchestrationChildren,
|
|
283
304
|
emptyFallback,
|
|
284
305
|
streamingHint,
|
|
285
306
|
startedAtMs,
|
|
@@ -294,6 +315,9 @@ export function TurnProcessGroup({
|
|
|
294
315
|
agents: LocalChatAgent[];
|
|
295
316
|
chats: LocalChat[];
|
|
296
317
|
chatId?: string | null;
|
|
318
|
+
onInspectTask?: (task: InspectTaskInput) => void;
|
|
319
|
+
/** 本回合子代理:委派行据此跳转到子代理过程。 */
|
|
320
|
+
orchestrationChildren?: ChildInfo[];
|
|
297
321
|
emptyFallback: ReactNode;
|
|
298
322
|
/** Shown while streaming after a tools row and before the summary lands. */
|
|
299
323
|
streamingHint?: ReactNode;
|
|
@@ -422,6 +446,8 @@ export function TurnProcessGroup({
|
|
|
422
446
|
agents={agents}
|
|
423
447
|
chats={chats}
|
|
424
448
|
chatId={chatId}
|
|
449
|
+
onInspectTask={onInspectTask}
|
|
450
|
+
orchestrationChildren={orchestrationChildren}
|
|
425
451
|
thinkingDisplay={thinkingDisplay}
|
|
426
452
|
keepFinishedThinkingOpen={!collapseWhenFinished}
|
|
427
453
|
thinkingElapsedByIndex={thinkingElapsedByIndex}
|
|
@@ -20,9 +20,9 @@ import { useCopy } from './useCopy';
|
|
|
20
20
|
* - **No edit button.** The cloud product's `editChatMessage` rewrites the
|
|
21
21
|
* SSE turn in place — we don't have that machinery yet. Tracking under
|
|
22
22
|
* phase 2c.
|
|
23
|
-
* - **No
|
|
24
|
-
*
|
|
25
|
-
*
|
|
23
|
+
* - **No inline `ref://` / `tool://` / `agent://` parsing.** Pinned-ref
|
|
24
|
+
* shortcuts stay cloud-only. `@智能体名` is a Markdown chip painted from
|
|
25
|
+
* the agent list + message text (refresh restores from content).
|
|
26
26
|
*
|
|
27
27
|
* What we KEEP from the old version (and why it matters for parity):
|
|
28
28
|
* - Left-aligned bubble (not right-aligned). The product chat layout stacks
|
|
@@ -62,7 +62,7 @@ export function UserMessage({ message, agents = [], chats = [] }: UserMessagePro
|
|
|
62
62
|
|
|
63
63
|
return (
|
|
64
64
|
<motion.div
|
|
65
|
-
className="group/message
|
|
65
|
+
className="group/message"
|
|
66
66
|
data-message-role="user"
|
|
67
67
|
data-message-id={message.id}
|
|
68
68
|
initial={{ opacity: 0, y: 20 }}
|
|
@@ -70,7 +70,7 @@ export function UserMessage({ message, agents = [], chats = [] }: UserMessagePro
|
|
|
70
70
|
transition={{ duration: 0.3 }}
|
|
71
71
|
>
|
|
72
72
|
<div className="flex justify-start">
|
|
73
|
-
<div className="mx-auto w-full max-w-[var(--chat-input-box-width)] rounded-lg border border-agent-border bg-agent-muted px-
|
|
73
|
+
<div className="mx-auto w-full max-w-[var(--chat-input-box-width)] rounded-agent-lg border border-agent-border/80 bg-agent-muted/70 px-3 py-2 text-xs text-agent-foreground">
|
|
74
74
|
<div className="relative">
|
|
75
75
|
<div
|
|
76
76
|
ref={contentRef}
|