@springbrand/message-panel 0.1.3-alpha.0 → 0.1.3-alpha.2

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.
@@ -71,9 +71,7 @@ export interface CamelChatMessagesProps {
71
71
  finalToolNames?: readonly string[];
72
72
  isHydrating?: boolean;
73
73
  isRecovering?: boolean;
74
- isServerStreaming?: boolean;
75
- /** Show the opt-in activity bar for recovering or server-streaming states. */
76
- showActivityStatus?: boolean;
74
+ recoveryStatusLabel?: string;
77
75
  hasPendingSteer?: boolean;
78
76
  startedWithPending?: boolean;
79
77
  error?: unknown;
@@ -163,6 +161,23 @@ export function CamelChatError({
163
161
  );
164
162
  }
165
163
 
164
+ export function CamelChatRecovery({
165
+ label = "模型响应暂时中断,正在重试…",
166
+ }: {
167
+ label?: string;
168
+ }) {
169
+ return (
170
+ <div
171
+ className="mt-4 flex items-center gap-2 rounded-lg border border-border/70 bg-muted/40 px-3 py-2 text-sm text-muted-foreground"
172
+ data-camel-chat-recovery=""
173
+ role="status"
174
+ >
175
+ <Loader2Icon className="size-4 shrink-0 animate-spin" aria-hidden="true" />
176
+ <span>{label}</span>
177
+ </div>
178
+ );
179
+ }
180
+
166
181
  export function CamelApprovalPrompts({
167
182
  approvals,
168
183
  error,
@@ -403,16 +418,21 @@ function CamelToolRow({
403
418
  onApprove: (approvalId: string, decision: ToolApprovalDecision) => void;
404
419
  getToolApproval?: (part: unknown) => ToolApprovalView | undefined;
405
420
  }) {
406
- const [open, setOpen] = useState(false);
407
421
  const approval = getToolApproval?.(part) ?? toolApprovalOf(part);
408
422
  const pendingApproval = approval && approval.approved === undefined;
409
423
  const presentation = getCamelToolPresentation(part, isActive);
424
+ const autoOpen = presentation.running && (
425
+ presentation.isSubAgent ||
426
+ presentation.kind === "write" ||
427
+ presentation.kind === "edit"
428
+ );
429
+ const [open, setOpen] = useState(autoOpen);
410
430
  const hasDetails = children != null || presentation.hasDetails;
411
431
  const title = presentation.summary;
412
432
 
413
433
  useEffect(() => {
414
- if (presentation.isSubAgent && presentation.running) setOpen(true);
415
- }, [presentation.isSubAgent, presentation.running]);
434
+ if (autoOpen) setOpen(true);
435
+ }, [autoOpen]);
416
436
 
417
437
  return (
418
438
  <div data-part-type={normalizedPartType(part)}>
@@ -1145,6 +1165,7 @@ function RenderAssistantPart({
1145
1165
  function AssistantTurn({
1146
1166
  turn,
1147
1167
  isActive,
1168
+ keepTraceExpanded,
1148
1169
  hasPendingSteer,
1149
1170
  hasPendingApproval,
1150
1171
  onFork,
@@ -1156,6 +1177,7 @@ function AssistantTurn({
1156
1177
  }: {
1157
1178
  turn: CamelAssistantTurn;
1158
1179
  isActive: boolean;
1180
+ keepTraceExpanded: boolean;
1159
1181
  hasPendingSteer: boolean;
1160
1182
  hasPendingApproval: boolean;
1161
1183
  onFork?: (messageId: string) => void | Promise<void>;
@@ -1179,7 +1201,10 @@ function AssistantTurn({
1179
1201
  />
1180
1202
  );
1181
1203
  const showWorkInPlace =
1182
- isActive || hasPendingApproval || turn.terminalState !== undefined;
1204
+ isActive ||
1205
+ keepTraceExpanded ||
1206
+ hasPendingApproval ||
1207
+ turn.terminalState !== undefined;
1183
1208
  const showSummary = !showWorkInPlace && turn.traceParts.length > 0;
1184
1209
  const completedAt = turn.completedAt ?? camelCompletedAt(turn.message);
1185
1210
  const messageTime =
@@ -1189,6 +1214,11 @@ function AssistantTurn({
1189
1214
  const thinkingActivity = deriveThinkingActivity(
1190
1215
  turn.traceParts.at(-1)?.part ?? turn.finalParts.at(-1)?.part,
1191
1216
  );
1217
+ const hasRunningTool = turn.traceParts.some(
1218
+ ({ part }) =>
1219
+ toolNameOfPart(part) !== null &&
1220
+ getCamelToolPresentation(part, isActive).running,
1221
+ );
1192
1222
  const activeActivity = hasPendingSteer
1193
1223
  ? pendingSteerActivity
1194
1224
  : thinkingActivity;
@@ -1269,7 +1299,7 @@ function AssistantTurn({
1269
1299
  {turn.copyText && <ClipboardAction text={turn.copyText} />}
1270
1300
  </div>
1271
1301
  )}
1272
- {isActive && turn.finalParts.length === 0 && (
1302
+ {isActive && turn.finalParts.length === 0 && !hasRunningTool && (
1273
1303
  <ThinkingIndicator {...activeActivity} />
1274
1304
  )}
1275
1305
  </div>
@@ -1283,8 +1313,7 @@ export function CamelChatMessages({
1283
1313
  finalToolNames,
1284
1314
  isHydrating = false,
1285
1315
  isRecovering = false,
1286
- isServerStreaming = false,
1287
- showActivityStatus = false,
1316
+ recoveryStatusLabel,
1288
1317
  hasPendingSteer = false,
1289
1318
  startedWithPending = false,
1290
1319
  error,
@@ -1316,6 +1345,10 @@ export function CamelChatMessages({
1316
1345
  isActive && lastItem?.kind === "assistant"
1317
1346
  ? lastItem.actionMessageId
1318
1347
  : null;
1348
+ const recoveringAssistantMessageId =
1349
+ isRecovering && lastItem?.kind === "assistant"
1350
+ ? lastItem.actionMessageId
1351
+ : null;
1319
1352
 
1320
1353
  const syncScrollButton = useCallback(() => {
1321
1354
  const element = scrollRef.current;
@@ -1329,7 +1362,7 @@ export function CamelChatMessages({
1329
1362
  const element = scrollRef.current;
1330
1363
  if (!element || showScrollButton) return;
1331
1364
  element.scrollTo({ top: element.scrollHeight });
1332
- }, [messages, status, showScrollButton]);
1365
+ }, [isRecovering, messages, status, showScrollButton]);
1333
1366
 
1334
1367
  return (
1335
1368
  <div
@@ -1339,15 +1372,6 @@ export function CamelChatMessages({
1339
1372
  data-status={status}
1340
1373
  aria-busy={isActive || isHydrating}
1341
1374
  >
1342
- {showActivityStatus && (isRecovering || isServerStreaming) && (
1343
- <div
1344
- className="absolute inset-x-0 top-0 z-10 flex h-7 items-center justify-center gap-2 bg-background/85 text-xs text-muted-foreground backdrop-blur-sm"
1345
- role="status"
1346
- >
1347
- <span className="size-1.5 animate-pulse rounded-full bg-blue-500" />
1348
- {isRecovering ? "Recovering…" : "Running…"}
1349
- </div>
1350
- )}
1351
1375
  <div
1352
1376
  ref={scrollRef}
1353
1377
  onScroll={syncScrollButton}
@@ -1368,6 +1392,9 @@ export function CamelChatMessages({
1368
1392
  key={item.key}
1369
1393
  turn={item}
1370
1394
  isActive={item.actionMessageId === activeAssistantMessageId}
1395
+ keepTraceExpanded={
1396
+ item.actionMessageId === recoveringAssistantMessageId
1397
+ }
1371
1398
  hasPendingSteer={hasPendingSteer}
1372
1399
  hasPendingApproval={item.parts.some((partRef) => {
1373
1400
  const approval =
@@ -1386,6 +1413,9 @@ export function CamelChatMessages({
1386
1413
  <StandaloneMessage key={item.key} item={item} />
1387
1414
  ),
1388
1415
  )}
1416
+ {isRecovering && (
1417
+ <CamelChatRecovery label={recoveryStatusLabel} />
1418
+ )}
1389
1419
  {error != null && <CamelChatError error={error} onRetry={onRetry} />}
1390
1420
  {isActive && activeAssistantMessageId === null && (
1391
1421
  <ThinkingIndicator
@@ -169,11 +169,20 @@ function isTraceWork(
169
169
  if (isVisibleReasoning(part)) return true;
170
170
  const kind = resolvePartKind(part);
171
171
  if (kind === "plan") return false;
172
- const toolName = toolNameOfPart(part);
173
- if (toolName !== null && finalToolNames.includes(toolName)) return false;
172
+ if (isFinalOutputTool(part, finalToolNames)) return false;
174
173
  return TRACE_PART_KINDS.has(kind ?? "");
175
174
  }
176
175
 
176
+ function isFinalOutputTool(
177
+ part: CamelPart,
178
+ finalToolNames: readonly string[],
179
+ ): boolean {
180
+ const toolName = toolNameOfPart(part);
181
+ return toolName !== null &&
182
+ (finalToolNames.includes(toolName) ||
183
+ resolvePartKind(part) === "suggestions");
184
+ }
185
+
177
186
  function isTraceBoundary(
178
187
  part: CamelPart,
179
188
  finalToolNames: readonly string[],
@@ -226,8 +235,17 @@ export function projectCamelAssistantTurn(
226
235
  partIndex,
227
236
  part,
228
237
  }));
229
- let lastTraceBoundary = -1;
238
+ let finalOutputBoundary = parts.length;
230
239
  for (let index = parts.length - 1; index >= 0; index -= 1) {
240
+ if (isFinalOutputTool(parts[index].part, finalToolNames)) {
241
+ finalOutputBoundary = index;
242
+ break;
243
+ }
244
+ }
245
+ let lastTraceBoundary = -1;
246
+ // Provider cleanup steps after final output must not pull the answer back
247
+ // into the collapsible work trace.
248
+ for (let index = finalOutputBoundary - 1; index >= 0; index -= 1) {
231
249
  if (isTraceBoundary(parts[index].part, finalToolNames)) {
232
250
  lastTraceBoundary = index;
233
251
  break;
package/src/message.tsx CHANGED
@@ -85,13 +85,6 @@ export const MessageAction = forwardRef<HTMLButtonElement, MessageActionProps>(
85
85
  MessageAction.displayName = "MessageAction";
86
86
 
87
87
  const streamdownPlugins = { cjk, code, math, mermaid };
88
- const streamAnimation = {
89
- animation: "fadeIn",
90
- sep: "word",
91
- duration: 260,
92
- stagger: 14,
93
- } as const;
94
-
95
88
  export type MessageResponseProps = StreamdownProps & {
96
89
  streaming?: boolean;
97
90
  resolveUrl?: MessageUrlResolver;
@@ -146,7 +139,6 @@ export const MessageResponse = memo(
146
139
  ? {
147
140
  mode: "streaming" as const,
148
141
  isAnimating: true,
149
- animated: streamAnimation,
150
142
  }
151
143
  : { mode: "static" as const })}
152
144
  {...props}
package/src/parts/plan.ts CHANGED
@@ -26,8 +26,9 @@ export function planStepsOfPart(part: unknown): PlanStep[] {
26
26
  const value = recordOf(step);
27
27
  const text = typeof value.text === "string" ? value.text.trim() : "";
28
28
  if (!text) return [];
29
- const status =
30
- value.status === "in_progress" || value.status === "done"
29
+ const status = value.status === "completed"
30
+ ? "done"
31
+ : value.status === "in_progress" || value.status === "done"
31
32
  ? value.status
32
33
  : "pending";
33
34
  return [{ text, status }];