@agent-native/toolkit 0.228.0-nightly-20261111212500 → 0.228.0

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.
Files changed (361) hide show
  1. package/agent-native.eject.json +2 -0
  2. package/dist/app/agentkit/react/approval-card.d.ts.map +1 -1
  3. package/dist/app/agentkit/react/approval-card.js +5 -3
  4. package/dist/app/agentkit/react/approval-card.js.map +1 -1
  5. package/dist/app/agentkit/react/code-block.d.ts +32 -0
  6. package/dist/app/agentkit/react/code-block.d.ts.map +1 -0
  7. package/dist/app/agentkit/react/code-block.js +125 -0
  8. package/dist/app/agentkit/react/code-block.js.map +1 -0
  9. package/dist/app/agentkit/react/components.d.ts +33 -14
  10. package/dist/app/agentkit/react/components.d.ts.map +1 -1
  11. package/dist/app/agentkit/react/components.js +843 -168
  12. package/dist/app/agentkit/react/components.js.map +1 -1
  13. package/dist/app/agentkit/react/context-usage.d.ts +45 -0
  14. package/dist/app/agentkit/react/context-usage.d.ts.map +1 -0
  15. package/dist/app/agentkit/react/context-usage.js +93 -0
  16. package/dist/app/agentkit/react/context-usage.js.map +1 -0
  17. package/dist/app/agentkit/react/context.d.ts +42 -3
  18. package/dist/app/agentkit/react/context.d.ts.map +1 -1
  19. package/dist/app/agentkit/react/context.js +46 -1
  20. package/dist/app/agentkit/react/context.js.map +1 -1
  21. package/dist/app/agentkit/react/disclosure-content.d.ts +12 -0
  22. package/dist/app/agentkit/react/disclosure-content.d.ts.map +1 -0
  23. package/dist/app/agentkit/react/disclosure-content.js +33 -0
  24. package/dist/app/agentkit/react/disclosure-content.js.map +1 -0
  25. package/dist/app/agentkit/react/duration.d.ts +6 -0
  26. package/dist/app/agentkit/react/duration.d.ts.map +1 -0
  27. package/dist/app/agentkit/react/duration.js +21 -0
  28. package/dist/app/agentkit/react/duration.js.map +1 -0
  29. package/dist/app/agentkit/react/file-change.d.ts +33 -0
  30. package/dist/app/agentkit/react/file-change.d.ts.map +1 -0
  31. package/dist/app/agentkit/react/file-change.js +83 -0
  32. package/dist/app/agentkit/react/file-change.js.map +1 -0
  33. package/dist/app/agentkit/react/file-diff.d.ts +17 -0
  34. package/dist/app/agentkit/react/file-diff.d.ts.map +1 -0
  35. package/dist/app/agentkit/react/file-diff.js +94 -0
  36. package/dist/app/agentkit/react/file-diff.js.map +1 -0
  37. package/dist/app/agentkit/react/index.d.ts +7 -0
  38. package/dist/app/agentkit/react/index.d.ts.map +1 -1
  39. package/dist/app/agentkit/react/index.js +7 -0
  40. package/dist/app/agentkit/react/index.js.map +1 -1
  41. package/dist/app/agentkit/react/question-card.d.ts +67 -0
  42. package/dist/app/agentkit/react/question-card.d.ts.map +1 -0
  43. package/dist/app/agentkit/react/question-card.js +198 -0
  44. package/dist/app/agentkit/react/question-card.js.map +1 -0
  45. package/dist/app/agentkit/react/root.d.ts +3 -2
  46. package/dist/app/agentkit/react/root.d.ts.map +1 -1
  47. package/dist/app/agentkit/react/root.js +2 -2
  48. package/dist/app/agentkit/react/root.js.map +1 -1
  49. package/dist/app/agentkit/react/roving-focus.d.ts +4 -0
  50. package/dist/app/agentkit/react/roving-focus.d.ts.map +1 -0
  51. package/dist/app/agentkit/react/roving-focus.js +75 -0
  52. package/dist/app/agentkit/react/roving-focus.js.map +1 -0
  53. package/dist/app/agentkit/react/stack-trace.d.ts +34 -0
  54. package/dist/app/agentkit/react/stack-trace.d.ts.map +1 -0
  55. package/dist/app/agentkit/react/stack-trace.js +121 -0
  56. package/dist/app/agentkit/react/stack-trace.js.map +1 -0
  57. package/dist/app/agentkit/react/stop-button.d.ts.map +1 -1
  58. package/dist/app/agentkit/react/stop-button.js +22 -6
  59. package/dist/app/agentkit/react/stop-button.js.map +1 -1
  60. package/dist/app/agentkit/react/styles.css +1306 -395
  61. package/dist/app/agentkit/react/terminal.d.ts +28 -0
  62. package/dist/app/agentkit/react/terminal.d.ts.map +1 -0
  63. package/dist/app/agentkit/react/terminal.js +84 -0
  64. package/dist/app/agentkit/react/terminal.js.map +1 -0
  65. package/dist/app/agentkit/react/trace.d.ts +31 -0
  66. package/dist/app/agentkit/react/trace.d.ts.map +1 -0
  67. package/dist/app/agentkit/react/trace.js +73 -0
  68. package/dist/app/agentkit/react/trace.js.map +1 -0
  69. package/dist/app/blocks/library/HighlightedCode.d.ts +2 -1
  70. package/dist/app/blocks/library/HighlightedCode.d.ts.map +1 -1
  71. package/dist/app/blocks/library/HighlightedCode.js +8 -111
  72. package/dist/app/blocks/library/HighlightedCode.js.map +1 -1
  73. package/dist/app/chat/AgentKitAssistantChat.d.ts.map +1 -1
  74. package/dist/app/chat/AgentKitAssistantChat.js +23 -245
  75. package/dist/app/chat/AgentKitAssistantChat.js.map +1 -1
  76. package/dist/app/chat/agentkit-chat/guided-questions.d.ts +15 -1
  77. package/dist/app/chat/agentkit-chat/guided-questions.d.ts.map +1 -1
  78. package/dist/app/chat/agentkit-chat/guided-questions.js +142 -2
  79. package/dist/app/chat/agentkit-chat/guided-questions.js.map +1 -1
  80. package/dist/app/chat/agentkit-chat/history.js +1 -1
  81. package/dist/app/chat/agentkit-chat/history.js.map +1 -1
  82. package/dist/app/chat/agentkit-chat/index.d.ts +1 -1
  83. package/dist/app/chat/agentkit-chat/index.d.ts.map +1 -1
  84. package/dist/app/chat/agentkit-chat/index.js +1 -1
  85. package/dist/app/chat/agentkit-chat/index.js.map +1 -1
  86. package/dist/app/chat/agentkit-chat/labels.d.ts +5 -0
  87. package/dist/app/chat/agentkit-chat/labels.d.ts.map +1 -0
  88. package/dist/app/chat/agentkit-chat/labels.js +245 -0
  89. package/dist/app/chat/agentkit-chat/labels.js.map +1 -0
  90. package/dist/app/chat/agentkit-chat/questions.d.ts +1 -1
  91. package/dist/app/chat/agentkit-chat/questions.d.ts.map +1 -1
  92. package/dist/app/chat/agentkit-chat/questions.js +1 -1
  93. package/dist/app/chat/agentkit-chat/questions.js.map +1 -1
  94. package/dist/app/chat/agentkit-chat/root.d.ts.map +1 -1
  95. package/dist/app/chat/agentkit-chat/root.js +5 -64
  96. package/dist/app/chat/agentkit-chat/root.js.map +1 -1
  97. package/dist/app/chat/agentkit-chat/work-step-labels.d.ts +2 -0
  98. package/dist/app/chat/agentkit-chat/work-step-labels.d.ts.map +1 -1
  99. package/dist/app/chat/agentkit-chat/work-step-labels.js +31 -0
  100. package/dist/app/chat/agentkit-chat/work-step-labels.js.map +1 -1
  101. package/dist/app/chat/chat/markdown-renderer.d.ts +2 -12
  102. package/dist/app/chat/chat/markdown-renderer.d.ts.map +1 -1
  103. package/dist/app/chat/chat/markdown-renderer.js +2 -41
  104. package/dist/app/chat/chat/markdown-renderer.js.map +1 -1
  105. package/dist/app/chat/chat/tool-call-display.d.ts.map +1 -1
  106. package/dist/app/chat/chat/tool-call-display.js +2 -2
  107. package/dist/app/chat/chat/tool-call-display.js.map +1 -1
  108. package/dist/app/chat/conversation/AgentConversation.d.ts.map +1 -1
  109. package/dist/app/chat/conversation/AgentConversation.js +2 -37
  110. package/dist/app/chat/conversation/AgentConversation.js.map +1 -1
  111. package/dist/app/chat/tool-cells/BashCell.d.ts +3 -1
  112. package/dist/app/chat/tool-cells/BashCell.d.ts.map +1 -1
  113. package/dist/app/chat/tool-cells/BashCell.js +4 -40
  114. package/dist/app/chat/tool-cells/BashCell.js.map +1 -1
  115. package/dist/app/chat/tool-cells/EditCell.d.ts.map +1 -1
  116. package/dist/app/chat/tool-cells/EditCell.js +5 -108
  117. package/dist/app/chat/tool-cells/EditCell.js.map +1 -1
  118. package/dist/app/chat/tool-cells/FilesChangedSummary.d.ts.map +1 -1
  119. package/dist/app/chat/tool-cells/FilesChangedSummary.js +23 -81
  120. package/dist/app/chat/tool-cells/FilesChangedSummary.js.map +1 -1
  121. package/dist/app/chat/tool-cells/WriteCell.d.ts.map +1 -1
  122. package/dist/app/chat/tool-cells/WriteCell.js +5 -24
  123. package/dist/app/chat/tool-cells/WriteCell.js.map +1 -1
  124. package/dist/app/code-highlighter.d.ts +11 -0
  125. package/dist/app/code-highlighter.d.ts.map +1 -0
  126. package/dist/app/code-highlighter.js +122 -0
  127. package/dist/app/code-highlighter.js.map +1 -0
  128. package/dist/app/context-xray/ThreadContextRing.js +3 -3
  129. package/dist/app/context-xray/ThreadContextRing.js.map +1 -1
  130. package/dist/app/i18n/catalogs/ar-SA.d.ts.map +1 -1
  131. package/dist/app/i18n/catalogs/ar-SA.js +22 -0
  132. package/dist/app/i18n/catalogs/ar-SA.js.map +1 -1
  133. package/dist/app/i18n/catalogs/de-DE.d.ts.map +1 -1
  134. package/dist/app/i18n/catalogs/de-DE.js +22 -0
  135. package/dist/app/i18n/catalogs/de-DE.js.map +1 -1
  136. package/dist/app/i18n/catalogs/en-US.d.ts +22 -0
  137. package/dist/app/i18n/catalogs/en-US.d.ts.map +1 -1
  138. package/dist/app/i18n/catalogs/en-US.js +22 -0
  139. package/dist/app/i18n/catalogs/en-US.js.map +1 -1
  140. package/dist/app/i18n/catalogs/es-ES.d.ts.map +1 -1
  141. package/dist/app/i18n/catalogs/es-ES.js +22 -0
  142. package/dist/app/i18n/catalogs/es-ES.js.map +1 -1
  143. package/dist/app/i18n/catalogs/fr-FR.d.ts.map +1 -1
  144. package/dist/app/i18n/catalogs/fr-FR.js +22 -0
  145. package/dist/app/i18n/catalogs/fr-FR.js.map +1 -1
  146. package/dist/app/i18n/catalogs/hi-IN.d.ts.map +1 -1
  147. package/dist/app/i18n/catalogs/hi-IN.js +22 -0
  148. package/dist/app/i18n/catalogs/hi-IN.js.map +1 -1
  149. package/dist/app/i18n/catalogs/ja-JP.d.ts.map +1 -1
  150. package/dist/app/i18n/catalogs/ja-JP.js +22 -0
  151. package/dist/app/i18n/catalogs/ja-JP.js.map +1 -1
  152. package/dist/app/i18n/catalogs/ko-KR.d.ts.map +1 -1
  153. package/dist/app/i18n/catalogs/ko-KR.js +22 -0
  154. package/dist/app/i18n/catalogs/ko-KR.js.map +1 -1
  155. package/dist/app/i18n/catalogs/pt-BR.d.ts.map +1 -1
  156. package/dist/app/i18n/catalogs/pt-BR.js +22 -0
  157. package/dist/app/i18n/catalogs/pt-BR.js.map +1 -1
  158. package/dist/app/i18n/catalogs/zh-CN.d.ts.map +1 -1
  159. package/dist/app/i18n/catalogs/zh-CN.js +22 -0
  160. package/dist/app/i18n/catalogs/zh-CN.js.map +1 -1
  161. package/dist/app/i18n/catalogs/zh-TW.d.ts.map +1 -1
  162. package/dist/app/i18n/catalogs/zh-TW.js +22 -0
  163. package/dist/app/i18n/catalogs/zh-TW.js.map +1 -1
  164. package/dist/app/onboarding/first-run-first-paint.d.ts +10 -0
  165. package/dist/app/onboarding/first-run-first-paint.d.ts.map +1 -0
  166. package/dist/app/onboarding/first-run-first-paint.js +22 -0
  167. package/dist/app/onboarding/first-run-first-paint.js.map +1 -0
  168. package/dist/app/onboarding/first-run-startup-gate.d.ts +3 -1
  169. package/dist/app/onboarding/first-run-startup-gate.d.ts.map +1 -1
  170. package/dist/app/onboarding/first-run-startup-gate.js +30 -6
  171. package/dist/app/onboarding/first-run-startup-gate.js.map +1 -1
  172. package/dist/app/providers/AppProviders.d.ts.map +1 -1
  173. package/dist/app/providers/AppProviders.js +9 -2
  174. package/dist/app/providers/AppProviders.js.map +1 -1
  175. package/dist/app/providers/BuilderSignupActivation.d.ts +2 -1
  176. package/dist/app/providers/BuilderSignupActivation.d.ts.map +1 -1
  177. package/dist/app/providers/BuilderSignupActivation.js +6 -3
  178. package/dist/app/providers/BuilderSignupActivation.js.map +1 -1
  179. package/dist/app/shared/AppShellSkeleton.d.ts.map +1 -1
  180. package/dist/app/shared/AppShellSkeleton.js +11 -1
  181. package/dist/app/shared/AppShellSkeleton.js.map +1 -1
  182. package/dist/app/styles/agent-native.css +2 -2
  183. package/dist/app/styles/tokens/agent-kit.css +20 -3
  184. package/dist/composer/AgentSuggestionBar.d.ts.map +1 -1
  185. package/dist/composer/AgentSuggestionBar.js +2 -2
  186. package/dist/composer/AgentSuggestionBar.js.map +1 -1
  187. package/dist/composer/ComposerContextMenu.js +4 -4
  188. package/dist/composer/ComposerContextMenu.js.map +1 -1
  189. package/dist/composer/ComposerContextPicker.d.ts.map +1 -1
  190. package/dist/composer/ComposerContextPicker.js +2 -2
  191. package/dist/composer/ComposerContextPicker.js.map +1 -1
  192. package/dist/composer/ComposerContextPickerDialog.d.ts.map +1 -1
  193. package/dist/composer/ComposerContextPickerDialog.js +2 -2
  194. package/dist/composer/ComposerContextPickerDialog.js.map +1 -1
  195. package/dist/composer/ComposerModePicker.d.ts.map +1 -1
  196. package/dist/composer/ComposerModePicker.js +2 -1
  197. package/dist/composer/ComposerModePicker.js.map +1 -1
  198. package/dist/composer/ComposerPlusMenu.js +1 -1
  199. package/dist/composer/ComposerPlusMenu.js.map +1 -1
  200. package/dist/composer/MentionChip.js +1 -1
  201. package/dist/composer/MentionChip.js.map +1 -1
  202. package/dist/composer/MentionPopover.d.ts +3 -0
  203. package/dist/composer/MentionPopover.d.ts.map +1 -1
  204. package/dist/composer/MentionPopover.js +34 -15
  205. package/dist/composer/MentionPopover.js.map +1 -1
  206. package/dist/composer/MessageQueueDrawer.d.ts.map +1 -1
  207. package/dist/composer/MessageQueueDrawer.js +14 -9
  208. package/dist/composer/MessageQueueDrawer.js.map +1 -1
  209. package/dist/composer/PastedTextChip.d.ts.map +1 -1
  210. package/dist/composer/PastedTextChip.js +14 -18
  211. package/dist/composer/PastedTextChip.js.map +1 -1
  212. package/dist/composer/PromptBar.js +1 -1
  213. package/dist/composer/PromptBar.js.map +1 -1
  214. package/dist/composer/PromptComposer.d.ts.map +1 -1
  215. package/dist/composer/PromptComposer.js +11 -9
  216. package/dist/composer/PromptComposer.js.map +1 -1
  217. package/dist/composer/RealtimeVoiceMode.js +2 -2
  218. package/dist/composer/RealtimeVoiceMode.js.map +1 -1
  219. package/dist/composer/TiptapComposer.d.ts.map +1 -1
  220. package/dist/composer/TiptapComposer.js +52 -89
  221. package/dist/composer/TiptapComposer.js.map +1 -1
  222. package/dist/composer/VoiceButton.d.ts +3 -1
  223. package/dist/composer/VoiceButton.d.ts.map +1 -1
  224. package/dist/composer/VoiceButton.js +10 -8
  225. package/dist/composer/VoiceButton.js.map +1 -1
  226. package/dist/composer/composer-notice.js +3 -3
  227. package/dist/composer/composer-notice.js.map +1 -1
  228. package/dist/composer/extensions/FileReference.js +1 -1
  229. package/dist/composer/extensions/FileReference.js.map +1 -1
  230. package/dist/composer/extensions/SkillReference.js +1 -1
  231. package/dist/composer/extensions/SkillReference.js.map +1 -1
  232. package/dist/composer/popover-surface.d.ts +1 -1
  233. package/dist/composer/popover-surface.d.ts.map +1 -1
  234. package/dist/composer/popover-surface.js +1 -1
  235. package/dist/composer/popover-surface.js.map +1 -1
  236. package/dist/context-ui/ContextUsageRing.d.ts +1 -45
  237. package/dist/context-ui/ContextUsageRing.d.ts.map +1 -1
  238. package/dist/context-ui/ContextUsageRing.js +2 -87
  239. package/dist/context-ui/ContextUsageRing.js.map +1 -1
  240. package/dist/context-ui/index.d.ts +2 -2
  241. package/dist/context-ui/index.d.ts.map +1 -1
  242. package/dist/context-ui/index.js +2 -2
  243. package/dist/context-ui/index.js.map +1 -1
  244. package/package.json +4 -4
  245. package/src/app/agentkit/react/approval-card.tsx +14 -2
  246. package/src/app/agentkit/react/code-block.spec.tsx +154 -0
  247. package/src/app/agentkit/react/code-block.tsx +317 -0
  248. package/src/app/agentkit/react/components.interaction.spec.tsx +7 -0
  249. package/src/app/agentkit/react/components.papercuts.spec.tsx +143 -2
  250. package/src/app/agentkit/react/components.spec.tsx +40 -47
  251. package/src/app/agentkit/react/components.tsx +1533 -413
  252. package/src/app/agentkit/react/composer-context.spec.tsx +35 -0
  253. package/src/app/agentkit/react/context-usage.tsx +353 -0
  254. package/src/app/agentkit/react/context.tsx +93 -1
  255. package/src/app/agentkit/react/disclosure-content.ts +42 -0
  256. package/src/app/agentkit/react/disclosures.spec.tsx +62 -4
  257. package/src/app/agentkit/react/duration.ts +22 -0
  258. package/src/app/agentkit/react/file-change.spec.tsx +124 -0
  259. package/src/app/agentkit/react/file-change.tsx +228 -0
  260. package/src/app/agentkit/react/file-diff.ts +110 -0
  261. package/src/app/agentkit/react/index.ts +7 -0
  262. package/src/app/agentkit/react/lifecycle.spec.tsx +565 -20
  263. package/src/app/agentkit/react/markdown.spec.tsx +18 -8
  264. package/src/app/agentkit/react/message-actions.spec.tsx +55 -1
  265. package/src/app/agentkit/react/notices.spec.tsx +4 -4
  266. package/src/app/agentkit/react/optimistic-send.spec.tsx +43 -0
  267. package/src/app/agentkit/react/question-card.spec.tsx +276 -0
  268. package/src/app/agentkit/react/question-card.tsx +533 -0
  269. package/src/app/agentkit/react/root.tsx +4 -0
  270. package/src/app/agentkit/react/roving-focus.spec.tsx +114 -0
  271. package/src/app/agentkit/react/roving-focus.ts +82 -0
  272. package/src/app/agentkit/react/stack-trace.spec.tsx +177 -0
  273. package/src/app/agentkit/react/stack-trace.tsx +281 -0
  274. package/src/app/agentkit/react/stop-button.spec.tsx +40 -0
  275. package/src/app/agentkit/react/stop-button.tsx +22 -5
  276. package/src/app/agentkit/react/styles.css +1306 -395
  277. package/src/app/agentkit/react/subagent-step.spec.tsx +554 -0
  278. package/src/app/agentkit/react/terminal.spec.tsx +228 -0
  279. package/src/app/agentkit/react/terminal.tsx +228 -0
  280. package/src/app/agentkit/react/trace.spec.tsx +120 -0
  281. package/src/app/agentkit/react/trace.tsx +239 -0
  282. package/src/app/blocks/library/HighlightedCode.tsx +14 -126
  283. package/src/app/chat/AgentKitAssistantChat.spec.tsx +28 -12
  284. package/src/app/chat/AgentKitAssistantChat.tsx +27 -253
  285. package/src/app/chat/agentkit-chat/approval-card.spec.tsx +1 -0
  286. package/src/app/chat/agentkit-chat/guided-questions.flow.spec.tsx +381 -0
  287. package/src/app/chat/agentkit-chat/guided-questions.tsx +236 -3
  288. package/src/app/chat/agentkit-chat/history.tsx +1 -1
  289. package/src/app/chat/agentkit-chat/index.ts +2 -0
  290. package/src/app/chat/agentkit-chat/labels.ts +259 -0
  291. package/src/app/chat/agentkit-chat/questions.ts +2 -0
  292. package/src/app/chat/agentkit-chat/root.spec.tsx +18 -0
  293. package/src/app/chat/agentkit-chat/root.tsx +5 -64
  294. package/src/app/chat/agentkit-chat/work-step-labels.ts +42 -0
  295. package/src/app/chat/chat/markdown-renderer.tsx +2 -59
  296. package/src/app/chat/chat/tool-call-display.spec.tsx +26 -14
  297. package/src/app/chat/chat/tool-call-display.tsx +2 -1
  298. package/src/app/chat/conversation/AgentConversation.tsx +2 -50
  299. package/src/app/chat/tool-cells/BashCell.tsx +14 -136
  300. package/src/app/chat/tool-cells/EditCell.tsx +11 -248
  301. package/src/app/chat/tool-cells/FilesChangedSummary.spec.tsx +62 -5
  302. package/src/app/chat/tool-cells/FilesChangedSummary.tsx +31 -162
  303. package/src/app/chat/tool-cells/WriteCell.tsx +11 -121
  304. package/src/app/code-highlighter.ts +131 -0
  305. package/src/app/context-xray/ThreadContextRing.tsx +7 -7
  306. package/src/app/i18n/catalogs/ar-SA.ts +22 -0
  307. package/src/app/i18n/catalogs/de-DE.ts +22 -0
  308. package/src/app/i18n/catalogs/en-US.ts +22 -0
  309. package/src/app/i18n/catalogs/es-ES.ts +22 -0
  310. package/src/app/i18n/catalogs/fr-FR.ts +22 -0
  311. package/src/app/i18n/catalogs/hi-IN.ts +22 -0
  312. package/src/app/i18n/catalogs/ja-JP.ts +22 -0
  313. package/src/app/i18n/catalogs/ko-KR.ts +22 -0
  314. package/src/app/i18n/catalogs/pt-BR.ts +22 -0
  315. package/src/app/i18n/catalogs/zh-CN.ts +22 -0
  316. package/src/app/i18n/catalogs/zh-TW.ts +22 -0
  317. package/src/app/onboarding/first-run-first-paint.spec.ts +39 -0
  318. package/src/app/onboarding/first-run-first-paint.ts +24 -0
  319. package/src/app/onboarding/first-run-startup-gate.spec.tsx +125 -13
  320. package/src/app/onboarding/first-run-startup-gate.tsx +48 -5
  321. package/src/app/providers/AppProviders.tsx +18 -0
  322. package/src/app/providers/BuilderSignupActivation.tsx +8 -2
  323. package/src/app/shared/AppShellSkeleton.spec.tsx +60 -1
  324. package/src/app/shared/AppShellSkeleton.tsx +31 -1
  325. package/src/app/styles/agent-native.css +2 -2
  326. package/src/app/styles/agent-native.spec.ts +2 -2
  327. package/src/app/styles/tokens/agent-kit.css +20 -3
  328. package/src/composer/AgentSuggestionBar.spec.tsx +3 -1
  329. package/src/composer/AgentSuggestionBar.tsx +5 -2
  330. package/src/composer/ComposerContextMenu.tsx +4 -4
  331. package/src/composer/ComposerContextPicker.tsx +8 -2
  332. package/src/composer/ComposerContextPickerDialog.tsx +5 -2
  333. package/src/composer/ComposerModePicker.tsx +2 -1
  334. package/src/composer/ComposerPlusMenu.tsx +1 -1
  335. package/src/composer/MentionChip.tsx +1 -1
  336. package/src/composer/MentionPopover.spec.tsx +108 -2
  337. package/src/composer/MentionPopover.tsx +93 -17
  338. package/src/composer/MessageQueueDrawer.spec.tsx +5 -5
  339. package/src/composer/MessageQueueDrawer.tsx +19 -12
  340. package/src/composer/PastedTextChip.spec.tsx +36 -0
  341. package/src/composer/PastedTextChip.tsx +52 -58
  342. package/src/composer/PromptBar.tsx +1 -1
  343. package/src/composer/PromptComposer.context.spec.tsx +2 -2
  344. package/src/composer/PromptComposer.submission.spec.tsx +27 -0
  345. package/src/composer/PromptComposer.tsx +17 -9
  346. package/src/composer/RealtimeVoiceMode.tsx +2 -2
  347. package/src/composer/TiptapComposer.model-picker.spec.tsx +23 -82
  348. package/src/composer/TiptapComposer.tsx +68 -145
  349. package/src/composer/VoiceButton.tsx +12 -7
  350. package/src/composer/composer-notice.tsx +3 -3
  351. package/src/composer/extensions/FileReference.tsx +1 -1
  352. package/src/composer/extensions/SkillReference.tsx +1 -1
  353. package/src/composer/popover-surface.ts +1 -1
  354. package/src/context-ui/ContextUsageRing.spec.tsx +33 -8
  355. package/src/context-ui/ContextUsageRing.tsx +13 -348
  356. package/src/context-ui/index.ts +7 -1
  357. package/dist/composer/ModelPickerParts.d.ts +0 -20
  358. package/dist/composer/ModelPickerParts.d.ts.map +0 -1
  359. package/dist/composer/ModelPickerParts.js +0 -77
  360. package/dist/composer/ModelPickerParts.js.map +0 -1
  361. package/src/composer/ModelPickerParts.tsx +0 -188
@@ -5,13 +5,11 @@ import {
5
5
  IconThumbDownFilled,
6
6
  IconThumbDown,
7
7
  IconActivity,
8
- IconAlertCircle,
9
8
  IconAlertTriangle,
10
9
  IconArrowDown,
11
10
  IconArrowFork,
12
11
  IconBook2,
13
12
  IconBrain,
14
- IconCheck,
15
13
  IconChevronLeft,
16
14
  IconChevronRight,
17
15
  IconCircle,
@@ -125,6 +123,10 @@ import {
125
123
  type AgentKitComposerSubmission,
126
124
  } from "./composer-submission.js";
127
125
  import { CoreFileStorageSetupContext } from "./core-file-storage.js";
126
+ import {
127
+ AgentKitDeferClosedContentContext,
128
+ useUntilFoundContent,
129
+ } from "./disclosure-content.js";
128
130
  export type { AgentKitComposerSubmission } from "./composer-submission.js";
129
131
 
130
132
  import { splitAgentKitMessageContext } from "@agent-native/agentkit";
@@ -153,9 +155,20 @@ import {
153
155
  type AgentThread,
154
156
  type AgentToolCall,
155
157
  type AgentWidget,
158
+ type CitationPart,
156
159
  type RunId,
157
160
  } from "@agent-native/agentkit/protocol";
158
161
 
162
+ import { prettyLanguageName } from "../../code-highlighter.js";
163
+ import {
164
+ CodeBlock,
165
+ CodeBlockActions,
166
+ CodeBlockContent,
167
+ CodeBlockCopyButton,
168
+ CodeBlockFooter,
169
+ CodeBlockHeader,
170
+ CodeBlockTitle,
171
+ } from "./code-block.js";
159
172
  import {
160
173
  defaultAgentKitLabels,
161
174
  useAgentCapability,
@@ -180,6 +193,8 @@ import {
180
193
  type AgentKitSuggestionsRenderProps,
181
194
  type AgentRunFailureRenderProps,
182
195
  } from "./context.js";
196
+ import { formatAgentKitDuration } from "./duration.js";
197
+ import { FileChange, type FileChangeData } from "./file-change.js";
183
198
  import { scheduleIdleMount } from "./idle-mount.js";
184
199
  import { orderMessageContent } from "./message-content-order.js";
185
200
  import {
@@ -187,12 +202,28 @@ import {
187
202
  agentKitTurnInProgress,
188
203
  pendingSubmissionFiles,
189
204
  } from "./pending-submissions.js";
205
+ import { parseStackTrace, StackTrace } from "./stack-trace.js";
206
+ import { Terminal } from "./terminal.js";
207
+
208
+ export { formatAgentKitDuration };
209
+ import { useRovingFocus } from "./roving-focus.js";
190
210
  import { useAgentKitStopButton } from "./stop-button.js";
191
211
  import {
192
212
  AgentStreamingText,
193
213
  usePrefersReducedMotion,
194
214
  useStreamingRevealOrigin,
195
215
  } from "./streaming-text.js";
216
+ import {
217
+ Trace,
218
+ TraceCollapsible,
219
+ TraceCollapsibleContent,
220
+ TraceCollapsibleTrigger,
221
+ TraceDescription,
222
+ TraceIcon,
223
+ TraceLine,
224
+ TraceTitle,
225
+ type TraceStatus,
226
+ } from "./trace.js";
196
227
  import { TranscriptGlide } from "./transcript-glide.js";
197
228
  import {
198
229
  workedForRowChevronStyle,
@@ -329,7 +360,7 @@ const AgentKitSurfaceBoundary = memo(function AgentKitSurfaceBoundary({
329
360
  data-render-surface={surface}
330
361
  role="alert"
331
362
  >
332
- <IconAlertCircle aria-hidden="true" className="agentkit-icon" />
363
+ <IconAlertTriangle aria-hidden="true" className="agentkit-icon" />
333
364
  <span>{labels.renderError}</span>
334
365
  </div>
335
366
  }
@@ -463,12 +494,31 @@ function AgentLink({
463
494
  );
464
495
  }
465
496
 
466
- function codeBlockLanguage(children: ReactNode): string | undefined {
497
+ function fencedCode(children: ReactNode): {
498
+ text: string;
499
+ language: string | undefined;
500
+ } {
467
501
  const code = Array.isArray(children) ? children[0] : children;
468
- if (!code || typeof code !== "object" || !("props" in code)) return undefined;
469
- const className = (code.props as { className?: unknown }).className;
470
- if (typeof className !== "string") return undefined;
471
- return /(?:^|\s)language-([\w+#.-]+)/.exec(className)?.[1];
502
+ if (!isValidElement<{ className?: unknown; children?: ReactNode }>(code)) {
503
+ return { text: nodeText(children), language: undefined };
504
+ }
505
+ const className = code.props.className;
506
+ return {
507
+ text: nodeText(code.props.children),
508
+ language:
509
+ typeof className === "string"
510
+ ? /(?:^|\s)language-([\w+#.-]+)/.exec(className)?.[1]
511
+ : undefined,
512
+ };
513
+ }
514
+
515
+ function nodeText(node: ReactNode): string {
516
+ if (typeof node === "string" || typeof node === "number") return `${node}`;
517
+ if (Array.isArray(node)) return node.map(nodeText).join("");
518
+ if (isValidElement<{ children?: ReactNode }>(node)) {
519
+ return nodeText(node.props.children);
520
+ }
521
+ return "";
472
522
  }
473
523
 
474
524
  function AgentMarkdownCodeBlock({
@@ -485,64 +535,36 @@ function AgentMarkdownCodeBlock({
485
535
  .filter((name) => name && name !== STREAMING_REVEAL_BLOCK_CLASS)
486
536
  .join(" ") || undefined;
487
537
  const { labels } = useAgentKit();
488
- const preRef = useRef<HTMLPreElement>(null);
489
- const [copied, setCopied] = useState(false);
490
- const copiedTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
491
- useEffect(
492
- () => () => {
493
- if (copiedTimer.current) clearTimeout(copiedTimer.current);
494
- },
495
- [],
496
- );
497
- const language = codeBlockLanguage(children);
498
- const copy = async () => {
499
- const text = preRef.current?.textContent ?? "";
500
- if (!(await writeClipboardText(text))) return;
501
- setCopied(true);
502
- if (copiedTimer.current) clearTimeout(copiedTimer.current);
503
- copiedTimer.current = setTimeout(() => setCopied(false), 2_000);
504
- };
538
+ const { text, language } = fencedCode(children);
539
+ const languageName = prettyLanguageName(language);
505
540
  return (
506
- <figure
507
- className={
508
- revealsAsBlock
509
- ? `agentkit-code-block ${STREAMING_REVEAL_BLOCK_CLASS}`
510
- : "agentkit-code-block"
511
- }
541
+ <CodeBlock
542
+ className={revealsAsBlock ? STREAMING_REVEAL_BLOCK_CLASS : undefined}
512
543
  >
513
- <div className="agentkit-code-block-header">
514
- <span className="agentkit-code-block-language" aria-hidden="true">
515
- {language}
516
- </span>
517
- <IconButton
518
- label={copied ? labels.codeCopied : labels.copyCode}
519
- icon={
520
- copied ? (
521
- <IconCheck aria-hidden="true" size={16} />
522
- ) : (
523
- <IconCopy aria-hidden="true" size={16} />
524
- )
525
- }
526
- size="compact"
527
- className="size-6"
528
- onPress={() => void copy()}
529
- />
530
- </div>
531
- <pre
544
+ <CodeBlockHeader>
545
+ <CodeBlockTitle aria-hidden="true">{languageName}</CodeBlockTitle>
546
+ <CodeBlockActions>
547
+ <CodeBlockCopyButton value={text} />
548
+ </CodeBlockActions>
549
+ </CodeBlockHeader>
550
+ <CodeBlockContent
532
551
  {...props}
533
552
  className={preClassName}
534
- ref={preRef}
535
- tabIndex={0}
536
- role="region"
553
+ language={language}
554
+ // A group, not a region: a landmark per code block buries the real ones.
555
+ role="group"
537
556
  aria-label={
538
- language
539
- ? labels.codeBlockLanguage.replace("{{language}}", () => language)
557
+ languageName
558
+ ? labels.codeBlockLanguage.replace(
559
+ "{{language}}",
560
+ () => languageName,
561
+ )
540
562
  : labels.codeBlock
541
563
  }
542
564
  >
543
- {children}
544
- </pre>
545
- </figure>
565
+ {text}
566
+ </CodeBlockContent>
567
+ </CodeBlock>
546
568
  );
547
569
  }
548
570
 
@@ -557,7 +579,7 @@ function AgentMarkdownTable({
557
579
  <div
558
580
  className="agentkit-markdown-table"
559
581
  tabIndex={0}
560
- role="region"
582
+ role="group"
561
583
  aria-label={labels.table}
562
584
  >
563
585
  <table ref={table} {...props} />
@@ -804,6 +826,8 @@ export interface AgentKitDisclosureSummaryProps {
804
826
  rowClassName?: string | undefined;
805
827
  style?: CSSProperties | undefined;
806
828
  chevronStyle?: CSSProperties | undefined;
829
+ /** Omit for finished work. */
830
+ status?: TraceStatus | undefined;
807
831
  /** Replaces the default icon, label and chevron. */
808
832
  children?: ReactNode;
809
833
  }
@@ -811,7 +835,7 @@ export interface AgentKitDisclosureSummaryProps {
811
835
  const DISCLOSURE_ROW_INTERACTIVE =
812
836
  "button, a, input, textarea, select, [role='button'], [contenteditable='true']";
813
837
 
814
- /** One summary row for every AgentKit disclosure: icon, label, then chevron. */
838
+ /** The label keeps its own styling; `TraceTitle` is for host rows. */
815
839
  export function AgentKitDisclosureSummary({
816
840
  open,
817
841
  onToggle,
@@ -825,16 +849,17 @@ export function AgentKitDisclosureSummary({
825
849
  rowClassName,
826
850
  style,
827
851
  chevronStyle,
852
+ status,
828
853
  children,
829
854
  }: AgentKitDisclosureSummaryProps) {
830
855
  const content = children ?? (
831
856
  <>
832
- {icon !== undefined ? (
833
- <span className="agentkit-disclosure-icon-slot" aria-hidden="true">
834
- {icon}
857
+ <TraceLine status={status}>
858
+ {icon !== undefined ? <TraceIcon>{icon}</TraceIcon> : null}
859
+ <span data-slot="trace-title" className="agentkit-disclosure-label">
860
+ {label}
835
861
  </span>
836
- ) : null}
837
- <span className="agentkit-disclosure-label">{label}</span>
862
+ </TraceLine>
838
863
  {expandable && as === "button" ? (
839
864
  <IconChevronRight
840
865
  aria-hidden="true"
@@ -859,6 +884,7 @@ export function AgentKitDisclosureSummary({
859
884
  ) : (
860
885
  <button
861
886
  type="button"
887
+ data-slot={expandable ? "trace-collapsible-trigger" : undefined}
862
888
  className={["agentkit-disclosure-summary", className]
863
889
  .filter(Boolean)
864
890
  .join(" ")}
@@ -906,14 +932,7 @@ export function AgentKitDisclosureSummary({
906
932
  );
907
933
  }
908
934
 
909
- /** True while a long thread opens, so closed rows can mount later. */
910
- const AgentKitDeferClosedContentContext = createContext(false);
911
-
912
- /**
913
- * Mounted while closed, like <details>, so find-in-page can reveal it. In a
914
- * long thread, closed content mounts in the next idle period rather than with
915
- * the transcript, so the history paints before every collapsed step renders.
916
- */
935
+ /** Mounted while closed, like <details>, so find-in-page can reveal it. */
917
936
  export function AgentKitDisclosureContent({
918
937
  id,
919
938
  open,
@@ -925,32 +944,17 @@ export function AgentKitDisclosureContent({
925
944
  onReveal?: (() => void) | undefined;
926
945
  children: ReactNode;
927
946
  }) {
928
- const ref = useRef<HTMLDivElement>(null);
929
- const revealRef = useRef(onReveal);
930
- revealRef.current = onReveal;
931
- const deferClosed = useContext(AgentKitDeferClosedContentContext);
932
- const [contentMounted, setContentMounted] = useState(open || !deferClosed);
933
- if (open && !contentMounted) setContentMounted(true);
934
- useEffect(() => {
935
- if (contentMounted) return;
936
- return scheduleIdleMount(() => setContentMounted(true));
937
- }, [contentMounted]);
938
- useEffect(() => {
939
- const element = ref.current;
940
- if (!element || open) return;
941
- element.setAttribute("hidden", "until-found");
942
- const reveal = () => revealRef.current?.();
943
- element.addEventListener("beforematch", reveal);
944
- return () => element.removeEventListener("beforematch", reveal);
945
- }, [open]);
947
+ const { ref, mounted } = useUntilFoundContent(open, onReveal);
946
948
  return (
947
949
  <div
948
950
  ref={ref}
949
951
  id={id}
952
+ data-slot="trace-collapsible-content"
953
+ data-state={open ? "open" : "closed"}
950
954
  className="agentkit-disclosure-content"
951
955
  hidden={!open}
952
956
  >
953
- {contentMounted || open ? children : null}
957
+ {mounted ? children : null}
954
958
  </div>
955
959
  );
956
960
  }
@@ -970,7 +974,7 @@ function interactionIcon(kind: string): ReactNode {
970
974
  case "completed":
971
975
  return <IconCircleCheck {...props} />;
972
976
  case "failed":
973
- return <IconAlertCircle {...props} />;
977
+ return <IconAlertTriangle {...props} />;
974
978
  case "closed":
975
979
  return <IconX {...props} />;
976
980
  default:
@@ -1497,6 +1501,82 @@ function formatActivityToolValue(
1497
1501
  return `${formatted.slice(0, contentLimit).trimEnd()}…${omissionMarker}`;
1498
1502
  }
1499
1503
 
1504
+ interface ActivityToolInput {
1505
+ text: string;
1506
+ language?: string | undefined;
1507
+ /** The call's other arguments, when `text` is its source. */
1508
+ options?: Array<[string, string]>;
1509
+ }
1510
+
1511
+ /** Fields whose string is the program a tool runs, with its grammar. */
1512
+ const ACTIVITY_TOOL_SOURCE_FIELDS: Record<string, string | undefined> = {
1513
+ command: "bash",
1514
+ script: undefined,
1515
+ code: undefined,
1516
+ };
1517
+
1518
+ function formatActivityToolInput(
1519
+ value: unknown,
1520
+ labels: Partial<AgentKitLabels> = {},
1521
+ ): ActivityToolInput | undefined {
1522
+ const source = activityToolSource(value);
1523
+ if (!source) {
1524
+ const text = formatActivityToolValue(value, labels);
1525
+ return text ? { text } : undefined;
1526
+ }
1527
+ const text = formatActivityToolValue(source.text, labels);
1528
+ if (!text) return undefined;
1529
+ const options = Object.entries(source.rest).flatMap(
1530
+ ([key, option]): Array<[string, string]> => {
1531
+ const formatted = formatActivityToolValue(option, labels);
1532
+ return formatted ? [[key, formatted.replace(/\s+/g, " ")]] : [];
1533
+ },
1534
+ );
1535
+ return {
1536
+ text,
1537
+ language: source.language,
1538
+ ...(options.length ? { options } : {}),
1539
+ };
1540
+ }
1541
+
1542
+ function activityToolSource(value: unknown):
1543
+ | {
1544
+ text: string;
1545
+ language: string | undefined;
1546
+ rest: Record<string, unknown>;
1547
+ }
1548
+ | undefined {
1549
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1550
+ return undefined;
1551
+ }
1552
+ const input = value as Record<string, unknown>;
1553
+ const field = Object.keys(ACTIVITY_TOOL_SOURCE_FIELDS).find((key) => {
1554
+ const text = input[key];
1555
+ // `code` also names short values such as a country or a promo code.
1556
+ return (
1557
+ typeof text === "string" &&
1558
+ (key === "command" ? text.trim() !== "" : /\n|.{40}/.test(text))
1559
+ );
1560
+ });
1561
+ if (!field) return undefined;
1562
+ const languageField = ["language", "lang"].find(
1563
+ (key) => typeof input[key] === "string",
1564
+ );
1565
+ const rest = Object.fromEntries(
1566
+ Object.entries(input).filter(
1567
+ ([key, option]) =>
1568
+ key !== field && key !== languageField && option !== undefined,
1569
+ ),
1570
+ );
1571
+ return {
1572
+ text: input[field] as string,
1573
+ language: languageField
1574
+ ? (input[languageField] as string)
1575
+ : ACTIVITY_TOOL_SOURCE_FIELDS[field],
1576
+ rest,
1577
+ };
1578
+ }
1579
+
1500
1580
  function activityLabel(activity: AgentActivity, workingLabel: string): string {
1501
1581
  const label = readableText(activity.label);
1502
1582
  if (label.toLowerCase() === "processing") return workingLabel;
@@ -1520,6 +1600,16 @@ function AgentIdentityChip({
1520
1600
  kind?: string;
1521
1601
  status?: AgentParticipant["status"];
1522
1602
  }) {
1603
+ const { labels } = useAgentKit();
1604
+ // The chip's tint and shimmer carry the state, so it is said in words too.
1605
+ const statusLabel =
1606
+ status === "working"
1607
+ ? labels.taskStatusRunning
1608
+ : status === "failed"
1609
+ ? labels.taskStatusFailed
1610
+ : status === "waiting"
1611
+ ? labels.taskStatusAwaitingInput
1612
+ : undefined;
1523
1613
  return (
1524
1614
  <span
1525
1615
  className="agentkit-agent"
@@ -1527,9 +1617,15 @@ function AgentIdentityChip({
1527
1617
  data-agent-kind={kind}
1528
1618
  data-status={status}
1529
1619
  >
1620
+ {status === "failed" ? (
1621
+ <IconAlertTriangle aria-hidden="true" className="agentkit-icon" />
1622
+ ) : null}
1530
1623
  <span className="agentkit-agent-name">
1531
1624
  {readableText(name) || fallbackName}
1532
1625
  </span>
1626
+ {statusLabel ? (
1627
+ <span className="agentkit-visually-hidden">{statusLabel}</span>
1628
+ ) : null}
1533
1629
  </span>
1534
1630
  );
1535
1631
  }
@@ -1565,8 +1661,11 @@ export function AgentInteractionItem({
1565
1661
  value: interaction,
1566
1662
  threadId,
1567
1663
  }: AgentKitRenderProps<AgentInteraction>) {
1568
- const { slots, registry, labels } = useAgentKit();
1664
+ const { slots, registry, labels, formatAgentInteractionDetail } =
1665
+ useAgentKit();
1569
1666
  const thread = useAgentThreadSlice(sameAgents, threadId);
1667
+ const showAgent =
1668
+ useContext(AgentSubagentStepContext) !== interaction.agentId;
1570
1669
  const agent = thread.agents[interaction.agentId];
1571
1670
  const target = interaction.targetAgentId
1572
1671
  ? thread.agents[interaction.targetAgentId]
@@ -1604,16 +1703,21 @@ export function AgentInteractionItem({
1604
1703
  const visibleLabel =
1605
1704
  humanizeAgentLabel(label) ||
1606
1705
  defaultInteractionLabel({ ...interaction, label: undefined }, labels);
1607
- const detail = readableText(interaction.detail);
1706
+ const detail = readableText(
1707
+ formatAgentInteractionDetail?.(interaction) ?? interaction.detail,
1708
+ );
1608
1709
  return (
1609
1710
  <div
1610
1711
  className="agentkit-agent-interaction"
1611
1712
  data-kind={interaction.kind}
1612
1713
  data-scope={interaction.scope}
1613
1714
  >
1614
- <span className="agentkit-agent-interaction-head">
1615
- {interactionIcon(interaction.kind)}
1616
- {agent && AgentRenderer ? (
1715
+ <TraceLine
1716
+ className="agentkit-agent-interaction-head"
1717
+ status={interaction.kind === "failed" ? "error" : undefined}
1718
+ >
1719
+ <TraceIcon>{interactionIcon(interaction.kind)}</TraceIcon>
1720
+ {!showAgent ? null : AgentRenderer ? (
1617
1721
  <AgentRenderer value={agent} threadId={threadId} />
1618
1722
  ) : (
1619
1723
  <AgentIdentityChip
@@ -1622,7 +1726,12 @@ export function AgentInteractionItem({
1622
1726
  fallbackName={labels.assistant}
1623
1727
  />
1624
1728
  )}
1625
- <span className="agentkit-agent-interaction-label">{visibleLabel}</span>
1729
+ <span
1730
+ data-slot="trace-title"
1731
+ className="agentkit-agent-interaction-label"
1732
+ >
1733
+ {visibleLabel}
1734
+ </span>
1626
1735
  {target && TargetRenderer ? (
1627
1736
  <TargetRenderer value={target} threadId={threadId} />
1628
1737
  ) : interaction.targetAgentId ? (
@@ -1633,7 +1742,7 @@ export function AgentInteractionItem({
1633
1742
  />
1634
1743
  ) : null}
1635
1744
  {object ? <ObjectRenderer value={object} threadId={threadId} /> : null}
1636
- </span>
1745
+ </TraceLine>
1637
1746
  {detail && (!object || interaction.kind === "failed") ? (
1638
1747
  <span
1639
1748
  {...(interaction.kind === "failed"
@@ -1685,6 +1794,7 @@ export function AgentActivityItem({
1685
1794
  previous.tools[activity.id] === next.tools[activity.id],
1686
1795
  );
1687
1796
  const [open, setOpen] = useState(false);
1797
+ const showAgent = useContext(AgentSubagentStepContext) !== activity.agentId;
1688
1798
  const agent = activity.agentId ? thread.agents[activity.agentId] : undefined;
1689
1799
  const agentName = resolveAgentName(
1690
1800
  thread,
@@ -1694,13 +1804,26 @@ export function AgentActivityItem({
1694
1804
  );
1695
1805
  const tool = thread.tools[activity.id] as AgentToolCall | undefined;
1696
1806
  const toolInput = useMemo(
1697
- () => (tool ? formatActivityToolValue(tool.input, labels) : undefined),
1807
+ () => (tool ? formatActivityToolInput(tool.input, labels) : undefined),
1698
1808
  [labels, tool],
1699
1809
  );
1700
1810
  const toolResult = useMemo(
1701
1811
  () => (tool ? formatActivityToolValue(tool.output, labels) : undefined),
1702
1812
  [labels, tool],
1703
1813
  );
1814
+ const stackTrace = useMemo(() => {
1815
+ if (!tool) return undefined;
1816
+ const sources = [
1817
+ toolResult,
1818
+ formatActivityToolValue(tool.error?.details, labels),
1819
+ formatActivityToolValue(tool.error?.message, labels),
1820
+ ];
1821
+ for (const source of sources) {
1822
+ const parsed = source ? parseStackTrace(source) : undefined;
1823
+ if (parsed) return { parsed, fromResult: source === toolResult };
1824
+ }
1825
+ return undefined;
1826
+ }, [labels, tool, toolResult]);
1704
1827
  const toolInputLabel = labels.toolInput;
1705
1828
  const toolResultLabel = labels.toolResult;
1706
1829
  const expandable = Boolean(
@@ -1716,6 +1839,14 @@ export function AgentActivityItem({
1716
1839
  activity.object ??
1717
1840
  sourceObjectUnlessRepresented(activity.source, thread, [activity.agentId]);
1718
1841
  const failed = activity.status === "failed";
1842
+ const statusWords =
1843
+ activity.status === "running"
1844
+ ? labels.taskStatusRunning
1845
+ : failed
1846
+ ? labels.taskStatusFailed
1847
+ : activity.status === "cancelled"
1848
+ ? labels.runStopped
1849
+ : undefined;
1719
1850
  const errorPreview = activityErrorPreview(activity, tool, labels);
1720
1851
  // A tool call names what it acted on, so rows in a group tell apart.
1721
1852
  const toolObject =
@@ -1726,134 +1857,347 @@ export function AgentActivityItem({
1726
1857
  tool || failed
1727
1858
  ? readableText(toolObject?.text)
1728
1859
  : readableText(activity.detail);
1729
- const summaryId = useId();
1730
- return (
1731
- <div
1732
- className="agentkit-activity-item"
1733
- data-activity-kind={activity.kind}
1734
- data-status={activity.status}
1735
- data-open={expandable && open ? "true" : "false"}
1736
- >
1737
- <AgentKitDisclosureSummary
1738
- rowClassName="agentkit-activity-row"
1739
- open={open}
1740
- onToggle={() => setOpen((current) => !current)}
1741
- expandable={expandable}
1742
- as={expandable ? "button" : "static"}
1743
- controls={open ? summaryId : undefined}
1744
- icon={
1745
- failed ? (
1746
- <IconAlertTriangle aria-hidden="true" className="agentkit-icon" />
1747
- ) : (
1748
- <ActivitySourceIcon activity={activity} threadId={threadId} />
1749
- )
1750
- }
1751
- label={
1752
- <>
1753
- {agent && AgentRenderer ? (
1754
- <AgentRenderer value={agent} threadId={threadId} />
1755
- ) : activity.agentId ? (
1756
- <AgentIdentityChip
1757
- id={activity.agentId}
1758
- name={agentName}
1759
- fallbackName={labels.assistant}
1760
- />
1761
- ) : null}
1762
- <span className="agentkit-activity-label">
1763
- {activityLabel(activity, labels.working)}
1764
- </span>
1765
- </>
1766
- }
1767
- meta={
1768
- object || detail ? (
1769
- <>
1770
- {object ? (
1771
- <ObjectRenderer value={object} threadId={threadId} />
1772
- ) : null}
1773
- {detail ? (
1774
- <span
1775
- {...(toolObject ? SESSION_REPLAY_MASK_PROPS : {})}
1776
- title={toolObject ? undefined : detail}
1777
- className="agentkit-activity-detail"
1778
- data-kind={toolObject?.kind}
1779
- >
1780
- {detail}
1781
- </span>
1782
- ) : null}
1783
- </>
1784
- ) : null
1785
- }
1786
- />
1787
- {errorPreview ? (
1788
- // Replays mask text but record attributes, so failure text gets no title.
1789
- <span
1790
- {...SESSION_REPLAY_MASK_PROPS}
1791
- className="agentkit-activity-error"
1860
+ const line = (
1861
+ <TraceLine status={traceStatus(activity.status)}>
1862
+ <TraceIcon>
1863
+ {failed ? (
1864
+ <IconAlertTriangle aria-hidden="true" className="agentkit-icon" />
1865
+ ) : (
1866
+ <ActivitySourceIcon activity={activity} threadId={threadId} />
1867
+ )}
1868
+ </TraceIcon>
1869
+ <span className="agentkit-disclosure-label">
1870
+ {!showAgent ? null : agent && AgentRenderer ? (
1871
+ <AgentRenderer value={agent} threadId={threadId} />
1872
+ ) : activity.agentId ? (
1873
+ <AgentIdentityChip
1874
+ id={activity.agentId}
1875
+ name={agentName}
1876
+ fallbackName={labels.assistant}
1877
+ />
1878
+ ) : null}
1879
+ <TraceTitle className="agentkit-activity-label">
1880
+ {activityLabel(activity, labels.working)}
1881
+ </TraceTitle>
1882
+ {/* State shows only as color, motion and icon, so say it in words too. */}
1883
+ {statusWords ? (
1884
+ <span className="agentkit-visually-hidden">{statusWords}</span>
1885
+ ) : null}
1886
+ </span>
1887
+ </TraceLine>
1888
+ );
1889
+ const meta = (
1890
+ <>
1891
+ {object ? <ObjectRenderer value={object} threadId={threadId} /> : null}
1892
+ {detail ? (
1893
+ <TraceDescription
1894
+ {...(toolObject ? SESSION_REPLAY_MASK_PROPS : {})}
1895
+ title={toolObject ? undefined : detail}
1896
+ className="agentkit-activity-detail"
1897
+ data-kind={toolObject?.kind}
1792
1898
  >
1793
- {errorPreview}
1794
- </span>
1899
+ {detail}
1900
+ </TraceDescription>
1795
1901
  ) : null}
1796
- {open ? (
1797
- // Summary parts can render links, and replays keep their attributes.
1798
- <div
1799
- {...(activity.status === "failed" ? SESSION_REPLAY_BLOCK_PROPS : {})}
1800
- id={summaryId}
1801
- className="agentkit-activity-summary"
1802
- >
1803
- {activity.summary?.map((part, index) => {
1804
- const safePart =
1805
- part.type === "text"
1806
- ? {
1807
- ...part,
1808
- text: part.text.trim()
1809
- ? formatActivityText(part.text, labels)
1810
- : "",
1811
- }
1812
- : part;
1813
- return safePart.type === "text" && !safePart.text ? null : (
1814
- <AgentMessagePartView
1815
- key={`${activity.id}-summary-${index}`}
1816
- value={safePart}
1817
- threadId={threadId}
1818
- />
1819
- );
1820
- })}
1821
- {toolInput ? (
1822
- <div className="agentkit-activity-summary-section">
1823
- <div className="agentkit-activity-summary-label">
1824
- {toolInputLabel}
1825
- </div>
1826
- <pre
1827
- role="region"
1828
- aria-label={toolInputLabel}
1829
- tabIndex={0}
1830
- className="agentkit-activity-summary-value"
1831
- >
1832
- {toolInput}
1833
- </pre>
1834
- </div>
1835
- ) : null}
1836
- {toolResult ? (
1837
- <div className="agentkit-activity-summary-section">
1838
- <div className="agentkit-activity-summary-label">
1839
- {toolResultLabel}
1840
- </div>
1841
- <pre
1842
- role="region"
1843
- aria-label={toolResultLabel}
1844
- tabIndex={0}
1845
- className="agentkit-activity-summary-value"
1846
- >
1847
- {toolResult}
1848
- </pre>
1849
- </div>
1850
- ) : null}
1902
+ </>
1903
+ );
1904
+ const errorLine = errorPreview ? (
1905
+ // Replays mask text but record attributes, so failure text gets no title.
1906
+ <span {...SESSION_REPLAY_MASK_PROPS} className="agentkit-activity-error">
1907
+ {errorPreview}
1908
+ </span>
1909
+ ) : null;
1910
+ const rootProps = {
1911
+ className: "agentkit-activity-item",
1912
+ "data-activity-kind": activity.kind,
1913
+ "data-status": activity.status,
1914
+ "data-open": expandable && open ? "true" : "false",
1915
+ };
1916
+ const fileChange = tool ? fileChangeTool(tool) : undefined;
1917
+ if (fileChange) {
1918
+ return (
1919
+ <div {...rootProps}>
1920
+ <FileChange
1921
+ change={fileChange}
1922
+ running={activity.status === "running"}
1923
+ />
1924
+ {errorLine}
1925
+ </div>
1926
+ );
1927
+ }
1928
+ const terminal = tool ? terminalTool(tool) : undefined;
1929
+ if (terminal) {
1930
+ return (
1931
+ <div {...rootProps}>
1932
+ <Terminal
1933
+ command={terminal.command}
1934
+ output={typeof tool?.output === "string" ? tool.output : toolResult}
1935
+ running={activity.status === "running"}
1936
+ exitCode={terminal.exitCode}
1937
+ durationMs={terminal.durationMs}
1938
+ timedOut={terminal.timedOut}
1939
+ failed={failed}
1940
+ />
1941
+ </div>
1942
+ );
1943
+ }
1944
+ if (!expandable) {
1945
+ return (
1946
+ <div {...rootProps}>
1947
+ <div className="agentkit-disclosure-row agentkit-activity-row">
1948
+ <span className="agentkit-disclosure-summary" data-expandable="false">
1949
+ {line}
1950
+ </span>
1951
+ {meta}
1851
1952
  </div>
1953
+ {errorLine}
1954
+ </div>
1955
+ );
1956
+ }
1957
+ return (
1958
+ <TraceCollapsible {...rootProps} open={open} onOpenChange={setOpen}>
1959
+ <div
1960
+ className="agentkit-disclosure-row agentkit-activity-row"
1961
+ data-expandable="true"
1962
+ onClick={(event) => {
1963
+ // Meta controls handle their own clicks; a text-selecting drag is no toggle.
1964
+ const target = event.target;
1965
+ if (
1966
+ target instanceof Element &&
1967
+ target.closest(DISCLOSURE_ROW_INTERACTIVE)
1968
+ )
1969
+ return;
1970
+ if (globalThis.getSelection()?.toString()) return;
1971
+ setOpen((current) => !current);
1972
+ }}
1973
+ >
1974
+ <TraceCollapsibleTrigger>{line}</TraceCollapsibleTrigger>
1975
+ {meta}
1976
+ </div>
1977
+ {errorLine}
1978
+ <TraceCollapsibleContent
1979
+ // A fresh node on each toggle: the recorder can drop a block marker
1980
+ // changed on a node it has recorded, and the closed body stays mounted.
1981
+ key={open ? "open" : "closed"}
1982
+ keepMounted
1983
+ {...(activity.status === "failed" || !open
1984
+ ? SESSION_REPLAY_BLOCK_PROPS
1985
+ : {})}
1986
+ className="agentkit-activity-summary"
1987
+ >
1988
+ {activity.summary?.map((part, index) => {
1989
+ const safePart =
1990
+ part.type === "text"
1991
+ ? {
1992
+ ...part,
1993
+ text: part.text.trim()
1994
+ ? formatActivityText(part.text, labels)
1995
+ : "",
1996
+ }
1997
+ : part;
1998
+ return safePart.type === "text" && !safePart.text ? null : (
1999
+ <AgentMessagePartView
2000
+ key={`${activity.id}-summary-${index}`}
2001
+ value={safePart}
2002
+ threadId={threadId}
2003
+ />
2004
+ );
2005
+ })}
2006
+ {toolInput ? (
2007
+ <ActivityToolValue
2008
+ label={toolInputLabel}
2009
+ value={toolInput.text}
2010
+ language={toolInput.language}
2011
+ options={toolInput.options}
2012
+ />
2013
+ ) : null}
2014
+ {stackTrace ? (
2015
+ <StackTrace trace={stackTrace.parsed} defaultOpen />
2016
+ ) : null}
2017
+ {toolResult && !(stackTrace?.fromResult && stackTrace.parsed.whole) ? (
2018
+ <ActivityToolValue label={toolResultLabel} value={toolResult} />
2019
+ ) : null}
2020
+ </TraceCollapsibleContent>
2021
+ </TraceCollapsible>
2022
+ );
2023
+ }
2024
+
2025
+ function terminalTool(tool: AgentToolCall):
2026
+ | {
2027
+ command: string;
2028
+ exitCode: number | null | undefined;
2029
+ durationMs: number | undefined;
2030
+ timedOut: boolean | undefined;
2031
+ }
2032
+ | undefined {
2033
+ const metadata = tool.metadata;
2034
+ if (metadata?.toolKind !== "bash" || typeof metadata.command !== "string") {
2035
+ return undefined;
2036
+ }
2037
+ return {
2038
+ command: metadata.command,
2039
+ exitCode:
2040
+ typeof metadata.exitCode === "number" || metadata.exitCode === null
2041
+ ? metadata.exitCode
2042
+ : undefined,
2043
+ durationMs:
2044
+ typeof metadata.durationMs === "number" ? metadata.durationMs : undefined,
2045
+ timedOut: metadata.timedOut === true ? true : undefined,
2046
+ };
2047
+ }
2048
+
2049
+ function fileChangeTool(tool: AgentToolCall): FileChangeData | undefined {
2050
+ const metadata = tool.metadata;
2051
+ const kind = metadata?.toolKind;
2052
+ if (
2053
+ (kind !== "edit" && kind !== "write") ||
2054
+ typeof metadata?.filePath !== "string"
2055
+ ) {
2056
+ return undefined;
2057
+ }
2058
+ const text = (value: unknown) =>
2059
+ typeof value === "string" ? value : undefined;
2060
+ return {
2061
+ filePath: metadata.filePath,
2062
+ kind,
2063
+ oldText: text(metadata.oldText),
2064
+ newText: text(metadata.newText),
2065
+ content: text(metadata.content),
2066
+ lineCount:
2067
+ typeof metadata.lineCount === "number" ? metadata.lineCount : undefined,
2068
+ truncated: metadata.truncated === true,
2069
+ };
2070
+ }
2071
+
2072
+ function ActivityToolValue({
2073
+ label,
2074
+ value,
2075
+ language: sourceLanguage,
2076
+ options,
2077
+ }: {
2078
+ label: string;
2079
+ value: string;
2080
+ language?: string | undefined;
2081
+ options?: Array<[string, string]> | undefined;
2082
+ }) {
2083
+ const language = useMemo(
2084
+ () => sourceLanguage ?? (isJsonText(value) ? "json" : undefined),
2085
+ [sourceLanguage, value],
2086
+ );
2087
+ return (
2088
+ <CodeBlock size="sm" className="agentkit-activity-summary-section">
2089
+ <CodeBlockHeader>
2090
+ <CodeBlockTitle>{label}</CodeBlockTitle>
2091
+ <CodeBlockActions>
2092
+ <CodeBlockCopyButton value={value} />
2093
+ </CodeBlockActions>
2094
+ </CodeBlockHeader>
2095
+ <CodeBlockContent
2096
+ wrap
2097
+ language={language}
2098
+ role="group"
2099
+ aria-label={label}
2100
+ className="agentkit-activity-summary-value"
2101
+ >
2102
+ {value}
2103
+ </CodeBlockContent>
2104
+ {options ? (
2105
+ <CodeBlockFooter>
2106
+ <dl className="agentkit-activity-tool-options">
2107
+ {options.map(([key, option]) => (
2108
+ <div key={key}>
2109
+ <dt>{key}</dt>
2110
+ <dd>{option}</dd>
2111
+ </div>
2112
+ ))}
2113
+ </dl>
2114
+ </CodeBlockFooter>
1852
2115
  ) : null}
1853
- </div>
2116
+ </CodeBlock>
1854
2117
  );
1855
2118
  }
1856
2119
 
2120
+ function isJsonText(text: string): boolean {
2121
+ const start = text.trimStart()[0];
2122
+ if (start !== "{" && start !== "[") return false;
2123
+ try {
2124
+ JSON.parse(text);
2125
+ return true;
2126
+ // coercion-ok: text that does not parse is the predicate's false answer.
2127
+ } catch {
2128
+ return false;
2129
+ }
2130
+ }
2131
+
2132
+ /** The same page cited twice counts as one source. */
2133
+ export function AgentSources({ citations }: { citations: CitationPart[] }) {
2134
+ const { labels } = useAgentKit();
2135
+ const sources = useMemo(() => {
2136
+ const seen = new Set<string>();
2137
+ return citations.filter((citation) => {
2138
+ const key = safeAgentHref(citation.url) ?? `title:${citation.title}`;
2139
+ if (seen.has(key)) return false;
2140
+ seen.add(key);
2141
+ return true;
2142
+ });
2143
+ }, [citations]);
2144
+ const label = (
2145
+ sources.length === 1
2146
+ ? labels.workStepSourcesOne
2147
+ : labels.workStepSourcesOther
2148
+ ).replace("{{count}}", () => `${sources.length}`);
2149
+ return (
2150
+ <Trace className="agentkit-sources">
2151
+ <TraceCollapsible>
2152
+ <TraceCollapsibleTrigger>
2153
+ <TraceLine>
2154
+ <TraceIcon>
2155
+ <IconWorld aria-hidden="true" className="agentkit-icon" />
2156
+ </TraceIcon>
2157
+ <span className="agentkit-disclosure-label">{label}</span>
2158
+ </TraceLine>
2159
+ </TraceCollapsibleTrigger>
2160
+ <TraceCollapsibleContent keepMounted>
2161
+ <ul className="agentkit-sources-list">
2162
+ {sources.map((source, index) => (
2163
+ <li key={`${index}:${source.url ?? source.title}`}>
2164
+ <AgentLink href={source.url}>
2165
+ <TraceLine>
2166
+ <TraceIcon>
2167
+ <IconWorld aria-hidden="true" className="agentkit-icon" />
2168
+ </TraceIcon>
2169
+ <TraceTitle>{sourceTitle(source)}</TraceTitle>
2170
+ </TraceLine>
2171
+ </AgentLink>
2172
+ </li>
2173
+ ))}
2174
+ </ul>
2175
+ </TraceCollapsibleContent>
2176
+ </TraceCollapsible>
2177
+ </Trace>
2178
+ );
2179
+ }
2180
+
2181
+ function sourceTitle(source: CitationPart): string {
2182
+ const title = readableText(source.title);
2183
+ if (title) return title;
2184
+ const href = safeAgentHref(source.url);
2185
+ if (!href) return source.title;
2186
+ try {
2187
+ return new URL(href).hostname;
2188
+ } catch {
2189
+ return href;
2190
+ }
2191
+ }
2192
+
2193
+ function traceStatus(status: AgentActivity["status"]): TraceStatus | undefined {
2194
+ return status === "running"
2195
+ ? "running"
2196
+ : status === "failed"
2197
+ ? "error"
2198
+ : undefined;
2199
+ }
2200
+
1857
2201
  function ActivitySourceIcon({
1858
2202
  activity,
1859
2203
  threadId,
@@ -1973,29 +2317,6 @@ function workedSummaryLabel(
1973
2317
  : labels.worked;
1974
2318
  }
1975
2319
 
1976
- export function formatAgentKitDuration(
1977
- ms: number,
1978
- units: { hour?: string; minute?: string; second?: string } = {},
1979
- ): string {
1980
- const hour = units.hour ?? "h";
1981
- const minute = units.minute ?? "m";
1982
- const second = units.second ?? "s";
1983
- const totalSeconds = Math.max(0, Math.round(ms / 1_000));
1984
- if (totalSeconds < 60) return `${Math.max(1, totalSeconds)}${second}`;
1985
- const minutes = Math.floor(totalSeconds / 60);
1986
- const seconds = totalSeconds % 60;
1987
- if (minutes < 60) {
1988
- return seconds === 0
1989
- ? `${minutes}${minute}`
1990
- : `${minutes}${minute} ${seconds}${second}`;
1991
- }
1992
- const hours = Math.floor(minutes / 60);
1993
- const remainingMinutes = minutes % 60;
1994
- return remainingMinutes === 0
1995
- ? `${hours}${hour}`
1996
- : `${hours}${hour} ${remainingMinutes}${minute}`;
1997
- }
1998
-
1999
2320
  function isVisibleReasoningPart(part: AgentMessagePart): boolean {
2000
2321
  return (
2001
2322
  part.type === "reasoning" &&
@@ -2802,6 +3123,7 @@ export const AgentActivityGroup = memo(function AgentActivityGroup({
2802
3123
  open={false}
2803
3124
  onToggle={() => {}}
2804
3125
  expandable={false}
3126
+ status={summaryLabel ? undefined : "running"}
2805
3127
  icon={summaryIcon}
2806
3128
  label={
2807
3129
  summaryLabel ? (
@@ -3009,6 +3331,7 @@ function AgentWorkGroupRow({
3009
3331
  open={open}
3010
3332
  onToggle={() => setOpen((current) => !current)}
3011
3333
  controls={contentId}
3334
+ status={group.failed ? "error" : group.running ? "running" : undefined}
3012
3335
  icon={
3013
3336
  group.failed ? (
3014
3337
  <IconAlertTriangle aria-hidden="true" className="agentkit-icon" />
@@ -3166,6 +3489,7 @@ function AgentWorkStepItem({
3166
3489
  className="agentkit-work-step-title"
3167
3490
  open={open}
3168
3491
  onToggle={() => setOpen((current) => !current)}
3492
+ status={step.active ? "running" : undefined}
3169
3493
  expandable={hasBody}
3170
3494
  as={hasBody ? "button" : "static"}
3171
3495
  controls={hasBody ? bodyId : undefined}
@@ -3424,6 +3748,7 @@ function AgentWorkDisclosure({
3424
3748
  rowClassName="agentkit-activities-row"
3425
3749
  open={open}
3426
3750
  onToggle={() => setOpen((value) => !value)}
3751
+ status={running ? "running" : undefined}
3427
3752
  expandable={expandable}
3428
3753
  controls={contentId}
3429
3754
  style={workedForRowSummaryStyle(workedForRowLayout)}
@@ -3474,6 +3799,7 @@ function AgentReasoningPartDisclosure({
3474
3799
  open={open}
3475
3800
  onToggle={() => setOpen((value) => !value)}
3476
3801
  controls={contentId}
3802
+ status={active ? "running" : undefined}
3477
3803
  icon={activityIcon("reasoning")}
3478
3804
  label={<span className="agentkit-reasoning-label">{label}</span>}
3479
3805
  />
@@ -3527,12 +3853,596 @@ function AgentReasoningSteps({
3527
3853
  [active, message],
3528
3854
  );
3529
3855
  return (
3530
- <AgentWorkStepList
3531
- steps={steps}
3532
- threadId={threadId}
3533
- running={active}
3534
- renderCall={() => null}
3535
- />
3856
+ <AgentWorkStepList
3857
+ steps={steps}
3858
+ threadId={threadId}
3859
+ running={active}
3860
+ renderCall={() => null}
3861
+ />
3862
+ );
3863
+ }
3864
+
3865
+ /** `sequence` is the entry's latest event. */
3866
+ type AgentCollaborationEntry =
3867
+ | { type: "interaction"; value: AgentInteraction; sequence: number }
3868
+ | { type: "activity"; value: AgentActivity; sequence: number };
3869
+
3870
+ type AgentCollaborationItem =
3871
+ | { type: "entry"; key: string; entry: AgentCollaborationEntry }
3872
+ | {
3873
+ type: "subagent";
3874
+ key: string;
3875
+ agentId: string;
3876
+ entries: AgentCollaborationEntry[];
3877
+ };
3878
+
3879
+ function collaborationSubjectId(
3880
+ entry: AgentCollaborationEntry,
3881
+ thread: AgentThreadState,
3882
+ ): string | undefined {
3883
+ const isPrimary = (id: string | undefined) =>
3884
+ id !== undefined && thread.agents[id]?.kind === "primary";
3885
+ if (entry.type === "activity") {
3886
+ const id = entry.value.agentId;
3887
+ return id && !isPrimary(id) ? id : undefined;
3888
+ }
3889
+ const { agentId, targetAgentId } = entry.value;
3890
+ if (isPrimary(agentId)) {
3891
+ return targetAgentId && !isPrimary(targetAgentId)
3892
+ ? targetAgentId
3893
+ : undefined;
3894
+ }
3895
+ return agentId;
3896
+ }
3897
+
3898
+ /** Each delegated agent's entries fold into one item where it first appears. */
3899
+ function agentCollaborationItems(
3900
+ thread: AgentThreadState,
3901
+ runEvents: readonly AgentEvent[],
3902
+ labels: Partial<AgentKitLabels>,
3903
+ range: {
3904
+ afterSequence?: number | undefined;
3905
+ throughSequence?: number | undefined;
3906
+ },
3907
+ ): AgentCollaborationItem[] {
3908
+ const entries = new Map<string, AgentCollaborationEntry>();
3909
+ const firstSequence = new Map<string, number>();
3910
+ const order: string[] = [];
3911
+ const delegatedAgentIds = new Map<string, string>();
3912
+ // A delegated agent's own tool calls leave the parent's work for its step.
3913
+ const delegatedToolAgentIds = new Map<string, string>();
3914
+ for (const event of runEvents) {
3915
+ if (
3916
+ event.type === "activity.started" ||
3917
+ event.type === "activity.updated" ||
3918
+ event.type === "activity.completed"
3919
+ ) {
3920
+ const activity = thread.activities[event.activity.id] ?? event.activity;
3921
+ const agentId = event.activity.agentId ?? activity.agentId;
3922
+ if (agentId) delegatedAgentIds.set(event.activity.id, agentId);
3923
+ }
3924
+ if (event.type === "tool.started" || event.type === "tool.updated") {
3925
+ const agentId =
3926
+ event.toolCall.agentId ?? thread.tools[event.toolCall.id]?.agentId;
3927
+ if (agentId) delegatedToolAgentIds.set(event.toolCall.id, agentId);
3928
+ }
3929
+ }
3930
+ const remember = (
3931
+ key: string,
3932
+ sequence: number,
3933
+ entry:
3934
+ | { type: "interaction"; value: AgentInteraction }
3935
+ | { type: "activity"; value: AgentActivity },
3936
+ ) => {
3937
+ if (!entries.has(key)) {
3938
+ order.push(key);
3939
+ firstSequence.set(key, sequence);
3940
+ }
3941
+ entries.set(key, { ...entry, sequence });
3942
+ };
3943
+ for (const event of runEvents) {
3944
+ if (event.type === "agent.interaction") {
3945
+ remember(`interaction:${event.interaction.id}`, event.sequence, {
3946
+ type: "interaction",
3947
+ value: event.interaction,
3948
+ });
3949
+ }
3950
+ if (
3951
+ event.type === "activity.started" ||
3952
+ event.type === "activity.updated" ||
3953
+ event.type === "activity.completed"
3954
+ ) {
3955
+ const agentId = delegatedAgentIds.get(event.activity.id);
3956
+ if (!agentId) continue;
3957
+ const activity = thread.activities[event.activity.id] ?? event.activity;
3958
+ remember(`activity:${event.activity.id}`, event.sequence, {
3959
+ type: "activity",
3960
+ value: { ...activity, agentId },
3961
+ });
3962
+ }
3963
+ const toolId =
3964
+ event.type === "tool.started" || event.type === "tool.updated"
3965
+ ? event.toolCall.id
3966
+ : event.type === "tool.delta"
3967
+ ? event.toolCallId
3968
+ : undefined;
3969
+ const toolAgentId = toolId ? delegatedToolAgentIds.get(toolId) : undefined;
3970
+ if (toolId && toolAgentId) {
3971
+ const tool =
3972
+ toolId in thread.tools
3973
+ ? thread.tools[toolId]
3974
+ : event.type === "tool.started" || event.type === "tool.updated"
3975
+ ? event.toolCall
3976
+ : undefined;
3977
+ if (tool) {
3978
+ remember(`tool:${toolId}`, event.sequence, {
3979
+ type: "activity",
3980
+ value: { ...toolToActivity(tool, labels), agentId: toolAgentId },
3981
+ });
3982
+ }
3983
+ }
3984
+ }
3985
+ const items: AgentCollaborationItem[] = [];
3986
+ const subagents = new Map<string, AgentCollaborationEntry[]>();
3987
+ for (const key of order) {
3988
+ const sequence = firstSequence.get(key);
3989
+ const entry = entries.get(key);
3990
+ if (!entry || sequence === undefined || !sequenceInRange(sequence, range)) {
3991
+ continue;
3992
+ }
3993
+ const agentId = collaborationSubjectId(entry, thread);
3994
+ if (!agentId) {
3995
+ items.push({ type: "entry", key, entry });
3996
+ continue;
3997
+ }
3998
+ const grouped = subagents.get(agentId);
3999
+ if (grouped) {
4000
+ grouped.push(entry);
4001
+ continue;
4002
+ }
4003
+ const created = [entry];
4004
+ subagents.set(agentId, created);
4005
+ items.push({
4006
+ type: "subagent",
4007
+ key: `subagent:${agentId}`,
4008
+ agentId,
4009
+ entries: created,
4010
+ });
4011
+ }
4012
+ return items;
4013
+ }
4014
+
4015
+ /** The one task a Subagent step shows: the agent's active task, else its first. */
4016
+ function subagentStepTask(
4017
+ thread: AgentThreadState,
4018
+ agentId: string,
4019
+ ): AgentTask | undefined {
4020
+ const agent = agentId in thread.agents ? thread.agents[agentId] : undefined;
4021
+ return Object.values(thread.tasks).find(
4022
+ (candidate) =>
4023
+ candidate.assignedAgentId === agentId &&
4024
+ (agent?.activeTaskId === undefined ||
4025
+ candidate.id === agent.activeTaskId),
4026
+ );
4027
+ }
4028
+
4029
+ /** Tasks the Subagent steps in this range of a run show. */
4030
+ function subagentStepTaskIds(
4031
+ thread: AgentThreadState,
4032
+ runId: RunId | undefined,
4033
+ labels: Partial<AgentKitLabels>,
4034
+ range: {
4035
+ afterSequence?: number | undefined;
4036
+ throughSequence?: number | undefined;
4037
+ },
4038
+ ): Set<string> {
4039
+ const ids = new Set<string>();
4040
+ for (const item of agentCollaborationItems(
4041
+ thread,
4042
+ agentRunEvents(thread, runId),
4043
+ labels,
4044
+ range,
4045
+ )) {
4046
+ if (item.type !== "subagent") continue;
4047
+ const task = subagentStepTask(thread, item.agentId);
4048
+ if (task) ids.add(task.id);
4049
+ }
4050
+ return ids;
4051
+ }
4052
+
4053
+ /** Set inside a Subagent step, so its rows do not repeat the agent's chip. */
4054
+ const AgentSubagentStepContext = createContext<string | undefined>(undefined);
4055
+
4056
+ const SUBAGENT_LIFECYCLE_KINDS = new Set([
4057
+ "started",
4058
+ "resumed",
4059
+ "delegated",
4060
+ "paused",
4061
+ "completed",
4062
+ "failed",
4063
+ "closed",
4064
+ ]);
4065
+
4066
+ type SubagentState = "running" | "paused" | "completed" | "failed" | "closed";
4067
+
4068
+ function subagentState(
4069
+ agent: AgentParticipant | undefined,
4070
+ entries: readonly AgentCollaborationEntry[],
4071
+ ): SubagentState {
4072
+ switch (agent?.status) {
4073
+ case "waiting":
4074
+ case "paused":
4075
+ return "paused";
4076
+ case "completed":
4077
+ return "completed";
4078
+ case "failed":
4079
+ return "failed";
4080
+ case "closed":
4081
+ return "closed";
4082
+ case "idle":
4083
+ case "working":
4084
+ case undefined:
4085
+ break;
4086
+ }
4087
+ // Status can stay "working" past its last event; a finished call is not a finished agent.
4088
+ const newestFirst = [...entries].sort((a, b) => b.sequence - a.sequence);
4089
+ for (const entry of newestFirst) {
4090
+ if (entry.type === "activity") {
4091
+ if (entry.value.status === "running") return "running";
4092
+ continue;
4093
+ }
4094
+ switch (entry.value.kind) {
4095
+ case "completed":
4096
+ return "completed";
4097
+ case "failed":
4098
+ return "failed";
4099
+ case "paused":
4100
+ return "paused";
4101
+ case "closed":
4102
+ return "closed";
4103
+ case "started":
4104
+ case "resumed":
4105
+ case "delegated":
4106
+ return "running";
4107
+ case "messaged":
4108
+ default:
4109
+ break;
4110
+ }
4111
+ }
4112
+ if (agent?.status === "working") return "running";
4113
+ const latest = newestFirst.at(0);
4114
+ return latest?.type === "activity" && latest.value.status === "failed"
4115
+ ? "failed"
4116
+ : latest?.type === "activity" && latest.value.status === "completed"
4117
+ ? "completed"
4118
+ : "running";
4119
+ }
4120
+
4121
+ function metadataDurationMs(
4122
+ metadata: Record<string, unknown> | undefined,
4123
+ ): number | undefined {
4124
+ const value = metadata?.durationMs;
4125
+ return typeof value === "number" && Number.isFinite(value) && value >= 0
4126
+ ? value
4127
+ : undefined;
4128
+ }
4129
+
4130
+ function subagentDurationMs(
4131
+ agent: AgentParticipant | undefined,
4132
+ entries: readonly AgentCollaborationEntry[],
4133
+ ): number | undefined {
4134
+ const fromAgent = metadataDurationMs(agent?.metadata);
4135
+ if (fromAgent !== undefined) return fromAgent;
4136
+ for (const entry of [...entries].sort((a, b) => b.sequence - a.sequence)) {
4137
+ const fromEntry = metadataDurationMs(entry.value.metadata);
4138
+ if (fromEntry !== undefined) return fromEntry;
4139
+ }
4140
+ const started = agent?.startedAt ? Date.parse(agent.startedAt) : Number.NaN;
4141
+ const completed = agent?.completedAt
4142
+ ? Date.parse(agent.completedAt)
4143
+ : Number.NaN;
4144
+ return Number.isFinite(started) &&
4145
+ Number.isFinite(completed) &&
4146
+ completed >= started
4147
+ ? completed - started
4148
+ : undefined;
4149
+ }
4150
+
4151
+ /** One row per delegated agent, with its own calls and its result inside. */
4152
+ export function AgentSubagentStep({
4153
+ agentId,
4154
+ entries,
4155
+ runId,
4156
+ threadId,
4157
+ }: {
4158
+ agentId: string;
4159
+ entries: readonly AgentCollaborationEntry[];
4160
+ runId: RunId | undefined;
4161
+ threadId: string;
4162
+ }) {
4163
+ const {
4164
+ slots,
4165
+ registry,
4166
+ labels,
4167
+ formatWorkedForDuration,
4168
+ formatAgentInteractionDetail,
4169
+ } = useAgentKit();
4170
+ const thread = useAgentThreadSlice(
4171
+ (previous, next) =>
4172
+ sameAgentsAndTasks(previous, next) &&
4173
+ previous.tools === next.tools &&
4174
+ previous.runs === next.runs,
4175
+ );
4176
+ const [open, setOpen] = useState(false);
4177
+ const agent = agentId in thread.agents ? thread.agents[agentId] : undefined;
4178
+ const AgentRenderer = agent
4179
+ ? (registry.agents?.[agent.kind ?? ""] ??
4180
+ slots.agent ??
4181
+ AgentParticipantView)
4182
+ : undefined;
4183
+ const name = resolveAgentName(
4184
+ thread,
4185
+ agentId,
4186
+ entries.map((entry) => entry.value.source),
4187
+ labels.assistant,
4188
+ );
4189
+ const runStatus =
4190
+ runId === undefined ? undefined : thread.runs[runId]?.status;
4191
+ const liveState = subagentState(agent, entries);
4192
+ // Agents can outlive their run, so one still "working" after it settled has stopped.
4193
+ const settledStatus =
4194
+ liveState !== "running"
4195
+ ? undefined
4196
+ : runStatus === "completed"
4197
+ ? "completed"
4198
+ : runStatus === "failed"
4199
+ ? "failed"
4200
+ : runStatus === "cancelled"
4201
+ ? "closed"
4202
+ : undefined;
4203
+ const state: SubagentState = settledStatus ?? liveState;
4204
+ const durationMs =
4205
+ state === "running" ? undefined : subagentDurationMs(agent, entries);
4206
+ const duration =
4207
+ durationMs === undefined
4208
+ ? undefined
4209
+ : (formatWorkedForDuration?.(durationMs) ??
4210
+ formatAgentKitDuration(durationMs, {
4211
+ hour: labels.durationHourShort,
4212
+ minute: labels.durationMinuteShort,
4213
+ second: labels.durationSecondShort,
4214
+ }));
4215
+ const task = subagentStepTask(thread, agentId);
4216
+ const taskTitle = task ? humanizeAgentLabel(task.title) : "";
4217
+ const shownTaskTitle =
4218
+ taskTitle && taskTitle.toLocaleLowerCase() !== name.toLocaleLowerCase()
4219
+ ? taskTitle
4220
+ : "";
4221
+ const newestFirst = [...entries].sort((a, b) => b.sequence - a.sequence);
4222
+ const latest = <T,>(
4223
+ pick: (entry: AgentCollaborationEntry) => T | undefined,
4224
+ ) => {
4225
+ for (const entry of newestFirst) {
4226
+ const value = pick(entry);
4227
+ if (value !== undefined) return value;
4228
+ }
4229
+ return undefined;
4230
+ };
4231
+ // A call's detail is its output, which is no headline.
4232
+ const progress =
4233
+ state === "running"
4234
+ ? latest((entry) =>
4235
+ entry.type === "activity" &&
4236
+ entry.value.status === "running" &&
4237
+ !(entry.value.id in thread.tools) &&
4238
+ readableText(entry.value.detail)
4239
+ ? entry.value
4240
+ : undefined,
4241
+ )
4242
+ : undefined;
4243
+ const progressDetail = progress ? readableText(progress.detail) : "";
4244
+ const failureDetail =
4245
+ state === "failed"
4246
+ ? latest((entry) =>
4247
+ entry.type === "interaction" && entry.value.kind === "failed"
4248
+ ? readableText(
4249
+ formatAgentInteractionDetail?.(entry.value) ??
4250
+ entry.value.detail,
4251
+ ) || undefined
4252
+ : entry.type === "activity" && entry.value.status === "failed"
4253
+ ? readableText(entry.value.detail) || undefined
4254
+ : undefined,
4255
+ )
4256
+ : undefined;
4257
+ const status =
4258
+ state === "running"
4259
+ ? labels.agentWorking
4260
+ : state === "failed"
4261
+ ? labels.agentFailed
4262
+ : state === "paused"
4263
+ ? labels.agentPaused
4264
+ : state === "closed"
4265
+ ? labels.agentClosed
4266
+ : duration
4267
+ ? labels.agentWorkedFor.replace("{{duration}}", () => duration)
4268
+ : labels.agentCompleted;
4269
+ const content = entries.flatMap((entry): ReactNode[] => {
4270
+ const key = `${entry.type}:${entry.value.id}`;
4271
+ if (entry.type === "interaction") {
4272
+ const interaction = entry.value;
4273
+ const Custom =
4274
+ registry.agentInteractions?.[interaction.kind] ??
4275
+ slots.agentInteraction;
4276
+ if (Custom) {
4277
+ return [<Custom key={key} value={interaction} threadId={threadId} />];
4278
+ }
4279
+ // The row's own status, duration and error line already say these.
4280
+ if (
4281
+ SUBAGENT_LIFECYCLE_KINDS.has(interaction.kind) &&
4282
+ (interaction.kind === "failed" ||
4283
+ !(
4284
+ interaction.label ||
4285
+ interaction.detail ||
4286
+ interaction.object ||
4287
+ interaction.targetAgentId
4288
+ ))
4289
+ ) {
4290
+ return [];
4291
+ }
4292
+ return [
4293
+ <AgentInteractionItem
4294
+ key={key}
4295
+ value={interaction}
4296
+ threadId={threadId}
4297
+ />,
4298
+ ];
4299
+ }
4300
+ const activity = entry.value;
4301
+ const Custom = registry.activities?.[activity.kind] ?? slots.activity;
4302
+ if (Custom) {
4303
+ return [<Custom key={key} value={activity} threadId={threadId} />];
4304
+ }
4305
+ if (activity.id in thread.tools) {
4306
+ return [
4307
+ <AgentActivityItem key={key} value={activity} threadId={threadId} />,
4308
+ ];
4309
+ }
4310
+ // The agent's report is the step's result, so it shows in place.
4311
+ const summary = (activity.summary ?? []).flatMap((part, partIndex) => {
4312
+ const safePart =
4313
+ part.type === "text"
4314
+ ? {
4315
+ ...part,
4316
+ text: part.text.trim()
4317
+ ? formatActivityText(part.text, labels)
4318
+ : "",
4319
+ }
4320
+ : part;
4321
+ return safePart.type === "text" && !safePart.text
4322
+ ? []
4323
+ : [
4324
+ <AgentMessagePartView
4325
+ key={`${key}:summary:${partIndex}`}
4326
+ value={safePart}
4327
+ threadId={threadId}
4328
+ />,
4329
+ ];
4330
+ });
4331
+ if (summary.length) return summary;
4332
+ // A summary-less "agent" activity is the status line the header carries.
4333
+ if (activity === progress || activity.kind === "agent") return [];
4334
+ return [
4335
+ <AgentActivityItem key={key} value={activity} threadId={threadId} />,
4336
+ ];
4337
+ });
4338
+ const failed = state === "failed";
4339
+ const line = (
4340
+ <TraceLine
4341
+ status={state === "running" ? "running" : failed ? "error" : undefined}
4342
+ >
4343
+ <TraceIcon>
4344
+ {/* A failed agent's chip already carries the triangle. */}
4345
+ <IconArrowFork aria-hidden="true" className="agentkit-icon" />
4346
+ </TraceIcon>
4347
+ <span className="agentkit-disclosure-label">
4348
+ {agent && AgentRenderer ? (
4349
+ <AgentRenderer
4350
+ value={settledStatus ? { ...agent, status: settledStatus } : agent}
4351
+ threadId={threadId}
4352
+ />
4353
+ ) : (
4354
+ <AgentIdentityChip
4355
+ id={agentId}
4356
+ name={name}
4357
+ fallbackName={labels.assistant}
4358
+ {...(failed
4359
+ ? { status: "failed" as const }
4360
+ : state === "running"
4361
+ ? { status: "working" as const }
4362
+ : {})}
4363
+ />
4364
+ )}
4365
+ <TraceTitle className="agentkit-activity-label">{status}</TraceTitle>
4366
+ </span>
4367
+ </TraceLine>
4368
+ );
4369
+ const detail = shownTaskTitle || progressDetail;
4370
+ const ObjectRenderer = slots.object ?? AgentObjectReferenceView;
4371
+ const taskObject = task
4372
+ ? (task.object ??
4373
+ sourceObjectUnlessRepresented(task.source, thread, [agentId]))
4374
+ : undefined;
4375
+ const meta =
4376
+ detail || taskObject ? (
4377
+ <>
4378
+ {taskObject ? (
4379
+ <ObjectRenderer value={taskObject} threadId={threadId} />
4380
+ ) : null}
4381
+ {detail ? (
4382
+ <TraceDescription
4383
+ {...SESSION_REPLAY_MASK_PROPS}
4384
+ className="agentkit-activity-detail"
4385
+ >
4386
+ {detail}
4387
+ </TraceDescription>
4388
+ ) : null}
4389
+ </>
4390
+ ) : null;
4391
+ const errorLine = failureDetail ? (
4392
+ <span {...SESSION_REPLAY_MASK_PROPS} className="agentkit-activity-error">
4393
+ {failureDetail}
4394
+ </span>
4395
+ ) : null;
4396
+ const rootProps = {
4397
+ className: "agentkit-subagent-step",
4398
+ "data-agent-id": agentId,
4399
+ // Not data-state: the collapsible keeps its open state there.
4400
+ "data-status": state,
4401
+ };
4402
+ if (!content.length) {
4403
+ return (
4404
+ <div {...rootProps}>
4405
+ <div className="agentkit-disclosure-row agentkit-activity-row">
4406
+ <span className="agentkit-disclosure-summary" data-expandable="false">
4407
+ {line}
4408
+ </span>
4409
+ {meta}
4410
+ </div>
4411
+ {errorLine}
4412
+ </div>
4413
+ );
4414
+ }
4415
+ return (
4416
+ <TraceCollapsible {...rootProps} open={open} onOpenChange={setOpen}>
4417
+ <div
4418
+ className="agentkit-disclosure-row agentkit-activity-row"
4419
+ data-expandable="true"
4420
+ onClick={(event) => {
4421
+ const target = event.target;
4422
+ if (
4423
+ target instanceof Element &&
4424
+ target.closest(DISCLOSURE_ROW_INTERACTIVE)
4425
+ )
4426
+ return;
4427
+ if (globalThis.getSelection()?.toString()) return;
4428
+ setOpen((current) => !current);
4429
+ }}
4430
+ >
4431
+ <TraceCollapsibleTrigger>{line}</TraceCollapsibleTrigger>
4432
+ {meta}
4433
+ </div>
4434
+ {errorLine}
4435
+ <TraceCollapsibleContent
4436
+ key={open ? "open" : "closed"}
4437
+ keepMounted
4438
+ {...(failed || !open ? SESSION_REPLAY_BLOCK_PROPS : {})}
4439
+ className="agentkit-subagent-content"
4440
+ >
4441
+ <AgentSubagentStepContext.Provider value={agentId}>
4442
+ {content}
4443
+ </AgentSubagentStepContext.Provider>
4444
+ </TraceCollapsibleContent>
4445
+ </TraceCollapsible>
3536
4446
  );
3537
4447
  }
3538
4448
 
@@ -3545,77 +4455,32 @@ export function AgentCollaborationFeed({
3545
4455
  const thread = useAgentThreadSlice((previous, next) =>
3546
4456
  sameRunWork(previous, next, runId),
3547
4457
  );
3548
- const runEvents = agentRunEvents(thread, runId);
3549
- const entries = new Map<
3550
- string,
3551
- | { type: "interaction"; value: AgentInteraction }
3552
- | { type: "activity"; value: AgentActivity }
3553
- >();
3554
- const delegatedAgentIds = new Map<string, string>();
3555
- for (const event of runEvents) {
3556
- if (
3557
- event.type === "activity.started" ||
3558
- event.type === "activity.updated" ||
3559
- event.type === "activity.completed"
3560
- ) {
3561
- const activity = thread.activities[event.activity.id] ?? event.activity;
3562
- const agentId = event.activity.agentId ?? activity.agentId;
3563
- if (agentId) delegatedAgentIds.set(event.activity.id, agentId);
3564
- }
3565
- }
3566
- const firstSequence = new Map<string, number>();
3567
- const order: string[] = [];
3568
- const remember = (
3569
- key: string,
3570
- sequence: number,
3571
- entry:
3572
- | { type: "interaction"; value: AgentInteraction }
3573
- | { type: "activity"; value: AgentActivity },
3574
- ) => {
3575
- if (!entries.has(key)) {
3576
- order.push(key);
3577
- firstSequence.set(key, sequence);
3578
- }
3579
- entries.set(key, entry);
3580
- };
3581
- for (const event of runEvents) {
3582
- if (event.type === "agent.interaction") {
3583
- remember(`interaction:${event.interaction.id}`, event.sequence, {
3584
- type: "interaction",
3585
- value: event.interaction,
3586
- });
3587
- }
3588
- if (
3589
- event.type === "activity.started" ||
3590
- event.type === "activity.updated" ||
3591
- event.type === "activity.completed"
3592
- ) {
3593
- const agentId = delegatedAgentIds.get(event.activity.id);
3594
- if (!agentId) continue;
3595
- const activity = thread.activities[event.activity.id] ?? event.activity;
3596
- remember(`activity:${event.activity.id}`, event.sequence, {
3597
- type: "activity",
3598
- value: { ...activity, agentId },
3599
- });
3600
- }
3601
- }
3602
- const selectedOrder = order.filter((key) => {
3603
- const sequence = firstSequence.get(key);
3604
- return (
3605
- sequence !== undefined &&
3606
- sequenceInRange(sequence, { afterSequence, throughSequence })
3607
- );
3608
- });
3609
- if (selectedOrder.length === 0) return null;
4458
+ const items = agentCollaborationItems(
4459
+ thread,
4460
+ agentRunEvents(thread, runId),
4461
+ labels,
4462
+ { afterSequence, throughSequence },
4463
+ );
4464
+ if (items.length === 0) return null;
3610
4465
  return (
3611
4466
  <section
3612
4467
  className="agentkit-agent-feed"
3613
4468
  aria-label={labels.agents}
3614
4469
  data-agent-collaboration-feed="true"
3615
4470
  >
3616
- {selectedOrder.map((key) => {
3617
- const entry = entries.get(key);
3618
- if (!entry) return null;
4471
+ {items.map((item) => {
4472
+ if (item.type === "subagent") {
4473
+ return (
4474
+ <AgentSubagentStep
4475
+ key={item.key}
4476
+ agentId={item.agentId}
4477
+ entries={item.entries}
4478
+ runId={runId}
4479
+ threadId={threadId}
4480
+ />
4481
+ );
4482
+ }
4483
+ const { entry, key } = item;
3619
4484
  if (entry.type === "interaction") {
3620
4485
  const Renderer =
3621
4486
  registry.agentInteractions?.[entry.value.kind] ??
@@ -3694,8 +4559,11 @@ export function AgentTaskItem({
3694
4559
  slots.agent ??
3695
4560
  AgentParticipantView)
3696
4561
  : undefined;
3697
- const progress = task.progress
3698
- ? `${task.progress.completed}/${task.progress.total}`
4562
+ const taskProgress = task.progress;
4563
+ const progress = taskProgress
4564
+ ? labels.tasksProgress
4565
+ .replace("{{completed}}", () => String(taskProgress.completed))
4566
+ .replace("{{total}}", () => String(taskProgress.total))
3699
4567
  : undefined;
3700
4568
  const title =
3701
4569
  humanizeAgentLabel(task.title) ||
@@ -3709,8 +4577,18 @@ export function AgentTaskItem({
3709
4577
  title.toLocaleLowerCase() !== agentName.toLocaleLowerCase();
3710
4578
  const status = taskStatusPresentation(task.status, labels);
3711
4579
  return (
3712
- <div className="agentkit-task-row" data-status={task.status}>
3713
- {status.icon}
4580
+ <TraceLine
4581
+ className="agentkit-task-row"
4582
+ data-status={task.status}
4583
+ status={
4584
+ task.status === "running"
4585
+ ? "running"
4586
+ : task.status === "failed"
4587
+ ? "error"
4588
+ : undefined
4589
+ }
4590
+ >
4591
+ <TraceIcon>{status.icon}</TraceIcon>
3714
4592
  <span className="agentkit-visually-hidden">{status.label}</span>
3715
4593
  {showAgent && agent && AgentRenderer ? (
3716
4594
  <AgentRenderer value={agent} threadId={threadId} />
@@ -3721,7 +4599,7 @@ export function AgentTaskItem({
3721
4599
  fallbackName={labels.assistant}
3722
4600
  />
3723
4601
  ) : null}
3724
- <span className="agentkit-task-title">{title}</span>
4602
+ <TraceTitle className="agentkit-task-title">{title}</TraceTitle>
3725
4603
  {object ? <ObjectRenderer value={object} threadId={threadId} /> : null}
3726
4604
  {detail ? (
3727
4605
  <span className="agentkit-task-detail" title={detail}>
@@ -3729,9 +4607,11 @@ export function AgentTaskItem({
3729
4607
  </span>
3730
4608
  ) : null}
3731
4609
  {progress ? (
3732
- <span className="agentkit-task-progress">{progress}</span>
4610
+ <TraceDescription className="agentkit-task-progress">
4611
+ {progress}
4612
+ </TraceDescription>
3733
4613
  ) : null}
3734
- </div>
4614
+ </TraceLine>
3735
4615
  );
3736
4616
  }
3737
4617
 
@@ -3763,14 +4643,23 @@ export function AgentTaskGroup({
3763
4643
  }
3764
4644
  tasks.set(event.task.id, thread.tasks[event.task.id] ?? event.task);
3765
4645
  }
4646
+ // Only the task a Subagent step shows moves out of this list.
4647
+ const stepTaskIds = subagentStepTaskIds(thread, runId, labels, {
4648
+ afterSequence,
4649
+ throughSequence,
4650
+ });
3766
4651
  const selectedIds = ids.filter((id) => {
3767
4652
  const sequence = firstSequence.get(id);
3768
4653
  return (
3769
4654
  sequence !== undefined &&
3770
- sequenceInRange(sequence, { afterSequence, throughSequence })
4655
+ sequenceInRange(sequence, { afterSequence, throughSequence }) &&
4656
+ !stepTaskIds.has(id)
3771
4657
  );
3772
4658
  });
3773
4659
  if (selectedIds.length === 0) return null;
4660
+ const completedCount = selectedIds.filter(
4661
+ (id) => tasks.get(id)?.status === "completed",
4662
+ ).length;
3774
4663
  return (
3775
4664
  <div className="agentkit-tasks" data-open={open ? "true" : "false"}>
3776
4665
  <AgentKitDisclosureSummary
@@ -3782,7 +4671,11 @@ export function AgentTaskGroup({
3782
4671
  label={
3783
4672
  <>
3784
4673
  <span>{labels.tasks}</span>
3785
- <span className="agentkit-tasks-count">{selectedIds.length}</span>
4674
+ <span className="agentkit-tasks-count">
4675
+ {labels.tasksProgress
4676
+ .replace("{{completed}}", () => String(completedCount))
4677
+ .replace("{{total}}", () => String(selectedIds.length))}
4678
+ </span>
3786
4679
  </>
3787
4680
  }
3788
4681
  />
@@ -4194,8 +5087,8 @@ export function AgentConnectionRequestCard({
4194
5087
  return (
4195
5088
  <Surface
4196
5089
  as="section"
4197
- className="agentkit-connection-request shadow-(--agent-kit-control-elevation)"
4198
- elevation="low"
5090
+ className="agentkit-connection-request"
5091
+ elevation="none"
4199
5092
  padding="none"
4200
5093
  data-status={request.status}
4201
5094
  aria-labelledby={`${request.id}-title`}
@@ -4249,7 +5142,7 @@ export function AgentConnectionRequestCard({
4249
5142
  ) : null}
4250
5143
  {resolution.error ? (
4251
5144
  <div className="agentkit-command-error" role="alert">
4252
- <IconAlertCircle aria-hidden="true" className="agentkit-icon" />
5145
+ <IconAlertTriangle aria-hidden="true" className="agentkit-icon" />
4253
5146
  <span {...SESSION_REPLAY_MASK_PROPS}>{resolution.error.message}</span>
4254
5147
  </div>
4255
5148
  ) : null}
@@ -5214,39 +6107,100 @@ function AgentMessageTimestamp({
5214
6107
  );
5215
6108
  }
5216
6109
 
6110
+ /** Enter and Space fire a click with no pointer detail. */
6111
+ function pressedByKeyboard(event: { detail: number } | undefined): boolean {
6112
+ return event?.detail === 0;
6113
+ }
6114
+
6115
+ /** Keyboard presses only: focusing the composer on touch opens the on-screen keyboard. */
6116
+ function useRescueFocusToComposer(threadId: string): () => void {
6117
+ const { requestComposerFocus } = useAgentKit();
6118
+ return useCallback(() => {
6119
+ requestAnimationFrame(() => {
6120
+ const active = document.activeElement;
6121
+ if (!active || active === document.body) requestComposerFocus(threadId);
6122
+ });
6123
+ }, [requestComposerFocus, threadId]);
6124
+ }
6125
+
5217
6126
  /**
5218
6127
  * Names an icon-only message action on hover and keyboard focus. Below the
5219
6128
  * button, so it never covers the message it acts on.
5220
6129
  */
5221
6130
  function MessageActionButton({
5222
6131
  title,
6132
+ busy = false,
6133
+ unavailable = false,
6134
+ icon,
6135
+ onPress,
6136
+ className,
5223
6137
  ...props
5224
- }: ComponentPropsWithoutRef<typeof IconButton>) {
5225
- // A disabled button gets no pointer events, so only a native title can
5226
- // explain it; an enabled one shows the reason in its tooltip instead, so a
5227
- // degraded action never opens both.
5228
- const inactive = Boolean(props.disabled || props.pending);
6138
+ }: ComponentPropsWithoutRef<typeof IconButton> & {
6139
+ /** Stays focusable: disabling the pressed button drops focus to the page. */
6140
+ busy?: boolean;
6141
+ /** Announced with aria-disabled and kept focusable. */
6142
+ unavailable?: boolean;
6143
+ }) {
6144
+ // Only a native title explains a disabled button, never also the tooltip. An
6145
+ // unavailable one stays focusable and a press opens its tooltip: taps never hover.
6146
+ const inactive = Boolean(props.disabled || props.pending || busy);
5229
6147
  const reason = inactive ? undefined : title;
6148
+ const reasonId = useId();
6149
+ const [tooltipOpen, setTooltipOpen] = useState(false);
5230
6150
  return (
5231
- <Tooltip
5232
- content={
5233
- reason ? (
5234
- <>
5235
- {props.label}
5236
- <span className="block text-muted-foreground">{reason}</span>
5237
- </>
5238
- ) : (
5239
- props.label
5240
- )
5241
- }
5242
- placement="bottom"
5243
- trigger={
5244
- <IconButton
5245
- {...props}
5246
- {...(inactive && title !== undefined ? { title } : {})}
5247
- />
5248
- }
5249
- />
6151
+ <>
6152
+ <Tooltip
6153
+ open={tooltipOpen}
6154
+ onOpenChange={setTooltipOpen}
6155
+ content={
6156
+ reason ? (
6157
+ <>
6158
+ {props.label}
6159
+ <span className="block text-muted-foreground">{reason}</span>
6160
+ </>
6161
+ ) : (
6162
+ props.label
6163
+ )
6164
+ }
6165
+ placement="bottom"
6166
+ trigger={
6167
+ <IconButton
6168
+ {...props}
6169
+ icon={
6170
+ busy ? (
6171
+ <IconLoader2 aria-hidden="true" className="animate-spin" />
6172
+ ) : (
6173
+ icon
6174
+ )
6175
+ }
6176
+ {...(unavailable
6177
+ ? {
6178
+ className: `${className ?? ""} aria-disabled:cursor-default aria-disabled:opacity-50`,
6179
+ }
6180
+ : className
6181
+ ? { className }
6182
+ : {})}
6183
+ {...(busy ? { "aria-busy": true } : {})}
6184
+ {...(unavailable ? { "aria-disabled": true } : {})}
6185
+ {...(reason ? { "aria-describedby": reasonId } : {})}
6186
+ onPress={(event) => {
6187
+ if (unavailable) {
6188
+ if (reason) setTooltipOpen(true);
6189
+ return;
6190
+ }
6191
+ if (busy) return;
6192
+ onPress?.(event);
6193
+ }}
6194
+ {...(inactive && title !== undefined ? { title } : {})}
6195
+ />
6196
+ }
6197
+ />
6198
+ {reason ? (
6199
+ <span id={reasonId} hidden>
6200
+ {reason}
6201
+ </span>
6202
+ ) : null}
6203
+ </>
5250
6204
  );
5251
6205
  }
5252
6206
 
@@ -5262,6 +6216,7 @@ export const AgentMessageActions = memo(function AgentMessageActions({
5262
6216
  onCopyMessage,
5263
6217
  branchNavigation,
5264
6218
  } = useAgentKit();
6219
+ const rescueFocusToComposer = useRescueFocusToComposer(threadId);
5265
6220
  const feedbackCapability = useAgentCapability("feedback");
5266
6221
  const forkingCapability = useAgentCapability("threadForking");
5267
6222
  const control = useAgentKitControl(threadId);
@@ -5315,6 +6270,10 @@ export const AgentMessageActions = memo(function AgentMessageActions({
5315
6270
  null,
5316
6271
  );
5317
6272
  const toolbarRef = useRef<HTMLDivElement>(null);
6273
+ const actionsRef = useRef<HTMLDivElement>(null);
6274
+ // One tab stop per message: the toolbar for a reply, the row for a sent message.
6275
+ useRovingFocus(toolbarRef, isAssistant);
6276
+ useRovingFocus(actionsRef, !isAssistant);
5318
6277
  const moreToggleRef = useRef<HTMLButtonElement>(null);
5319
6278
  const secondaryActionsRef = useRef<HTMLDivElement>(null);
5320
6279
  const pendingFocusRef = useRef<"secondary" | "toggle" | null>(null);
@@ -5475,7 +6434,7 @@ export const AgentMessageActions = memo(function AgentMessageActions({
5475
6434
  )
5476
6435
  }
5477
6436
  size="compact"
5478
- className="size-6"
6437
+ className="size-6 rounded-[var(--agent-kit-radius-xs,0.25rem)]"
5479
6438
  // Copies stay enabled while they run: disabling the pressed button drops
5480
6439
  // focus to the page, where Escape can no longer close the toolbar.
5481
6440
  aria-busy={copyAction.pending || undefined}
@@ -5498,11 +6457,9 @@ export const AgentMessageActions = memo(function AgentMessageActions({
5498
6457
  label={labels.previousBranch}
5499
6458
  icon={<IconChevronLeft aria-hidden="true" />}
5500
6459
  size="compact"
5501
- className="size-6"
5502
- disabled={
5503
- branchNavigation.index <= 1 || branchNavigationAction.pending
5504
- }
5505
- pending={branchNavigationAction.pending}
6460
+ className="size-6 rounded-[var(--agent-kit-radius-xs,0.25rem)]"
6461
+ unavailable={branchNavigation.index <= 1}
6462
+ busy={branchNavigationAction.pending}
5506
6463
  onPress={() =>
5507
6464
  void branchNavigationAction
5508
6465
  .execute("previous")
@@ -5523,12 +6480,9 @@ export const AgentMessageActions = memo(function AgentMessageActions({
5523
6480
  label={labels.nextBranch}
5524
6481
  icon={<IconChevronRight aria-hidden="true" />}
5525
6482
  size="compact"
5526
- className="size-6"
5527
- disabled={
5528
- branchNavigation.index >= branchNavigation.count ||
5529
- branchNavigationAction.pending
5530
- }
5531
- pending={branchNavigationAction.pending}
6483
+ className="size-6 rounded-[var(--agent-kit-radius-xs,0.25rem)]"
6484
+ unavailable={branchNavigation.index >= branchNavigation.count}
6485
+ busy={branchNavigationAction.pending}
5532
6486
  onPress={() =>
5533
6487
  void branchNavigationAction.execute("next").catch(() => undefined)
5534
6488
  }
@@ -5539,7 +6493,7 @@ export const AgentMessageActions = memo(function AgentMessageActions({
5539
6493
  <div
5540
6494
  ref={toolbarRef}
5541
6495
  className="agentkit-message-toolbar"
5542
- role="group"
6496
+ role="toolbar"
5543
6497
  aria-label={labels.messageActions}
5544
6498
  data-placement={placement}
5545
6499
  data-latest={isLastAssistantMessage ? "true" : undefined}
@@ -5617,15 +6571,14 @@ export const AgentMessageActions = memo(function AgentMessageActions({
5617
6571
  )
5618
6572
  }
5619
6573
  size="compact"
5620
- className="size-6"
6574
+ className="size-6 rounded-[var(--agent-kit-radius-xs,0.25rem)]"
5621
6575
  data-message-action={`feedback-${value}`}
5622
6576
  aria-pressed={chosen}
5623
6577
  aria-haspopup={toggle ? "dialog" : undefined}
5624
6578
  aria-expanded={toggle ? open : undefined}
5625
6579
  aria-controls={toggle && open ? contentId : undefined}
5626
- disabled={
5627
- !feedbackCapability.enabled || feedbackAction.pending
5628
- }
6580
+ unavailable={!feedbackCapability.enabled}
6581
+ busy={feedbackAction.pending}
5629
6582
  {...(feedbackCapability.reason === undefined
5630
6583
  ? {}
5631
6584
  : { title: feedbackCapability.reason })}
@@ -5664,20 +6617,23 @@ export const AgentMessageActions = memo(function AgentMessageActions({
5664
6617
  label={labels.regenerateResponse}
5665
6618
  icon={<IconRefresh aria-hidden="true" />}
5666
6619
  size="compact"
5667
- className="size-6"
6620
+ className="size-6 rounded-[var(--agent-kit-radius-xs,0.25rem)]"
5668
6621
  data-message-action="retry"
5669
- pending={regenerateAction.pending}
5670
- disabled={
5671
- regenerateAction.pending ||
5672
- position.active ||
5673
- !forkingCapability.enabled
5674
- }
6622
+ busy={regenerateAction.pending}
6623
+ unavailable={position.active || !forkingCapability.enabled}
5675
6624
  {...(forkingCapability.reason === undefined
5676
6625
  ? {}
5677
6626
  : { title: forkingCapability.reason })}
5678
- onPress={() =>
5679
- void regenerateAction.execute().catch(() => undefined)
5680
- }
6627
+ onPress={(event) => {
6628
+ const keyboard = pressedByKeyboard(event);
6629
+ void regenerateAction
6630
+ .execute()
6631
+ .catch(() => undefined)
6632
+ // The new response replaces this toolbar.
6633
+ .finally(() => {
6634
+ if (keyboard) rescueFocusToComposer();
6635
+ });
6636
+ }}
5681
6637
  />
5682
6638
  ) : null}
5683
6639
  {showFork ? (
@@ -5685,10 +6641,10 @@ export const AgentMessageActions = memo(function AgentMessageActions({
5685
6641
  label={labels.forkFromHere}
5686
6642
  icon={<IconArrowFork aria-hidden="true" />}
5687
6643
  size="compact"
5688
- className="size-6"
6644
+ className="size-6 rounded-[var(--agent-kit-radius-xs,0.25rem)]"
5689
6645
  data-message-action="fork"
5690
- pending={forkAction.pending}
5691
- disabled={!forkingCapability.enabled || forkAction.pending}
6646
+ busy={forkAction.pending}
6647
+ unavailable={!forkingCapability.enabled}
5692
6648
  {...(forkingCapability.reason === undefined
5693
6649
  ? {}
5694
6650
  : { title: forkingCapability.reason })}
@@ -5708,7 +6664,7 @@ export const AgentMessageActions = memo(function AgentMessageActions({
5708
6664
  )
5709
6665
  }
5710
6666
  size="compact"
5711
- className="size-6"
6667
+ className="size-6 rounded-[var(--agent-kit-radius-xs,0.25rem)]"
5712
6668
  data-message-action="request-id"
5713
6669
  aria-busy={requestIdAction.pending || undefined}
5714
6670
  onPress={() => {
@@ -5728,7 +6684,7 @@ export const AgentMessageActions = memo(function AgentMessageActions({
5728
6684
  size="compact"
5729
6685
  // Hover is lighter than open, the toolkit's menu-trigger pattern,
5730
6686
  // so pressing it reads as a change while the pointer is still on it.
5731
- className="agentkit-message-actions-toggle size-6 hover:bg-accent/60 aria-expanded:bg-accent aria-expanded:text-foreground"
6687
+ className="agentkit-message-actions-toggle size-6 rounded-[var(--agent-kit-radius-xs,0.25rem)] hover:bg-accent/60 aria-expanded:bg-accent aria-expanded:text-foreground"
5732
6688
  data-message-actions-toggle=""
5733
6689
  aria-expanded={moreActionsOpen}
5734
6690
  aria-controls={secondaryActionsId}
@@ -5743,7 +6699,7 @@ export const AgentMessageActions = memo(function AgentMessageActions({
5743
6699
  <span className="agentkit-visually-hidden" aria-live="polite">
5744
6700
  {feedbackConfirmed
5745
6701
  ? labels.feedbackSubmitted
5746
- : requestIdCopied
6702
+ : requestIdCopied || copied
5747
6703
  ? labels.copied
5748
6704
  : ""}
5749
6705
  </span>
@@ -5751,6 +6707,7 @@ export const AgentMessageActions = memo(function AgentMessageActions({
5751
6707
  ) : null;
5752
6708
  return (
5753
6709
  <div
6710
+ ref={actionsRef}
5754
6711
  className="agentkit-message-actions"
5755
6712
  data-role={message.role}
5756
6713
  data-placement={placement}
@@ -5758,6 +6715,9 @@ export const AgentMessageActions = memo(function AgentMessageActions({
5758
6715
  data-streaming={message.status === "streaming" ? "true" : undefined}
5759
6716
  data-expanded={moreActionsOpen ? "true" : undefined}
5760
6717
  data-has-error={footerError ? "true" : undefined}
6718
+ {...(isAssistant
6719
+ ? {}
6720
+ : { role: "toolbar", "aria-label": labels.messageActions })}
5761
6721
  >
5762
6722
  {isAssistant ? (
5763
6723
  <>
@@ -5776,8 +6736,8 @@ export const AgentMessageActions = memo(function AgentMessageActions({
5776
6736
  label={labels.editMessage}
5777
6737
  icon={<IconPencil aria-hidden="true" />}
5778
6738
  size="compact"
5779
- className="size-6"
5780
- disabled={position.active || !forkingCapability.enabled}
6739
+ className="size-6 rounded-[var(--agent-kit-radius-xs,0.25rem)]"
6740
+ unavailable={position.active || !forkingCapability.enabled}
5781
6741
  {...(forkingCapability.reason === undefined
5782
6742
  ? {}
5783
6743
  : { title: forkingCapability.reason })}
@@ -5794,6 +6754,9 @@ export const AgentMessageActions = memo(function AgentMessageActions({
5794
6754
  />
5795
6755
  ) : null}
5796
6756
  </div>
6757
+ <span className="agentkit-visually-hidden" aria-live="polite">
6758
+ {copied ? labels.copied : ""}
6759
+ </span>
5797
6760
  </>
5798
6761
  )}
5799
6762
  {footerError ? (
@@ -5909,15 +6872,28 @@ export const AgentMessageView = memo(function AgentMessageView({
5909
6872
  let hasSeenWidget = false;
5910
6873
  const renderedContent: ReactNode[] = [];
5911
6874
  let citationRun: ReactNode[] = [];
6875
+ let citationParts: CitationPart[] = [];
5912
6876
  let citationRunKey = "";
5913
6877
  const flushCitations = () => {
5914
6878
  if (!citationRun.length) return;
5915
6879
  renderedContent.push(
5916
- <span key={`citations:${citationRunKey}`} className="agentkit-citations">
5917
- {citationRun}
5918
- </span>,
6880
+ // A host citation renderer keeps the inline run it was written for.
6881
+ slots.citation || registry.messageParts?.citation ? (
6882
+ <span
6883
+ key={`citations:${citationRunKey}`}
6884
+ className="agentkit-citations"
6885
+ >
6886
+ {citationRun}
6887
+ </span>
6888
+ ) : (
6889
+ <AgentSources
6890
+ key={`citations:${citationRunKey}`}
6891
+ citations={citationParts}
6892
+ />
6893
+ ),
5919
6894
  );
5920
6895
  citationRun = [];
6896
+ citationParts = [];
5921
6897
  };
5922
6898
  orderedContent.forEach((item, index) => {
5923
6899
  const isWidget = item.type === "widget" || item.part.type === "widget";
@@ -5944,6 +6920,7 @@ export const AgentMessageView = memo(function AgentMessageView({
5944
6920
  if (item.type === "part" && item.part.type === "citation") {
5945
6921
  if (!citationRun.length) citationRunKey = item.key;
5946
6922
  citationRun.push(<Fragment key={item.key}>{content}</Fragment>);
6923
+ citationParts.push(item.part);
5947
6924
  return;
5948
6925
  }
5949
6926
  flushCitations();
@@ -5970,6 +6947,10 @@ export const AgentMessageView = memo(function AgentMessageView({
5970
6947
  aria-label={message.role === "user" ? labels.you : labels.assistant}
5971
6948
  aria-busy={message.status === "streaming"}
5972
6949
  >
6950
+ {/* Lets screen-reader users jump between turns by heading. */}
6951
+ <h2 className="agentkit-visually-hidden">
6952
+ {message.role === "user" ? labels.you : labels.assistant}
6953
+ </h2>
5973
6954
  <div className="agentkit-message-content">
5974
6955
  {renderedContent}
5975
6956
  {attachedAnnotations.length ? (
@@ -6057,19 +7038,17 @@ export function AgentRunFailure({
6057
7038
  setContinueFailed(true);
6058
7039
  });
6059
7040
  };
6060
- // A failure the user can recover from is a warning; red means it is final.
6061
- const tone = error.retryable === true ? "warning" : "danger";
6062
- const ToneIcon = tone === "warning" ? IconAlertTriangle : IconAlertCircle;
7041
+ // Amber, not red, even when not retryable: a failed run destroys nothing.
6063
7042
  return (
6064
7043
  <div
6065
7044
  className="agentkit-run-failure"
6066
- data-tone={tone}
7045
+ data-tone="warning"
6067
7046
  data-error-code={error.code}
6068
7047
  data-run-id={runId}
6069
7048
  data-thread-id={threadId}
6070
7049
  role={!superseded && latestFailure ? "alert" : undefined}
6071
7050
  >
6072
- <ToneIcon aria-hidden="true" className="agentkit-icon" />
7051
+ <IconAlertTriangle aria-hidden="true" className="agentkit-icon" />
6073
7052
  <div className="agentkit-run-failure-copy">
6074
7053
  <strong>{labels.runFailed}</strong>
6075
7054
  <span {...SESSION_REPLAY_MASK_PROPS}>{error.message}</span>
@@ -6106,7 +7085,7 @@ export function AgentConnectionErrorView({
6106
7085
  const { labels } = useAgentKit();
6107
7086
  return (
6108
7087
  <ComposerNotice
6109
- tone={error.retryable ? "warning" : "danger"}
7088
+ tone="warning"
6110
7089
  role="alert"
6111
7090
  noticeId="connection-error"
6112
7091
  className="agentkit-connection-notice"
@@ -6622,7 +7601,7 @@ export function AgentKitComposer({
6622
7601
  trigger={
6623
7602
  <span
6624
7603
  tabIndex={0}
6625
- className="inline-flex min-w-0 items-center gap-2 rounded-sm outline-none focus-visible:ring-2 focus-visible:ring-ring"
7604
+ className="inline-flex min-w-0 items-center gap-2 rounded-[var(--agent-kit-radius-xs,0.25rem)] outline-none focus-visible:ring-2 focus-visible:ring-ring"
6626
7605
  >
6627
7606
  <IconPencil aria-hidden="true" className="size-4 shrink-0" />
6628
7607
  <span id={editingLabelId} className="truncate">
@@ -6799,6 +7778,9 @@ export function AgentKitComposer({
6799
7778
  threadId: forkedThread.id,
6800
7779
  text: payload.text,
6801
7780
  attachments: [...payload.attachments],
7781
+ ...(payload.references.length
7782
+ ? { references: [...payload.references] }
7783
+ : {}),
6802
7784
  options: payload.options,
6803
7785
  metadata: sendMetadata,
6804
7786
  onLocalSubmit,
@@ -6841,6 +7823,9 @@ export function AgentKitComposer({
6841
7823
  const message = {
6842
7824
  text: payload.text,
6843
7825
  attachments: [...payload.attachments],
7826
+ ...(payload.references.length
7827
+ ? { references: [...payload.references] }
7828
+ : {}),
6844
7829
  options: payload.options,
6845
7830
  metadata: sendMetadata,
6846
7831
  queueWhileRunning,
@@ -7280,6 +8265,8 @@ export interface AgentKitChatProps {
7280
8265
  emptyComposerPlacement?: "center" | "bottom";
7281
8266
  /** Follow new output until the user deliberately scrolls away. */
7282
8267
  autoScroll?: boolean;
8268
+ /** Hold a sent message near the top while its reply streams; `false` follows the bottom. */
8269
+ anchorTurns?: boolean;
7283
8270
  /** Optional transcript geometry. Omitted values keep the shared defaults. */
7284
8271
  transcriptLayout?: AgentChatTranscriptLayout;
7285
8272
  /**
@@ -7408,6 +8395,8 @@ const AGENTKIT_HISTORY_ROW_CHUNK = 16;
7408
8395
  const AGENTKIT_TRANSCRIPT_SCROLLBAR_IDLE_MS = 900;
7409
8396
  /** Outlasts the reveal's bounded drain after a run ends. */
7410
8397
  const AGENTKIT_TRANSCRIPT_GLIDE_AFTER_RUN_MS = 2_000;
8398
+ /** How much of the previous turn shows above an anchored message. */
8399
+ const AGENTKIT_TURN_ANCHOR_PEEK_PX = 32;
7411
8400
  const AGENTKIT_TRANSCRIPT_SCROLL_KEYS = new Set([
7412
8401
  "ArrowDown",
7413
8402
  "ArrowUp",
@@ -7417,7 +8406,8 @@ const AGENTKIT_TRANSCRIPT_SCROLL_KEYS = new Set([
7417
8406
  "PageUp",
7418
8407
  " ",
7419
8408
  ]);
7420
- const AGENTKIT_ACTIVITY_ANNOUNCE_INTERVAL_MS = 2_000;
8409
+ // Long enough for a screen reader to finish speaking between step names.
8410
+ const AGENTKIT_ACTIVITY_ANNOUNCE_INTERVAL_MS = 10_000;
7421
8411
 
7422
8412
  export function isAgentKitTranscriptNearBottom(
7423
8413
  element: Pick<HTMLElement, "clientHeight" | "scrollHeight" | "scrollTop">,
@@ -7445,7 +8435,7 @@ export function AgentKitTranscriptSkeleton({
7445
8435
  >
7446
8436
  <span className="agentkit-visually-hidden">{label}</span>
7447
8437
  <div className="agentkit-transcript-skeleton-user" aria-hidden="true">
7448
- <Skeleton className="agentkit-transcript-skeleton-bubble ms-auto h-10 w-2/5 rounded-2xl" />
8438
+ <Skeleton className="agentkit-transcript-skeleton-bubble ms-auto h-11 w-2/5 rounded-[var(--agent-kit-radius-sm,0.5rem)]" />
7449
8439
  </div>
7450
8440
  <div
7451
8441
  className="agentkit-transcript-skeleton-assistant"
@@ -7565,6 +8555,24 @@ function currentRunActivityLabel(
7565
8555
  return labels.working;
7566
8556
  }
7567
8557
 
8558
+ // A run that stops to wait on the user is as much news as one that ends.
8559
+ const RUN_STATUS_ANNOUNCEMENTS: Partial<
8560
+ Record<
8561
+ string,
8562
+ | "responseReady"
8563
+ | "responseFailed"
8564
+ | "runStopped"
8565
+ | "waitingForApproval"
8566
+ | "waitingForInput"
8567
+ >
8568
+ > = {
8569
+ completed: "responseReady",
8570
+ failed: "responseFailed",
8571
+ cancelled: "runStopped",
8572
+ awaiting_approval: "waitingForApproval",
8573
+ awaiting_input: "waitingForInput",
8574
+ };
8575
+
7568
8576
  /**
7569
8577
  * One polite status region for the whole chat. The transcript itself stays
7570
8578
  * aria-live="off": streamed text would otherwise be read token by token.
@@ -7575,6 +8583,8 @@ function AgentKitLiveStatus({ threadId }: { threadId: string }) {
7575
8583
  const runId = latestRunId(thread);
7576
8584
  const run = runId ? thread.runs[runId] : undefined;
7577
8585
  const status = run?.status;
8586
+ // A failure with an error has its own alert; announcing it here reads it twice.
8587
+ const failureHasAlert = status === "failed" && Boolean(run?.error);
7578
8588
  const running = status === "running";
7579
8589
  const activityText =
7580
8590
  running && runId ? currentRunActivityLabel(thread, runId, labels) : "";
@@ -7599,15 +8609,14 @@ function AgentKitLiveStatus({ threadId }: { threadId: string }) {
7599
8609
  if (!runId || previous.runId !== runId || previous.status === status) {
7600
8610
  return;
7601
8611
  }
7602
- if (status === "completed" || status === "failed") {
7603
- if (pendingTimerRef.current) clearTimeout(pendingTimerRef.current);
7604
- pendingTimerRef.current = undefined;
7605
- lastAnnouncedAtRef.current = Date.now();
7606
- setAnnouncement(
7607
- status === "completed" ? labels.responseReady : labels.responseFailed,
7608
- );
7609
- }
7610
- }, [labels.responseFailed, labels.responseReady, runId, status]);
8612
+ const announced = status ? RUN_STATUS_ANNOUNCEMENTS[status] : undefined;
8613
+ if (!announced) return;
8614
+ if (pendingTimerRef.current) clearTimeout(pendingTimerRef.current);
8615
+ pendingTimerRef.current = undefined;
8616
+ lastAnnouncedAtRef.current = Date.now();
8617
+ // Cleared rather than left on the last step name, which is stale now.
8618
+ setAnnouncement(failureHasAlert ? "" : labels[announced]);
8619
+ }, [failureHasAlert, labels, runId, status]);
7611
8620
  useEffect(() => {
7612
8621
  if (!activityText) return;
7613
8622
  const announce = () => {
@@ -7672,6 +8681,7 @@ export function AgentKitChat({
7672
8681
  hasRenderedMessages = false,
7673
8682
  emptyComposerPlacement = "center",
7674
8683
  autoScroll = true,
8684
+ anchorTurns = true,
7675
8685
  transcriptLayout,
7676
8686
  expectExistingThread,
7677
8687
  onStartNewChat,
@@ -7686,6 +8696,7 @@ export function AgentKitChat({
7686
8696
  onThreadForked,
7687
8697
  threadLoad,
7688
8698
  formatWorkedForDuration,
8699
+ requestComposerFocus,
7689
8700
  } = useAgentKit();
7690
8701
  const formatReasoningDuration = (ms: number) =>
7691
8702
  formatWorkedForDuration?.(ms) ??
@@ -7773,6 +8784,9 @@ export function AgentKitChat({
7773
8784
  }, [threadId]);
7774
8785
  useRealtimeVoiceTranscripts(controller, threadId, hasActiveRuns(thread));
7775
8786
  const transcriptContentRef = useRef<HTMLDivElement>(null);
8787
+ const turnSpacerRef = useRef<HTMLDivElement>(null);
8788
+ // Set by a message sent in this thread; a thread that opens has no anchor.
8789
+ const turnAnchoredRef = useRef(false);
7776
8790
  const fileDragDepthRef = useRef(0);
7777
8791
  const followsTranscriptRef = useRef(true);
7778
8792
  const programmaticScrollTopRef = useRef<number | undefined>(undefined);
@@ -7968,7 +8982,41 @@ export function AgentKitChat({
7968
8982
  scheduleTranscriptScrollbarHide();
7969
8983
  }, [scheduleTranscriptScrollbarHide]);
7970
8984
 
8985
+ // The spacer pads what the reply has not filled, so following the bottom holds
8986
+ // the sent message near the top. Sized imperatively so streaming never re-renders.
8987
+ const updateTurnSpacer = useCallback(() => {
8988
+ const transcript = transcriptRef.current;
8989
+ const content = transcriptContentRef.current;
8990
+ const spacer = turnSpacerRef.current;
8991
+ if (!transcript || !content || !spacer) return;
8992
+ // No stylesheet may size the spacer: its inline height is read back as-is.
8993
+ const current = Number.parseFloat(spacer.style.blockSize) || 0;
8994
+ const rows = content.querySelectorAll<HTMLElement>(
8995
+ '.agentkit-message[data-role="user"]',
8996
+ );
8997
+ const row = rows.length ? rows[rows.length - 1] : undefined;
8998
+ if (!anchorTurns || !turnAnchoredRef.current || !row) {
8999
+ if (current) spacer.style.blockSize = "";
9000
+ return;
9001
+ }
9002
+ const rowTop =
9003
+ row.getBoundingClientRect().top -
9004
+ transcript.getBoundingClientRect().top +
9005
+ transcript.scrollTop;
9006
+ const anchorTop = Math.max(0, rowTop - AGENTKIT_TURN_ANCHOR_PEEK_PX);
9007
+ const next = Math.max(
9008
+ 0,
9009
+ Math.ceil(
9010
+ anchorTop +
9011
+ transcript.clientHeight -
9012
+ (transcript.scrollHeight - current),
9013
+ ),
9014
+ );
9015
+ if (next !== current) spacer.style.blockSize = next ? `${next}px` : "";
9016
+ }, [anchorTurns]);
9017
+
7971
9018
  const scrollTranscriptToBottom = useCallback(() => {
9019
+ updateTurnSpacer();
7972
9020
  const transcript = transcriptRef.current;
7973
9021
  if (!autoScroll || !transcript || !followsTranscriptRef.current) return;
7974
9022
  const nextScrollTop = Math.max(
@@ -7977,7 +9025,7 @@ export function AgentKitChat({
7977
9025
  );
7978
9026
  if (Math.abs(transcript.scrollTop - nextScrollTop) <= 1) return;
7979
9027
  writeTranscriptScrollTop(transcript, nextScrollTop);
7980
- }, [autoScroll, writeTranscriptScrollTop]);
9028
+ }, [autoScroll, updateTurnSpacer, writeTranscriptScrollTop]);
7981
9029
 
7982
9030
  const stopTranscriptGlide = useCallback(() => {
7983
9031
  transcriptGlideRef.current?.glide.stop();
@@ -7986,6 +9034,7 @@ export function AgentKitChat({
7986
9034
  // Streamed growth glides to the new bottom; anything else (opening a
7987
9035
  // thread, a long jump) lands there at once instead of gliding through it.
7988
9036
  const followTranscriptToBottom = useCallback(() => {
9037
+ updateTurnSpacer();
7989
9038
  const transcript = transcriptRef.current;
7990
9039
  if (!autoScroll || !transcript || !followsTranscriptRef.current) return;
7991
9040
  const distance =
@@ -8004,7 +9053,22 @@ export function AgentKitChat({
8004
9053
  transcriptGlideRef.current = {
8005
9054
  element: transcript,
8006
9055
  glide: new TranscriptGlide({
8007
- element: transcript,
9056
+ // Size the spacer first so an anchored message holds still as the reply grows.
9057
+ element: {
9058
+ get scrollTop() {
9059
+ return transcript.scrollTop;
9060
+ },
9061
+ set scrollTop(value: number) {
9062
+ transcript.scrollTop = value;
9063
+ },
9064
+ get scrollHeight() {
9065
+ updateTurnSpacer();
9066
+ return transcript.scrollHeight;
9067
+ },
9068
+ get clientHeight() {
9069
+ return transcript.clientHeight;
9070
+ },
9071
+ },
8008
9072
  write: (scrollTop) => writeTranscriptScrollTop(transcript, scrollTop),
8009
9073
  following: () => followsTranscriptRef.current,
8010
9074
  }),
@@ -8015,6 +9079,7 @@ export function AgentKitChat({
8015
9079
  autoScroll,
8016
9080
  scrollTranscriptToBottom,
8017
9081
  stopTranscriptGlide,
9082
+ updateTurnSpacer,
8018
9083
  writeTranscriptScrollTop,
8019
9084
  ]);
8020
9085
 
@@ -8110,6 +9175,7 @@ export function AgentKitChat({
8110
9175
  if (transcriptThreadIdRef.current !== threadId) {
8111
9176
  transcriptThreadIdRef.current = threadId;
8112
9177
  previousLastMessageIdRef.current = undefined;
9178
+ turnAnchoredRef.current = false;
8113
9179
  transcriptGlidesUntilRef.current = 0;
8114
9180
  jumps = true;
8115
9181
  setTranscriptFollow(true);
@@ -8124,6 +9190,8 @@ export function AgentKitChat({
8124
9190
  lastMessage.id !== previousLastMessageId
8125
9191
  ) {
8126
9192
  jumps = true;
9193
+ // Only a send anchors: the first render of an opened thread is not one.
9194
+ if (previousLastMessageId !== undefined) turnAnchoredRef.current = true;
8127
9195
  setTranscriptFollow(true);
8128
9196
  hideTranscriptScrollbar();
8129
9197
  }
@@ -8146,6 +9214,31 @@ export function AgentKitChat({
8146
9214
  threadId,
8147
9215
  ]);
8148
9216
 
9217
+ // Anchor from the pending submission so the message never lands at the bottom first.
9218
+ const newestPendingSubmissionId = pendingSubmissions.at(-1)?.id;
9219
+ const previousPendingSubmissionIdRef = useRef(newestPendingSubmissionId);
9220
+ useLayoutEffect(() => {
9221
+ const previous = previousPendingSubmissionIdRef.current;
9222
+ previousPendingSubmissionIdRef.current = newestPendingSubmissionId;
9223
+ if (
9224
+ !autoScroll ||
9225
+ !newestPendingSubmissionId ||
9226
+ newestPendingSubmissionId === previous
9227
+ ) {
9228
+ return;
9229
+ }
9230
+ turnAnchoredRef.current = true;
9231
+ setTranscriptFollow(true);
9232
+ stopTranscriptGlide();
9233
+ scrollTranscriptToBottom();
9234
+ }, [
9235
+ autoScroll,
9236
+ newestPendingSubmissionId,
9237
+ scrollTranscriptToBottom,
9238
+ setTranscriptFollow,
9239
+ stopTranscriptGlide,
9240
+ ]);
9241
+
8149
9242
  useLayoutEffect(() => {
8150
9243
  const transcript = transcriptRef.current;
8151
9244
  const content = transcriptContentRef.current;
@@ -9164,13 +10257,29 @@ export function AgentKitChat({
9164
10257
  ) : null}
9165
10258
  {toolbar ? (
9166
10259
  <AgentKitRegionSlot slot={Toolbar} threadId={threadId}>
9167
- <div className="agentkit-chat-toolbar">{toolbar}</div>
10260
+ <div
10261
+ // Host buttons carry the primitive's own radius
10262
+ // utility, which beats layered CSS.
10263
+ className="agentkit-chat-toolbar [&_:is(button,[role=button])]:rounded-[var(--agentkit-radius-xs)]"
10264
+ >
10265
+ {toolbar}
10266
+ </div>
9168
10267
  </AgentKitRegionSlot>
9169
10268
  ) : null}
9170
10269
  </header>
9171
10270
  </AgentKitRegionSlot>
9172
10271
  </AgentKitSurfaceBoundary>
9173
10272
  ) : null}
10273
+ {composer && !showEmptyState ? (
10274
+ <button
10275
+ type="button"
10276
+ className="agentkit-skip-link"
10277
+ data-agentkit-skip-link="composer"
10278
+ onClick={() => requestComposerFocus(threadId)}
10279
+ >
10280
+ {labels.skipToComposer}
10281
+ </button>
10282
+ ) : null}
9174
10283
  <div
9175
10284
  ref={transcriptRef}
9176
10285
  className="agentkit-transcript"
@@ -9296,6 +10405,11 @@ export function AgentKitChat({
9296
10405
  </AgentKitSurfaceBoundary>
9297
10406
  ) : null}
9298
10407
  </AgentKitRegionSlot>
10408
+ <div
10409
+ ref={turnSpacerRef}
10410
+ className="agentkit-turn-spacer"
10411
+ aria-hidden="true"
10412
+ />
9299
10413
  </div>
9300
10414
  </div>
9301
10415
  {/* Outside the scroller so its shadow is never clipped and it
@@ -9310,8 +10424,14 @@ export function AgentKitChat({
9310
10424
  label={labels.scrollToBottom}
9311
10425
  icon={<IconArrowDown aria-hidden="true" />}
9312
10426
  size="compact"
9313
- className="rounded-full border border-[color:var(--agentkit-border)] bg-(--agentkit-surface) text-[color:var(--agentkit-text)] shadow-(--agent-kit-overlay-elevation) hover:bg-(--agentkit-subtle) hover:text-[color:var(--agentkit-text)]"
9314
- onPress={returnTranscriptToBottom}
10427
+ className="rounded-[var(--agent-kit-radius-sm,0.5rem)] border border-[color:var(--agentkit-border)] bg-(--agentkit-surface) text-[color:var(--agentkit-text)] shadow-(--agent-kit-overlay-elevation) hover:bg-(--agentkit-subtle) hover:text-[color:var(--agentkit-text)]"
10428
+ onPress={(event) => {
10429
+ returnTranscriptToBottom();
10430
+ // The control goes inert at the bottom, which would drop
10431
+ // a keyboard user's focus to the page.
10432
+ if (pressedByKeyboard(event))
10433
+ requestComposerFocus(threadId);
10434
+ }}
9315
10435
  />
9316
10436
  </div>
9317
10437
  <AgentKitLiveStatus threadId={threadId} />