fix(server): preserve tool lifecycle identity#7151

merged1
main
ui-refresh/tool-activity
openedAug 16, 2026byMmaria
View on GitHubTwo-way sync enabledPR last imported 3d ago
3 unresolved threads Ā· Jump to file
Selected thread: apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts:219 — <!-- MURMUR_IGNORE --> 🟔 **Medium** `Layers/ProviderRuntimeIngestion.ts:219` For completed or started lifecycle events whose `data` is a string, array, or other non-object value, `withToolCallId` replaces `payload.data` with `{ toolCallId, value }`, so consumers no longer receive the original value at `payload.data`. Preserve the original top-level data shape and expose `toolCallId` separately, rather than wrapping opaque `Schema.Unknown` payloads. No longer relevant as of ce306d218e5fd1676ad2bd84979a2d88a6b1dd07
0 / 4 viewed4 files changedLine totals unavailable

apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts Ā· Earlier revision d25f98f1 Ā· right 219

Original diff
@@ -211,6 +211,17 @@ function truncateDetail(value: string, limit = 180): string {
   return value.length > limit ? `${value.slice(0, limit - 3)}...` : value;
 }
 
+function withToolCallId(data: unknown, toolCallId: string | undefined): unknown {
+  if (toolCallId === undefined) return data;
+  if (data !== null && typeof data === "object" && !Array.isArray(data)) {
+    return { toolCallId, ...data };
+  }
+  return {
M
macroscopeappBotAug 16, 2026

🟔 Medium Layers/ProviderRuntimeIngestion.ts:219

For completed or started lifecycle events whose data is a string, array, or other non-object value, withToolCallId replaces payload.data with { toolCallId, value }, so consumers no longer receive the original value at payload.data. Preserve the original top-level data shape and expose toolCallId separately, rather than wrapping opaque Schema.Unknown payloads.

No longer relevant as of ce306d218e5fd1676ad2bd84979a2d88a6b1dd07

apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts Ā· Earlier revision 20324466 Ā· right 806

Original diff
@@ -803,6 +803,7 @@ export function runtimeEventToActivities(
           summary: event.payload.title ?? "Tool updated",
           payload: {
             itemType: event.payload.itemType,
+            ...(event.itemId !== undefined ? { toolCallId: event.itemId } : {}),
M
macroscopeappBotAug 16, 2026

🟔 Medium Layers/ProviderRuntimeIngestion.ts:806

event.itemId is stored as top-level payload.toolCallId, but clients only read payload.data.toolCallId, so tool.updated and tool.completed rows whose identity exists only in event.itemId still fall back to title/detail and can fail to fold when those fields change. Store the ID inside data as well, or update the client extractors in the same change.

āœ… Resolved in 8e450c7309922efdbfeaf7f84b795afea292cc70

apps/web/src/components/chat/ComposerTasksBadge.tsx Ā· Earlier revision 9df356b0 Ā· right 151–159

Original diff
@@ -148,16 +148,15 @@ export const ComposerTasksBadge = memo(function ComposerTasksBadge({
         </span>
         <TaskSegments className="w-20" steps={steps} />
       </button>
-      <Button
-        size="icon-micro"
-        variant="ghost-muted"
+      <button
+        type="button"
         aria-label="Dismiss tasks for this turn"
-        className="shrink-0"
+        className="inline-flex size-5 shrink-0 cursor-pointer items-center justify-center rounded-md text-muted-foreground/65 hover:bg-muted/60 hover:text-foreground"
         onClick={onDismiss}
         onPointerDown={(event) => event.preventDefault()}
       >
         <XIcon aria-hidden className="size-3" />
-      </Button>
+      </button>

apps/web/src/components/chat/ComposerTasksBadge.tsx Ā· right 151–159

M
macroscopeappBotAug 20, 2026

The same "Dismiss tasks for this turn" action is rendered with Button size="icon-micro" variant="ghost-muted" in the inline placement above (lines 102-111), but hand-rolled as a raw <button> here and again in ComposerTasksDrawer (lines 191-199). The raw version drops the primitive's focus-visible ring tokens (focus-visible:ring-2 focus-visible:ring-ring ring-offset-*), the coarse-pointer 44px hit target (pointer-coarse:after:min-h-11/min-w-11), and uses rounded-md/hover:bg-muted/60 instead of the shared rounded-sm/hover:bg-accent treatment, so the identical control behaves differently depending on placement. Consider using the primitive in both places (it is already imported):

suggestion
1 <Button
2 size="icon-micro"
3 variant="ghost-muted"
4 aria-label="Dismiss tasks for this turn"
5 className="shrink-0"
6 onClick={onDismiss}
7 onPointerDown={(event) => event.preventDefault()}
8 >
9 <XIcon aria-hidden className="size-3" />
10 </Button>

Posted via Macroscope — UI Consistency

M
mariaAug 20, 2026

This thread targets ComposerTasksBadge.tsx from the old stacked diff. PR #7151 now contains only the server lifecycle projection files, so the UI consistency suggestion belongs to the already-merged composer PR and is no longer actionable here.