@springbrand/message-panel 0.3.0-alpha.8 → 0.3.0-alpha.9

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.
@@ -21,6 +21,7 @@ export interface ToolCallProps {
21
21
  query: string;
22
22
  result: string;
23
23
  running: boolean;
24
+ iconUrl?: string;
24
25
  open: boolean;
25
26
  onOpenChange: (open: boolean) => void;
26
27
  className?: string;
@@ -33,6 +34,7 @@ export function ToolCall({
33
34
  query,
34
35
  result,
35
36
  running,
37
+ iconUrl,
36
38
  open,
37
39
  onOpenChange,
38
40
  className,
@@ -47,6 +49,15 @@ export function ToolCall({
47
49
  >
48
50
  <CollapsibleTrigger className="group/trigger text-foreground/55 hover:text-foreground/90 flex items-center gap-2 rounded-md py-1 text-[13.5px] transition-colors outline-none">
49
51
  <ChevronRightIcon className="size-3.5 shrink-0 opacity-60 transition-transform duration-200 ease-[cubic-bezier(0.32,0.72,0,1)] group-data-open/trigger:rotate-90 group-data-panel-open/trigger:rotate-90 motion-reduce:transition-none" />
52
+ {iconUrl && (
53
+ <img
54
+ src={iconUrl}
55
+ alt=""
56
+ aria-hidden="true"
57
+ className="size-4 shrink-0 rounded-sm object-contain"
58
+ data-tool-icon=""
59
+ />
60
+ )}
50
61
  <SwapLabel active={running ? 0 : 1} className="text-start">
51
62
  <ShimmerLabel
52
63
  active={running}
@@ -64,6 +64,9 @@ export interface CloudOsToolCall {
64
64
  input: UnknownRecord;
65
65
  output: unknown;
66
66
  outputText: string;
67
+ capabilityName?: string;
68
+ toolBrief?: string;
69
+ iconUrl?: string;
67
70
  errorText: string;
68
71
  /** UIMessage 的 part.state 原值。 */
69
72
  state: string;
@@ -77,6 +80,7 @@ export interface CloudOsToolCall {
77
80
  export interface ToolCallGroup {
78
81
  key: string;
79
82
  Icon: PhosphorIcon;
83
+ iconUrl?: string;
80
84
  label: string;
81
85
  detailLines: string[];
82
86
  calls: CloudOsToolCall[];
@@ -380,6 +384,12 @@ export function getToolCallSummary(call: CloudOsToolCall): {
380
384
  break;
381
385
  }
382
386
  const displayName = humanizeToolName(toolName);
387
+ if (call.capabilityName) {
388
+ const capabilityLabel = [call.capabilityName, call.toolBrief]
389
+ .filter(Boolean)
390
+ .join(" · ");
391
+ return { verb: running ? `Using ${capabilityLabel}` : `Used ${capabilityLabel}` };
392
+ }
383
393
  return { verb: running ? `Using ${displayName}` : `Used ${displayName}` };
384
394
  }
385
395
 
@@ -448,10 +458,12 @@ export function toCloudOsToolCall(
448
458
  part: unknown,
449
459
  fallbackId: string,
450
460
  isActive: boolean,
461
+ liveMetadata?: Record<string, unknown>,
451
462
  ): CloudOsToolCall {
452
463
  const record = recordOf(part);
453
464
  const toolName = toolNameOfPart(part) ?? stringField(record, "name") ?? "tool";
454
465
  const data = recordOf(record.data);
466
+ const liveMeta = recordOf(liveMetadata);
455
467
  const input =
456
468
  record.input != null
457
469
  ? recordOf(record.input)
@@ -459,6 +471,18 @@ export function toCloudOsToolCall(
459
471
  ? recordOf(data.input)
460
472
  : data;
461
473
  const output = record.output ?? data.output ?? record.result;
474
+ const outputRecord = recordOf(output);
475
+ const presentation = recordOf(outputRecord.presentation);
476
+ const toolMeta = recordOf(outputRecord.toolMeta);
477
+ const capabilityName =
478
+ stringField(presentation, "capabilityName") || stringField(toolMeta, "capabilityName") ||
479
+ stringField(liveMeta, "capabilityName") || undefined;
480
+ const toolBrief =
481
+ stringField(presentation, "toolBrief") || stringField(toolMeta, "toolBrief") ||
482
+ stringField(liveMeta, "toolBrief") || undefined;
483
+ const iconUrl =
484
+ stringField(presentation, "iconUrl") || stringField(toolMeta, "iconUrl") ||
485
+ stringField(liveMeta, "iconUrl") || undefined;
462
486
  const errorText = stringField(record, "errorText", "error");
463
487
  const state = stringField(record, "state", "status");
464
488
  const failed =
@@ -475,9 +499,12 @@ export function toCloudOsToolCall(
475
499
  input,
476
500
  output,
477
501
  outputText: normalizeOutputText(output),
502
+ capabilityName,
503
+ toolBrief,
504
+ iconUrl,
478
505
  errorText,
479
506
  state,
480
- running: isActive && !failed && RUNNING_STATES.has(state),
507
+ running: isActive && !failed && (RUNNING_STATES.has(state) || record.preliminary === true),
481
508
  failed,
482
509
  awaitingApproval,
483
510
  raw: part,
@@ -608,10 +635,16 @@ export function buildToolCallGroups(calls: CloudOsToolCall[]): ToolCallGroup[] {
608
635
  labelParts.push(`${summary.verb}${summary.target ? ` ${summary.target}` : ""}`);
609
636
  } else if (distinctKinds.length === 1) {
610
637
  const summary = getToolCallSummary(calls[0]);
638
+ const sameCapability = calls.every(
639
+ (call) => call.capabilityName === calls[0].capabilityName &&
640
+ call.toolBrief === calls[0].toolBrief,
641
+ );
611
642
  labelParts.push(
612
- detailLines.length === 1 && summary.target
613
- ? `${summary.verb} ${summary.target}`
614
- : describeToolCallCount(calls[0].kind, calls[0].toolName, calls.length),
643
+ sameCapability && calls[0].capabilityName
644
+ ? `${summary.verb} ${formatTimes(calls.length)}`
645
+ : detailLines.length === 1 && summary.target
646
+ ? `${summary.verb} ${summary.target}`
647
+ : describeToolCallCount(calls[0].kind, calls[0].toolName, calls.length),
615
648
  );
616
649
  } else if (distinctKinds.length <= 3) {
617
650
  labelParts.push(
@@ -629,6 +662,7 @@ export function buildToolCallGroups(calls: CloudOsToolCall[]): ToolCallGroup[] {
629
662
  // 用第一条 work item 的 id 作 key:展开态在「流式 → 已落库」之间要活下来。
630
663
  key: `group-${calls[0].toolCallId}`,
631
664
  Icon: getToolIcon(calls[0].kind),
665
+ iconUrl: calls.find((call) => call.iconUrl)?.iconUrl,
632
666
  label: labelParts
633
667
  .map((part, index) => (index === 0 ? part : lowerFirst(part)))
634
668
  .join(", "),
@@ -54,6 +54,7 @@ function toolCallView(tc: CloudOsToolCall) {
54
54
  return {
55
55
  label: settled.verb,
56
56
  activeLabel: active.verb,
57
+ iconUrl: tc.iconUrl,
57
58
  query: settled.target ?? "",
58
59
  result:
59
60
  tc.errorText ||
@@ -246,6 +247,7 @@ export const ToolGroupRow = memo(function ToolGroupRow({
246
247
  restingLabel={group.label}
247
248
  activeLabel={`Working for ${elapsed}s`}
248
249
  stats={[]}
250
+ iconUrl={group.iconUrl}
249
251
  className="max-w-none"
250
252
  >
251
253
  {group.calls.map((toolCall) => {
@@ -32,6 +32,7 @@ export interface ToolTimelineProps {
32
32
  restingLabel: string;
33
33
  activeLabel: string;
34
34
  stats: TimelineStat[];
35
+ iconUrl?: string;
35
36
  className?: string;
36
37
  children?: ReactNode;
37
38
  }
@@ -45,6 +46,7 @@ export function ToolTimeline({
45
46
  restingLabel,
46
47
  activeLabel,
47
48
  stats,
49
+ iconUrl,
48
50
  className,
49
51
  children,
50
52
  }: ToolTimelineProps) {
@@ -57,6 +59,15 @@ export function ToolTimeline({
57
59
  >
58
60
  <CollapsibleTrigger className="group/trigger text-foreground/55 hover:text-foreground/90 flex items-center gap-1.5 rounded-md py-1 text-[13.5px] transition-colors outline-none">
59
61
  <ChevronRightIcon className="size-3.5 shrink-0 opacity-60 transition-transform duration-200 ease-[cubic-bezier(0.32,0.72,0,1)] group-data-open/trigger:rotate-90 group-data-panel-open/trigger:rotate-90 motion-reduce:transition-none" />
62
+ {iconUrl && (
63
+ <img
64
+ src={iconUrl}
65
+ alt=""
66
+ aria-hidden="true"
67
+ className="size-4 shrink-0 rounded-sm object-contain"
68
+ data-tool-icon=""
69
+ />
70
+ )}
60
71
  <SwapLabel
61
72
  active={streaming ? 0 : 1}
62
73
  className="text-start tabular-nums"
@@ -431,6 +431,14 @@ function assistantBlocks(
431
431
  approvalIdOf: (part: unknown) => string | undefined,
432
432
  ): AssistantBlock[] {
433
433
  const blocks: AssistantBlock[] = [];
434
+ const actionToolMetaByCallId = new Map<string, UnknownRecord>();
435
+ for (const part of message.parts) {
436
+ if (dataKindOf(part) !== "action_tool_meta") continue;
437
+ const record = recordOf(part);
438
+ const data = dataOf(part);
439
+ const toolCallId = String(data.toolCallId ?? record.id ?? "");
440
+ if (toolCallId) actionToolMetaByCallId.set(toolCallId, data);
441
+ }
434
442
  let pendingCalls: CloudOsToolCall[] = [];
435
443
  let pendingActionCalls: CloudOsToolCall[] = [];
436
444
 
@@ -498,6 +506,7 @@ function assistantBlocks(
498
506
  }
499
507
 
500
508
  const dataKind = dataKindOf(part);
509
+ if (dataKind === "action_tool_meta") return;
501
510
  if (dataKind === "error") {
502
511
  const data = dataOf(part);
503
512
  flush();
@@ -583,7 +592,13 @@ function assistantBlocks(
583
592
  const toolName = toolNameOf(part);
584
593
  if (toolName === null) return;
585
594
 
586
- const call = toCloudOsToolCall(part, key, isActive);
595
+ const toolCallId = String(record.toolCallId ?? record.id ?? "");
596
+ const call = toCloudOsToolCall(
597
+ part,
598
+ key,
599
+ isActive,
600
+ actionToolMetaByCallId.get(toolCallId),
601
+ );
587
602
  const presentation = actionPresentationOf(call.output);
588
603
  if (presentation) {
589
604
  if (presentation.state === "running") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@springbrand/message-panel",
3
- "version": "0.3.0-alpha.8",
3
+ "version": "0.3.0-alpha.9",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src",