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

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.
@@ -237,6 +237,7 @@ function UserBubble({
237
237
 
238
238
  function ActionPresentationBlock({
239
239
  presentation,
240
+ compact = false,
240
241
  failureCount = 1,
241
242
  partial = false,
242
243
  renderFile,
@@ -245,6 +246,7 @@ function ActionPresentationBlock({
245
246
  onOpenArtifact,
246
247
  }: {
247
248
  presentation: CloudOsActionPresentation;
249
+ compact?: boolean;
248
250
  failureCount?: number;
249
251
  partial?: boolean;
250
252
  renderFile?: FileRenderer;
@@ -254,16 +256,41 @@ function ActionPresentationBlock({
254
256
  }) {
255
257
  const { state } = presentation;
256
258
  if (state === "ready") {
259
+ const hasFileContent = presentation.content.length > 0;
260
+ const statusLabel = hasFileContent
261
+ ? presentation.capabilityName || "Completed"
262
+ : presentation.toolBrief || "Completed";
263
+ const hasStatusMetadata = hasFileContent
264
+ ? Boolean(presentation.capabilityName || presentation.iconUrl)
265
+ : Boolean(presentation.toolBrief || presentation.iconUrl);
266
+ const status = (
267
+ <span
268
+ className="flex shrink-0 items-center gap-2 text-[14px] leading-5 text-kumo-subtle"
269
+ data-action-status=""
270
+ >
271
+ {presentation.iconUrl && (
272
+ <img
273
+ src={presentation.iconUrl}
274
+ alt=""
275
+ aria-hidden="true"
276
+ className="size-4 rounded-sm object-contain"
277
+ data-action-icon=""
278
+ />
279
+ )}
280
+ <span>{statusLabel}</span>
281
+ </span>
282
+ );
257
283
  return (
258
284
  <div
259
285
  data-action-presentation={state}
260
286
  role="status"
261
- aria-label="Completed"
262
- className="space-y-2"
287
+ aria-label={statusLabel}
288
+ className={compact
289
+ ? "inline-flex items-center rounded-full border border-kumo-line bg-kumo-elevated px-3 py-1.5"
290
+ : "space-y-2"}
263
291
  >
264
- {presentation.content.length === 0
265
- ? <p className="m-0 text-[14px] leading-5 text-kumo-subtle">Completed</p>
266
- : presentation.content.map((file, index) => {
292
+ {presentation.content.length === 0 && status}
293
+ {presentation.content.map((file, index) => {
267
294
  const url = resolveUrl?.(file.url) ?? file.url;
268
295
  const artifactId = resolveArtifactId?.(url);
269
296
  return (
@@ -276,6 +303,7 @@ function ActionPresentationBlock({
276
303
  },
277
304
  placement: "assistant-message",
278
305
  variant: "product",
306
+ leadingMeta: hasStatusMetadata ? status : undefined,
279
307
  onOpen: file.mediaType?.startsWith("image/") && artifactId && onOpenArtifact
280
308
  ? () => onOpenArtifact(artifactId)
281
309
  : undefined,
@@ -897,10 +925,40 @@ export function CloudOsChatMessages({
897
925
 
898
926
  {actionResultBlocks.length > 0 && (
899
927
  <div data-action-results="" className="space-y-4">
928
+ {actionResultBlocks.filter((block) =>
929
+ block.presentation.state === "ready" &&
930
+ block.presentation.content.length === 0,
931
+ ).length > 0 && (
932
+ <div data-action-empty-results="" className="flex flex-wrap gap-2">
933
+ {actionResultBlocks.map((block) => {
934
+ if (
935
+ block.presentation.state !== "ready" ||
936
+ block.presentation.content.length !== 0 ||
937
+ (block.presentation.failureCode === "insufficient_credits" &&
938
+ block !== firstInsufficientFailure)
939
+ ) return null;
940
+ return (
941
+ <ActionPresentationBlock
942
+ key={block.key}
943
+ presentation={block.presentation}
944
+ compact
945
+ failureCount={insufficientFailures.length}
946
+ partial={hasCompletedAction}
947
+ renderFile={renderFile}
948
+ resolveUrl={resolveUrl}
949
+ resolveArtifactId={resolveArtifactId}
950
+ onOpenArtifact={onOpenArtifact}
951
+ />
952
+ );
953
+ })}
954
+ </div>
955
+ )}
900
956
  {actionResultBlocks.map((block) => {
901
957
  if (
902
958
  block.presentation.failureCode === "insufficient_credits" &&
903
- block !== firstInsufficientFailure
959
+ block !== firstInsufficientFailure ||
960
+ (block.presentation.state === "ready" &&
961
+ block.presentation.content.length === 0)
904
962
  ) return null;
905
963
  return (
906
964
  <ActionPresentationBlock
@@ -62,6 +62,9 @@ export interface CloudOsActionPresentation {
62
62
  executionId: string;
63
63
  state: "running" | "ready" | "failed" | "outcome_unknown";
64
64
  failureCode?: "insufficient_credits";
65
+ capabilityName?: string;
66
+ toolBrief?: string;
67
+ iconUrl?: string;
65
68
  produces?: CloudOsActionProduces;
66
69
  content: CloudOsActionPresentationFile[];
67
70
  }
@@ -338,6 +341,23 @@ function actionPresentationOf(value: unknown): CloudOsActionPresentation | null
338
341
  }
339
342
  if (state !== "ready" && content.length > 0) return null;
340
343
 
344
+ const toolBrief = typeof presentation.toolBrief === "string" && presentation.toolBrief.trim()
345
+ ? presentation.toolBrief.trim()
346
+ : undefined;
347
+ const capabilityName = typeof presentation.capabilityName === "string" && presentation.capabilityName.trim()
348
+ ? presentation.capabilityName.trim()
349
+ : undefined;
350
+ let iconUrl: string | undefined;
351
+ if (presentation.iconUrl !== undefined) {
352
+ if (typeof presentation.iconUrl !== "string" || !presentation.iconUrl.trim()) return null;
353
+ try {
354
+ if (new URL(presentation.iconUrl).protocol !== "https:") return null;
355
+ } catch {
356
+ return null;
357
+ }
358
+ iconUrl = presentation.iconUrl;
359
+ }
360
+
341
361
  return {
342
362
  type: "action-execution",
343
363
  executionId: presentation.executionId,
@@ -345,6 +365,9 @@ function actionPresentationOf(value: unknown): CloudOsActionPresentation | null
345
365
  ...(state === "failed" && presentation.failureCode === "insufficient_credits"
346
366
  ? { failureCode: presentation.failureCode }
347
367
  : {}),
368
+ ...(capabilityName ? { capabilityName } : {}),
369
+ ...(toolBrief ? { toolBrief } : {}),
370
+ ...(iconUrl ? { iconUrl } : {}),
348
371
  ...(produces ? { produces } : {}),
349
372
  content,
350
373
  };
@@ -409,8 +432,17 @@ function assistantBlocks(
409
432
  ): AssistantBlock[] {
410
433
  const blocks: AssistantBlock[] = [];
411
434
  let pendingCalls: CloudOsToolCall[] = [];
435
+ let pendingActionCalls: CloudOsToolCall[] = [];
412
436
 
413
- const flush = () => {
437
+ const flushActionCalls = () => {
438
+ if (pendingActionCalls.length === 0) return;
439
+ for (const group of buildToolCallGroups(pendingActionCalls)) {
440
+ blocks.push({ kind: "toolGroup", key: group.key, group });
441
+ }
442
+ pendingActionCalls = [];
443
+ };
444
+
445
+ const flushRegularCalls = () => {
414
446
  if (pendingCalls.length === 0) return;
415
447
  for (const group of buildToolCallGroups(pendingCalls)) {
416
448
  blocks.push({ kind: "toolGroup", key: group.key, group });
@@ -418,6 +450,11 @@ function assistantBlocks(
418
450
  pendingCalls = [];
419
451
  };
420
452
 
453
+ const flush = () => {
454
+ flushActionCalls();
455
+ flushRegularCalls();
456
+ };
457
+
421
458
  message.parts.forEach((part, partIndex) => {
422
459
  const key = `${message.id}:${partIndex}`;
423
460
  const record = recordOf(part);
@@ -549,8 +586,14 @@ function assistantBlocks(
549
586
  const call = toCloudOsToolCall(part, key, isActive);
550
587
  const presentation = actionPresentationOf(call.output);
551
588
  if (presentation) {
552
- pendingCalls.push(call);
553
- flush();
589
+ if (presentation.state === "running") {
590
+ flushActionCalls();
591
+ pendingCalls.push(call);
592
+ flush();
593
+ } else {
594
+ flushRegularCalls();
595
+ pendingActionCalls.push(call);
596
+ }
554
597
  blocks.push({ kind: "actionPresentation", key, presentation });
555
598
  return;
556
599
  }
@@ -600,6 +643,7 @@ function assistantBlocks(
600
643
  return;
601
644
  }
602
645
 
646
+ flushActionCalls();
603
647
  pendingCalls.push(call);
604
648
  });
605
649
 
@@ -830,11 +874,19 @@ function dropSupersededActionPresentations(entries: CloudOsEntry[]): CloudOsEntr
830
874
  return;
831
875
  }
832
876
  if (block.presentation.state !== "running") {
833
- current.presentation = block.presentation;
877
+ current.presentation = {
878
+ ...block.presentation,
879
+ capabilityName: block.presentation.capabilityName ?? current.presentation.capabilityName,
880
+ toolBrief: block.presentation.toolBrief ?? current.presentation.toolBrief,
881
+ iconUrl: block.presentation.iconUrl ?? current.presentation.iconUrl,
882
+ };
834
883
  } else if (current.presentation.state === "running") {
835
884
  current.presentation = {
836
885
  ...block.presentation,
837
886
  produces: block.presentation.produces ?? current.presentation.produces,
887
+ capabilityName: block.presentation.capabilityName ?? current.presentation.capabilityName,
888
+ toolBrief: block.presentation.toolBrief ?? current.presentation.toolBrief,
889
+ iconUrl: block.presentation.iconUrl ?? current.presentation.iconUrl,
838
890
  };
839
891
  }
840
892
  });
@@ -61,6 +61,7 @@ export interface FileViewProps {
61
61
  file: FileViewFile;
62
62
  placement: FileViewPlacement;
63
63
  variant?: FileViewVariant;
64
+ leadingMeta?: ReactNode;
64
65
  status?: FileViewStatus;
65
66
  progress?: number;
66
67
  onOpen?: () => void;
@@ -172,11 +173,13 @@ function ProductFileView({
172
173
  file,
173
174
  label,
174
175
  placement,
176
+ leadingMeta,
175
177
  onOpen,
176
178
  }: {
177
179
  file: FileViewFile;
178
180
  label: string;
179
181
  placement: Exclude<FileViewPlacement, "composer">;
182
+ leadingMeta?: ReactNode;
180
183
  onOpen?: () => void;
181
184
  }) {
182
185
  const type = productTypeForMediaType(file.mediaType);
@@ -225,6 +228,8 @@ function ProductFileView({
225
228
  <span className="flex w-fit max-w-full flex-col gap-2.5" data-product-file-view={type}>
226
229
  {preview}
227
230
  <span className="flex min-w-0 max-w-full items-center gap-1 text-xs text-kumo-inactive">
231
+ {leadingMeta}
232
+ {leadingMeta && <span aria-hidden="true">·</span>}
228
233
  <span className="capitalize text-kumo-subtle">{type}</span>
229
234
  <span aria-hidden="true">·</span>
230
235
  <span className="min-w-0 truncate">{label}</span>
@@ -306,6 +311,7 @@ export function FileView({
306
311
  file,
307
312
  placement,
308
313
  variant = "compact",
314
+ leadingMeta,
309
315
  status = "ready",
310
316
  progress = 0,
311
317
  onOpen,
@@ -413,6 +419,7 @@ export function FileView({
413
419
  file={file}
414
420
  label={label}
415
421
  placement={placement}
422
+ leadingMeta={leadingMeta}
416
423
  onOpen={onOpen}
417
424
  />
418
425
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@springbrand/message-panel",
3
- "version": "0.3.0-alpha.6",
3
+ "version": "0.3.0-alpha.8",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "src",
@@ -1 +1 @@
1
- {"version":"4.1.10","results":[[":bot/cycles.test.ts",{"duration":55.58770800000002,"failed":false}],[":bot/skins.test.ts",{"duration":3032.4465840000003,"failed":false}],[":bot/engine.test.ts",{"duration":100.501834,"failed":false}],[":bot/shape.test.ts",{"duration":16.04204200000001,"failed":false}],[":bot/expressions.test.ts",{"duration":8.212458999999996,"failed":false}],[":bot/face.test.ts",{"duration":3.6211250000000064,"failed":false}],[":drag.test.ts",{"duration":1.7712499999999807,"failed":false}],[":animation.test.ts",{"duration":2.3806249999999807,"failed":false}]]}
1
+ {"version":"4.1.10","results":[[":bot/cycles.test.ts",{"duration":54.568250000000006,"failed":false}],[":bot/skins.test.ts",{"duration":2830.836917,"failed":false}],[":bot/engine.test.ts",{"duration":80.72770900000003,"failed":false}],[":bot/shape.test.ts",{"duration":10.531834000000003,"failed":false}],[":bot/expressions.test.ts",{"duration":8.110375000000005,"failed":false}],[":bot/face.test.ts",{"duration":2.7700000000000102,"failed":false}],[":drag.test.ts",{"duration":1.9160829999999862,"failed":false}],[":animation.test.ts",{"duration":2.0596669999999904,"failed":false}]]}