@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
|
@@ -7,23 +7,41 @@ import {
|
|
|
7
7
|
} from '@/lib/show-thinking-content';
|
|
8
8
|
import { Markdown } from './Markdown';
|
|
9
9
|
import { ToolsFlow } from './ExecutedActionsCard';
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
10
|
+
import type { InspectTaskInput } from './executed-actions-model';
|
|
11
|
+
import type { ChildInfo } from './OrchestrationChildrenCard';
|
|
12
|
+
import {
|
|
13
|
+
estimateReasoningDurationMs,
|
|
14
|
+
processStatusLabel,
|
|
15
|
+
thinkingFoldLabel,
|
|
16
|
+
} from './process-status';
|
|
17
|
+
import { splitTurnProcess, type TurnBlock } from './turn-timeline';
|
|
12
18
|
|
|
13
19
|
/**
|
|
14
|
-
* Codex / DeepSeek-style turn process
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
20
|
+
* Codex / DeepSeek-style turn process. Each LLM round keeps three surfaces
|
|
21
|
+
* distinct: 思考 (muted CoT), 思考后的 response (reply bubble), 工具
|
|
22
|
+
* (cards). Trailing text becomes 最后一次结论 only after the stream ends —
|
|
23
|
+
* promoting it earlier would restyle it as thinking when the next round
|
|
24
|
+
* starts. Settings 「显示思考内容」 only clip the 思考正文: hidden /
|
|
25
|
+
* 5-line peek / full. A finished 思考 fold auto-collapses in chat;
|
|
26
|
+
* only the live round stays open. Tools and the work row stay independent.
|
|
18
27
|
*/
|
|
19
28
|
|
|
20
29
|
export const THINKING_PEEK_LINES = 5;
|
|
30
|
+
/** 思考正文行高(`leading-snug`)。 */
|
|
31
|
+
const THINKING_LINE_HEIGHT = 1.375;
|
|
21
32
|
/**
|
|
22
|
-
* 5
|
|
33
|
+
* 最多 5 行:用 max-height,短思考按内容收,不撑空。
|
|
23
34
|
* 不用 CSS `lh`:Windows Electron 在中文字体尚未就绪时 `lh` 会算成 0,
|
|
24
35
|
* 思考 peek 整块消失,字体加载后又把主列表高度撑跳。
|
|
25
36
|
*/
|
|
26
|
-
export const THINKING_PEEK_HEIGHT = `${THINKING_PEEK_LINES *
|
|
37
|
+
export const THINKING_PEEK_HEIGHT = `${THINKING_PEEK_LINES * THINKING_LINE_HEIGHT}em`;
|
|
38
|
+
|
|
39
|
+
const THINKING_TEXT =
|
|
40
|
+
'border-l border-agent-border/70 pl-2 text-xs leading-snug text-agent-muted-foreground';
|
|
41
|
+
|
|
42
|
+
function splitThinkingParagraphs(content: string): string[] {
|
|
43
|
+
return content.split(/\n{2,}/).filter((part) => part.length > 0);
|
|
44
|
+
}
|
|
27
45
|
|
|
28
46
|
function ReasoningBody({
|
|
29
47
|
content,
|
|
@@ -32,9 +50,16 @@ function ReasoningBody({
|
|
|
32
50
|
content: string;
|
|
33
51
|
showCursor?: boolean;
|
|
34
52
|
}) {
|
|
53
|
+
const parts = splitThinkingParagraphs(content);
|
|
35
54
|
return (
|
|
36
55
|
<>
|
|
37
|
-
<div className="
|
|
56
|
+
<div className="space-y-1">
|
|
57
|
+
{parts.map((part, index) => (
|
|
58
|
+
<div key={index} className="whitespace-pre-wrap break-words">
|
|
59
|
+
{part}
|
|
60
|
+
</div>
|
|
61
|
+
))}
|
|
62
|
+
</div>
|
|
38
63
|
{showCursor ? (
|
|
39
64
|
<span className="ml-0.5 inline-block h-3 w-[2px] animate-agent-cursor-blink bg-agent-muted-foreground/60 align-text-bottom" />
|
|
40
65
|
) : null}
|
|
@@ -64,83 +89,206 @@ function useLiveNow(enabled: boolean): number {
|
|
|
64
89
|
return now;
|
|
65
90
|
}
|
|
66
91
|
|
|
67
|
-
function
|
|
92
|
+
function ReasoningPane({
|
|
93
|
+
content,
|
|
94
|
+
showCursor,
|
|
95
|
+
clipped,
|
|
96
|
+
}: {
|
|
97
|
+
content: string;
|
|
98
|
+
showCursor?: boolean;
|
|
99
|
+
clipped?: boolean;
|
|
100
|
+
}) {
|
|
101
|
+
const scrollerRef = useRef<HTMLDivElement>(null);
|
|
102
|
+
useLayoutEffect(() => {
|
|
103
|
+
if (!clipped) return;
|
|
104
|
+
const el = scrollerRef.current;
|
|
105
|
+
if (!el) return;
|
|
106
|
+
el.scrollTop = el.scrollHeight;
|
|
107
|
+
}, [clipped, content]);
|
|
108
|
+
const body = (
|
|
109
|
+
<div className={THINKING_TEXT}>
|
|
110
|
+
<ReasoningBody content={content} showCursor={showCursor} />
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
if (!clipped) return body;
|
|
114
|
+
return (
|
|
115
|
+
<div
|
|
116
|
+
ref={scrollerRef}
|
|
117
|
+
className="overflow-y-auto overflow-anchor-none [scrollbar-width:none] [-ms-overflow-style:none] [&::-webkit-scrollbar]:hidden"
|
|
118
|
+
style={{ maxHeight: THINKING_PEEK_HEIGHT }}
|
|
119
|
+
data-thinking-peek=""
|
|
120
|
+
data-testid="thinking-peek"
|
|
121
|
+
data-peek-lines={THINKING_PEEK_LINES}
|
|
122
|
+
>
|
|
123
|
+
{body}
|
|
124
|
+
</div>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function ThinkingFold({
|
|
129
|
+
content,
|
|
130
|
+
label,
|
|
131
|
+
showCursor,
|
|
132
|
+
mode,
|
|
133
|
+
defaultOpen,
|
|
134
|
+
}: {
|
|
135
|
+
content: string;
|
|
136
|
+
label: string;
|
|
137
|
+
showCursor?: boolean;
|
|
138
|
+
mode: ThinkingDisplayMode;
|
|
139
|
+
defaultOpen: boolean;
|
|
140
|
+
}) {
|
|
141
|
+
const [open, setOpen] = useState(defaultOpen);
|
|
142
|
+
useEffect(() => {
|
|
143
|
+
setOpen(defaultOpen);
|
|
144
|
+
}, [defaultOpen]);
|
|
145
|
+
|
|
146
|
+
if (mode === 'hidden') return null;
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<div className="space-y-1" data-testid="turn-thinking">
|
|
150
|
+
<button
|
|
151
|
+
type="button"
|
|
152
|
+
onClick={() => setOpen((value) => !value)}
|
|
153
|
+
className="flex min-w-0 items-center gap-1 py-0.5 text-left text-[11px] text-agent-muted-foreground/80 transition-colors hover:text-agent-foreground"
|
|
154
|
+
aria-expanded={open}
|
|
155
|
+
data-thinking-fold=""
|
|
156
|
+
>
|
|
157
|
+
<span className="truncate">{label}</span>
|
|
158
|
+
{open ? (
|
|
159
|
+
<LuChevronDown className="h-3 w-3 shrink-0" />
|
|
160
|
+
) : (
|
|
161
|
+
<LuChevronRight className="h-3 w-3 shrink-0" />
|
|
162
|
+
)}
|
|
163
|
+
</button>
|
|
164
|
+
{open ? (
|
|
165
|
+
<ReasoningPane
|
|
166
|
+
content={content}
|
|
167
|
+
showCursor={showCursor}
|
|
168
|
+
clipped={mode === 'peek'}
|
|
169
|
+
/>
|
|
170
|
+
) : null}
|
|
171
|
+
</div>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function ProcessBlockItems({
|
|
68
176
|
blocks,
|
|
69
177
|
isStreaming,
|
|
70
178
|
agents,
|
|
71
179
|
chats,
|
|
72
180
|
chatId,
|
|
181
|
+
onInspectTask,
|
|
182
|
+
orchestrationChildren,
|
|
183
|
+
thinkingDisplay,
|
|
184
|
+
keepFinishedThinkingOpen = false,
|
|
185
|
+
thinkingElapsedByIndex,
|
|
73
186
|
}: {
|
|
74
187
|
blocks: TurnBlock[];
|
|
75
188
|
isStreaming: boolean;
|
|
76
189
|
agents: LocalChatAgent[];
|
|
77
190
|
chats: LocalChat[];
|
|
78
191
|
chatId?: string | null;
|
|
192
|
+
onInspectTask?: (task: InspectTaskInput) => void;
|
|
193
|
+
orchestrationChildren?: ChildInfo[];
|
|
194
|
+
thinkingDisplay: ThinkingDisplayMode;
|
|
195
|
+
keepFinishedThinkingOpen?: boolean;
|
|
196
|
+
thinkingElapsedByIndex?: Array<number | undefined>;
|
|
79
197
|
}) {
|
|
80
198
|
const lastIndex = blocks.length - 1;
|
|
81
199
|
return (
|
|
82
|
-
|
|
200
|
+
<>
|
|
83
201
|
{blocks.map((block, index) => {
|
|
84
202
|
const isLast = index === lastIndex;
|
|
85
203
|
if (block.type === 'reasoning') {
|
|
204
|
+
const isLive = isStreaming && isLast;
|
|
205
|
+
const elapsedMs =
|
|
206
|
+
thinkingElapsedByIndex?.[index]
|
|
207
|
+
?? block.durationMs
|
|
208
|
+
?? (isLive ? undefined : estimateReasoningDurationMs(block.content));
|
|
86
209
|
return (
|
|
87
|
-
<
|
|
210
|
+
<ThinkingFold
|
|
88
211
|
key={`reasoning-${index}`}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
212
|
+
content={block.content}
|
|
213
|
+
label={thinkingFoldLabel({
|
|
214
|
+
content: block.content,
|
|
215
|
+
isLive,
|
|
216
|
+
elapsedMs,
|
|
217
|
+
})}
|
|
218
|
+
showCursor={isLive}
|
|
219
|
+
mode={thinkingDisplay}
|
|
220
|
+
defaultOpen={isLive || keepFinishedThinkingOpen}
|
|
221
|
+
/>
|
|
93
222
|
);
|
|
94
223
|
}
|
|
95
224
|
if (block.type === 'tools') {
|
|
96
|
-
return
|
|
225
|
+
return (
|
|
226
|
+
<div key={`tools-${index}`} data-testid="turn-tools">
|
|
227
|
+
<ToolsFlow
|
|
228
|
+
actions={block.actions}
|
|
229
|
+
compact
|
|
230
|
+
agents={agents}
|
|
231
|
+
chatId={chatId}
|
|
232
|
+
onInspectTask={onInspectTask}
|
|
233
|
+
orchestrationChildren={orchestrationChildren}
|
|
234
|
+
/>
|
|
235
|
+
</div>
|
|
236
|
+
);
|
|
97
237
|
}
|
|
98
238
|
return (
|
|
99
|
-
<div
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
239
|
+
<div key={`text-${index}`} data-testid="turn-response">
|
|
240
|
+
<div>
|
|
241
|
+
<div className="markdown-content text-xs leading-relaxed text-agent-foreground">
|
|
242
|
+
<Markdown agents={agents} chats={chats} chatId={chatId}>{block.content}</Markdown>
|
|
243
|
+
</div>
|
|
244
|
+
{isStreaming && isLast ? (
|
|
245
|
+
<span className="ml-0.5 inline-block h-3.5 w-[3px] animate-agent-cursor-blink bg-agent-foreground/60 align-text-bottom" />
|
|
246
|
+
) : null}
|
|
247
|
+
</div>
|
|
104
248
|
</div>
|
|
105
249
|
);
|
|
106
250
|
})}
|
|
107
|
-
|
|
251
|
+
</>
|
|
108
252
|
);
|
|
109
253
|
}
|
|
110
254
|
|
|
111
|
-
function
|
|
255
|
+
function ProcessBlocks({
|
|
112
256
|
blocks,
|
|
113
257
|
isStreaming,
|
|
258
|
+
agents,
|
|
259
|
+
chats,
|
|
260
|
+
chatId,
|
|
261
|
+
onInspectTask,
|
|
262
|
+
orchestrationChildren,
|
|
263
|
+
thinkingDisplay,
|
|
264
|
+
keepFinishedThinkingOpen,
|
|
265
|
+
thinkingElapsedByIndex,
|
|
114
266
|
}: {
|
|
115
267
|
blocks: TurnBlock[];
|
|
116
268
|
isStreaming: boolean;
|
|
269
|
+
agents: LocalChatAgent[];
|
|
270
|
+
chats: LocalChat[];
|
|
271
|
+
chatId?: string | null;
|
|
272
|
+
onInspectTask?: (task: InspectTaskInput) => void;
|
|
273
|
+
orchestrationChildren?: ChildInfo[];
|
|
274
|
+
thinkingDisplay: ThinkingDisplayMode;
|
|
275
|
+
keepFinishedThinkingOpen: boolean;
|
|
276
|
+
thinkingElapsedByIndex?: Array<number | undefined>;
|
|
117
277
|
}) {
|
|
118
|
-
const scrollerRef = useRef<HTMLDivElement>(null);
|
|
119
|
-
const reasoning = blocks.filter(
|
|
120
|
-
(block): block is Extract<TurnBlock, { type: 'reasoning' }> =>
|
|
121
|
-
block.type === 'reasoning' && block.content.trim().length > 0,
|
|
122
|
-
);
|
|
123
|
-
const sig = reasoning.map((block) => block.content.length).join(',');
|
|
124
|
-
useLayoutEffect(() => {
|
|
125
|
-
const el = scrollerRef.current;
|
|
126
|
-
if (!el) return;
|
|
127
|
-
el.scrollTop = el.scrollHeight;
|
|
128
|
-
}, [sig]);
|
|
129
|
-
const lastIndex = reasoning.length - 1;
|
|
130
278
|
return (
|
|
131
|
-
<div
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
279
|
+
<div className="space-y-1.5">
|
|
280
|
+
<ProcessBlockItems
|
|
281
|
+
blocks={blocks}
|
|
282
|
+
isStreaming={isStreaming}
|
|
283
|
+
agents={agents}
|
|
284
|
+
chats={chats}
|
|
285
|
+
chatId={chatId}
|
|
286
|
+
onInspectTask={onInspectTask}
|
|
287
|
+
orchestrationChildren={orchestrationChildren}
|
|
288
|
+
thinkingDisplay={thinkingDisplay}
|
|
289
|
+
keepFinishedThinkingOpen={keepFinishedThinkingOpen}
|
|
290
|
+
thinkingElapsedByIndex={thinkingElapsedByIndex}
|
|
291
|
+
/>
|
|
144
292
|
</div>
|
|
145
293
|
);
|
|
146
294
|
}
|
|
@@ -151,12 +299,15 @@ export function TurnProcessGroup({
|
|
|
151
299
|
agents,
|
|
152
300
|
chats,
|
|
153
301
|
chatId,
|
|
302
|
+
onInspectTask,
|
|
303
|
+
orchestrationChildren,
|
|
154
304
|
emptyFallback,
|
|
155
305
|
streamingHint,
|
|
156
306
|
startedAtMs,
|
|
157
307
|
durationMs,
|
|
158
308
|
thinkingDisplay: thinkingDisplayProp,
|
|
159
309
|
showThinkingContent: showThinkingContentProp,
|
|
310
|
+
collapseWhenFinished = true,
|
|
160
311
|
renderAnswer,
|
|
161
312
|
}: {
|
|
162
313
|
blocks: TurnBlock[];
|
|
@@ -164,6 +315,9 @@ export function TurnProcessGroup({
|
|
|
164
315
|
agents: LocalChatAgent[];
|
|
165
316
|
chats: LocalChat[];
|
|
166
317
|
chatId?: string | null;
|
|
318
|
+
onInspectTask?: (task: InspectTaskInput) => void;
|
|
319
|
+
/** 本回合子代理:委派行据此跳转到子代理过程。 */
|
|
320
|
+
orchestrationChildren?: ChildInfo[];
|
|
167
321
|
emptyFallback: ReactNode;
|
|
168
322
|
/** Shown while streaming after a tools row and before the summary lands. */
|
|
169
323
|
streamingHint?: ReactNode;
|
|
@@ -178,6 +332,11 @@ export function TurnProcessGroup({
|
|
|
178
332
|
* Task process panel passes true so the inspector stays expanded.
|
|
179
333
|
*/
|
|
180
334
|
showThinkingContent?: boolean;
|
|
335
|
+
/**
|
|
336
|
+
* After the stream ends, fold the work row to the summary line.
|
|
337
|
+
* Task inspector sets false so the replay stays open.
|
|
338
|
+
*/
|
|
339
|
+
collapseWhenFinished?: boolean;
|
|
181
340
|
renderAnswer: (block: Extract<TurnBlock, { type: 'text' }>, isLast: boolean) => ReactNode;
|
|
182
341
|
}) {
|
|
183
342
|
const preference = useThinkingDisplay();
|
|
@@ -187,15 +346,16 @@ export function TurnProcessGroup({
|
|
|
187
346
|
showThinkingContentProp,
|
|
188
347
|
);
|
|
189
348
|
const showFullThinking = thinkingDisplay === 'full';
|
|
190
|
-
const
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
);
|
|
195
|
-
const
|
|
196
|
-
|
|
349
|
+
const { process, answer } = splitTurnProcess(blocks, { finalize: !isStreaming });
|
|
350
|
+
const defaultOpen = isStreaming
|
|
351
|
+
? thinkingDisplay !== 'hidden'
|
|
352
|
+
: showFullThinking && !collapseWhenFinished;
|
|
353
|
+
const [userOpen, setUserOpen] = useState<boolean | null>(null);
|
|
354
|
+
const open = userOpen ?? defaultOpen;
|
|
355
|
+
useEffect(() => {
|
|
356
|
+
setUserOpen(null);
|
|
357
|
+
}, [isStreaming]);
|
|
197
358
|
const answerLenRef = useRef(answer.length);
|
|
198
|
-
isStreamingRef.current = isStreaming;
|
|
199
359
|
answerLenRef.current = answer.length;
|
|
200
360
|
const now = useLiveNow(isStreaming);
|
|
201
361
|
const streamClockRef = useRef<number | undefined>(undefined);
|
|
@@ -216,76 +376,67 @@ export function TurnProcessGroup({
|
|
|
216
376
|
: durationMs ?? lastLiveElapsedRef.current;
|
|
217
377
|
|
|
218
378
|
const lastProcess = process[process.length - 1];
|
|
379
|
+
const lastIsReasoning = lastProcess?.type === 'reasoning';
|
|
380
|
+
const reasoningDurationsRef = useRef<number[]>([]);
|
|
219
381
|
const reasoningClockRef = useRef<{ index: number; startedAt: number } | null>(null);
|
|
220
|
-
if (isStreaming &&
|
|
382
|
+
if (isStreaming && lastIsReasoning) {
|
|
221
383
|
const index = process.length - 1;
|
|
222
384
|
if (reasoningClockRef.current?.index !== index) {
|
|
385
|
+
const prev = reasoningClockRef.current;
|
|
386
|
+
if (prev) {
|
|
387
|
+
reasoningDurationsRef.current[prev.index] = Math.max(0, now - prev.startedAt);
|
|
388
|
+
}
|
|
223
389
|
reasoningClockRef.current = { index, startedAt: now };
|
|
224
390
|
}
|
|
225
|
-
} else if (
|
|
391
|
+
} else if (reasoningClockRef.current) {
|
|
392
|
+
const prev = reasoningClockRef.current;
|
|
393
|
+
reasoningDurationsRef.current[prev.index] = Math.max(0, now - prev.startedAt);
|
|
226
394
|
reasoningClockRef.current = null;
|
|
227
395
|
}
|
|
228
|
-
const
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
238
|
-
if (isStreamingRef.current || answerLenRef.current === 0) setOpen(true);
|
|
239
|
-
}, [showFullThinking]);
|
|
240
|
-
|
|
241
|
-
useEffect(() => {
|
|
242
|
-
const wasStreaming = wasStreamingRef.current;
|
|
243
|
-
if (wasStreaming && !isStreaming && answer.length > 0) {
|
|
244
|
-
setOpen(false);
|
|
245
|
-
}
|
|
246
|
-
if (!wasStreaming && isStreaming) {
|
|
247
|
-
setOpen(showFullThinking);
|
|
396
|
+
const thinkingElapsedByIndex = process.map((block, index) => {
|
|
397
|
+
if (block.type !== 'reasoning') return undefined;
|
|
398
|
+
if (
|
|
399
|
+
isStreaming &&
|
|
400
|
+
lastIsReasoning &&
|
|
401
|
+
index === process.length - 1 &&
|
|
402
|
+
reasoningClockRef.current
|
|
403
|
+
) {
|
|
404
|
+
return Math.max(0, now - reasoningClockRef.current.startedAt);
|
|
248
405
|
}
|
|
249
|
-
|
|
250
|
-
}
|
|
406
|
+
return reasoningDurationsRef.current[index];
|
|
407
|
+
});
|
|
251
408
|
|
|
252
409
|
if (blocks.length === 0) return <>{emptyFallback}</>;
|
|
253
410
|
|
|
254
411
|
const showToggle = process.length > 0;
|
|
255
412
|
const showProcess = !showToggle || open;
|
|
256
|
-
const showThinkingPeek =
|
|
257
|
-
showThinkingPeekAllowed &&
|
|
258
|
-
isStreaming &&
|
|
259
|
-
!open &&
|
|
260
|
-
showToggle &&
|
|
261
|
-
processHasReasoning(process);
|
|
262
413
|
const showStreamingHint =
|
|
263
414
|
Boolean(streamingHint) && isStreaming && answer.length === 0 && !showToggle;
|
|
264
415
|
|
|
265
416
|
return (
|
|
266
|
-
<div className="space-y-1
|
|
417
|
+
<div className="space-y-1" data-turn-timeline>
|
|
267
418
|
{showToggle && (
|
|
268
419
|
<button
|
|
269
420
|
type="button"
|
|
270
|
-
onClick={() =>
|
|
421
|
+
onClick={() => setUserOpen(!open)}
|
|
271
422
|
className="flex w-full items-center gap-1 py-0.5 text-left text-xs text-agent-muted-foreground transition-colors hover:text-agent-foreground"
|
|
272
423
|
aria-expanded={open}
|
|
273
424
|
data-turn-process=""
|
|
425
|
+
data-testid="turn-process-toggle"
|
|
274
426
|
data-open={open || undefined}
|
|
275
427
|
>
|
|
276
|
-
|
|
277
|
-
<LuChevronDown className="h-3.5 w-3.5 shrink-0" />
|
|
278
|
-
) : (
|
|
279
|
-
<LuChevronRight className="h-3.5 w-3.5 shrink-0" />
|
|
280
|
-
)}
|
|
281
|
-
<span className="truncate">
|
|
428
|
+
<span className="min-w-0 truncate">
|
|
282
429
|
{processStatusLabel({
|
|
283
430
|
process,
|
|
284
431
|
isStreaming,
|
|
285
432
|
elapsedMs,
|
|
286
|
-
reasoningElapsedMs,
|
|
287
433
|
})}
|
|
288
434
|
</span>
|
|
435
|
+
{open ? (
|
|
436
|
+
<LuChevronDown className="h-3.5 w-3.5 shrink-0" />
|
|
437
|
+
) : (
|
|
438
|
+
<LuChevronRight className="h-3.5 w-3.5 shrink-0" />
|
|
439
|
+
)}
|
|
289
440
|
</button>
|
|
290
441
|
)}
|
|
291
442
|
{showProcess && (
|
|
@@ -295,11 +446,13 @@ export function TurnProcessGroup({
|
|
|
295
446
|
agents={agents}
|
|
296
447
|
chats={chats}
|
|
297
448
|
chatId={chatId}
|
|
449
|
+
onInspectTask={onInspectTask}
|
|
450
|
+
orchestrationChildren={orchestrationChildren}
|
|
451
|
+
thinkingDisplay={thinkingDisplay}
|
|
452
|
+
keepFinishedThinkingOpen={!collapseWhenFinished}
|
|
453
|
+
thinkingElapsedByIndex={thinkingElapsedByIndex}
|
|
298
454
|
/>
|
|
299
455
|
)}
|
|
300
|
-
{showThinkingPeek && (
|
|
301
|
-
<ThinkingPeek blocks={process} isStreaming={answer.length === 0} />
|
|
302
|
-
)}
|
|
303
456
|
{showStreamingHint ? streamingHint : null}
|
|
304
457
|
{answer.map((block, index) => (
|
|
305
458
|
<Fragment key={`answer-${index}`}>
|
|
@@ -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
|
|
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}
|
|
@@ -121,7 +121,7 @@ export function UserMessage({ message, agents = [], chats = [] }: UserMessagePro
|
|
|
121
121
|
</button>
|
|
122
122
|
)}
|
|
123
123
|
|
|
124
|
-
<div className="mt-
|
|
124
|
+
<div className="mt-1 flex items-center justify-between">
|
|
125
125
|
<button
|
|
126
126
|
type="button"
|
|
127
127
|
onClick={() => void copy()}
|
|
@@ -140,7 +140,7 @@ export function UserMessage({ message, agents = [], chats = [] }: UserMessagePro
|
|
|
140
140
|
<LuCopy className="h-3 w-3" />
|
|
141
141
|
)}
|
|
142
142
|
</button>
|
|
143
|
-
<div className="text-[
|
|
143
|
+
<div className="text-[11px] text-agent-muted-foreground">
|
|
144
144
|
{message.createdAt
|
|
145
145
|
? getFriendlyDate(new Date(message.createdAt))
|
|
146
146
|
: getFriendlyDate(new Date())}
|