@opengeni/react 1.3.0 → 1.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (240) hide show
  1. package/README.md +29 -1
  2. package/dist/accounts.js +2 -0
  3. package/dist/accounts.js.map +1 -1
  4. package/dist/artifacts-document.js +5 -4
  5. package/dist/artifacts-presentation.js +5 -4
  6. package/dist/artifacts-spreadsheet.js +4 -3
  7. package/dist/artifacts.js +14 -13
  8. package/dist/artifacts.js.map +1 -1
  9. package/dist/browser-viewer-WGJ43YGI.js +17 -0
  10. package/dist/chat-interactive-block-ZFHOKTMS.js +12 -0
  11. package/dist/{chunk-WEHMPOXK.js → chunk-2DDJ6YH6.js} +3 -1
  12. package/dist/{chunk-WEHMPOXK.js.map → chunk-2DDJ6YH6.js.map} +1 -1
  13. package/dist/{chunk-C5X5AJXK.js → chunk-2HVLQ26H.js} +5 -4
  14. package/dist/{chunk-C5X5AJXK.js.map → chunk-2HVLQ26H.js.map} +1 -1
  15. package/dist/{chunk-A4UJB2MM.js → chunk-32CGLXW2.js} +4 -3
  16. package/dist/{chunk-A4UJB2MM.js.map → chunk-32CGLXW2.js.map} +1 -1
  17. package/dist/{chunk-5CVXZA3Y.js → chunk-45NUOSXQ.js} +3 -2
  18. package/dist/{chunk-5CVXZA3Y.js.map → chunk-45NUOSXQ.js.map} +1 -1
  19. package/dist/{chunk-WUISZ3WU.js → chunk-53KJBGPZ.js} +5 -4
  20. package/dist/{chunk-WUISZ3WU.js.map → chunk-53KJBGPZ.js.map} +1 -1
  21. package/dist/{chunk-TMYGL6X5.js → chunk-55DXD22O.js} +73 -7
  22. package/dist/chunk-55DXD22O.js.map +1 -0
  23. package/dist/{chunk-JT4JW7BC.js → chunk-5I4VRWBB.js} +3 -1
  24. package/dist/{chunk-JT4JW7BC.js.map → chunk-5I4VRWBB.js.map} +1 -1
  25. package/dist/{chunk-UK4TINKT.js → chunk-B27E3V37.js} +3 -1
  26. package/dist/{chunk-UK4TINKT.js.map → chunk-B27E3V37.js.map} +1 -1
  27. package/dist/{chunk-J6DBS4T3.js → chunk-BNKV5ZLF.js} +5 -4
  28. package/dist/{chunk-J6DBS4T3.js.map → chunk-BNKV5ZLF.js.map} +1 -1
  29. package/dist/{chunk-YM2DE7BQ.js → chunk-C5BF4L26.js} +13 -12
  30. package/dist/{chunk-YM2DE7BQ.js.map → chunk-C5BF4L26.js.map} +1 -1
  31. package/dist/{chunk-YBXQDQFX.js → chunk-C7CMD56T.js} +6 -5
  32. package/dist/{chunk-YBXQDQFX.js.map → chunk-C7CMD56T.js.map} +1 -1
  33. package/dist/{chunk-UGLEYEMP.js → chunk-CM7LVUKC.js} +3 -1
  34. package/dist/{chunk-UGLEYEMP.js.map → chunk-CM7LVUKC.js.map} +1 -1
  35. package/dist/{chunk-PGSX7UCP.js → chunk-CREMJMVN.js} +1502 -833
  36. package/dist/chunk-CREMJMVN.js.map +1 -0
  37. package/dist/{chunk-JVYQRRC4.js → chunk-CVYCLFMH.js} +3 -1
  38. package/dist/{chunk-JVYQRRC4.js.map → chunk-CVYCLFMH.js.map} +1 -1
  39. package/dist/{chunk-Y2PRMYLC.js → chunk-G3U2A6GS.js} +3 -2
  40. package/dist/{chunk-Y2PRMYLC.js.map → chunk-G3U2A6GS.js.map} +1 -1
  41. package/dist/{chunk-WWEUO2PL.js → chunk-G6YRCIYJ.js} +202 -14
  42. package/dist/chunk-G6YRCIYJ.js.map +1 -0
  43. package/dist/{chunk-B3V6TG77.js → chunk-GC3PH6N7.js} +3 -1
  44. package/dist/{chunk-B3V6TG77.js.map → chunk-GC3PH6N7.js.map} +1 -1
  45. package/dist/{chunk-FZCP527Y.js → chunk-HXYOSSQO.js} +3 -2
  46. package/dist/{chunk-FZCP527Y.js.map → chunk-HXYOSSQO.js.map} +1 -1
  47. package/dist/{chunk-BRM6FVZA.js → chunk-IEE7WCLN.js} +4 -3
  48. package/dist/{chunk-BRM6FVZA.js.map → chunk-IEE7WCLN.js.map} +1 -1
  49. package/dist/{chunk-DYWBUX5Q.js → chunk-IMSUVYQB.js} +6 -5
  50. package/dist/{chunk-DYWBUX5Q.js.map → chunk-IMSUVYQB.js.map} +1 -1
  51. package/dist/{chunk-CY7WK5FD.js → chunk-IUHHPMAX.js} +203 -46
  52. package/dist/chunk-IUHHPMAX.js.map +1 -0
  53. package/dist/{chunk-SGKD2OCN.js → chunk-IYQCWGU5.js} +6 -5
  54. package/dist/{chunk-SGKD2OCN.js.map → chunk-IYQCWGU5.js.map} +1 -1
  55. package/dist/{chunk-J2QWVSQY.js → chunk-JH7V627O.js} +8 -7
  56. package/dist/{chunk-J2QWVSQY.js.map → chunk-JH7V627O.js.map} +1 -1
  57. package/dist/{chunk-EESD3PY3.js → chunk-JHTIMBOL.js} +6 -5
  58. package/dist/{chunk-EESD3PY3.js.map → chunk-JHTIMBOL.js.map} +1 -1
  59. package/dist/{chunk-LNIRAOA3.js → chunk-L2J3PU7A.js} +7 -6
  60. package/dist/{chunk-LNIRAOA3.js.map → chunk-L2J3PU7A.js.map} +1 -1
  61. package/dist/{chunk-ZYRGEKWS.js → chunk-LJUMYUBO.js} +3 -1
  62. package/dist/{chunk-ZYRGEKWS.js.map → chunk-LJUMYUBO.js.map} +1 -1
  63. package/dist/{chunk-4RVYOY4O.js → chunk-LSW2OJVV.js} +4 -3
  64. package/dist/{chunk-4RVYOY4O.js.map → chunk-LSW2OJVV.js.map} +1 -1
  65. package/dist/{chunk-UTRTG6LH.js → chunk-LVS3MH5T.js} +3 -1
  66. package/dist/{chunk-UTRTG6LH.js.map → chunk-LVS3MH5T.js.map} +1 -1
  67. package/dist/chunk-M357MHH3.js +2 -0
  68. package/dist/{chunk-GYVRFZDY.js → chunk-MDIXFKA3.js} +9 -8
  69. package/dist/{chunk-GYVRFZDY.js.map → chunk-MDIXFKA3.js.map} +1 -1
  70. package/dist/{chunk-WRHIZYZF.js → chunk-MEQM4LI3.js} +6 -5
  71. package/dist/{chunk-WRHIZYZF.js.map → chunk-MEQM4LI3.js.map} +1 -1
  72. package/dist/chunk-NX4ADH73.js +14 -0
  73. package/dist/chunk-NX4ADH73.js.map +1 -0
  74. package/dist/{chunk-XCHWDJ22.js → chunk-OGTTUYNT.js} +3 -1
  75. package/dist/{chunk-XCHWDJ22.js.map → chunk-OGTTUYNT.js.map} +1 -1
  76. package/dist/{chunk-GIMRELBW.js → chunk-OYQQJL3J.js} +3 -1
  77. package/dist/{chunk-GIMRELBW.js.map → chunk-OYQQJL3J.js.map} +1 -1
  78. package/dist/{chunk-RSI7XYXL.js → chunk-PBCZL4JP.js} +6 -5
  79. package/dist/{chunk-RSI7XYXL.js.map → chunk-PBCZL4JP.js.map} +1 -1
  80. package/dist/{chunk-I7IWRQLN.js → chunk-PHBFJCEU.js} +17 -11
  81. package/dist/chunk-PHBFJCEU.js.map +1 -0
  82. package/dist/{chunk-3CTRFMAK.js → chunk-PKCAYHDC.js} +5 -4
  83. package/dist/{chunk-3CTRFMAK.js.map → chunk-PKCAYHDC.js.map} +1 -1
  84. package/dist/{chunk-K3HQAKQX.js → chunk-PMV53IMM.js} +3 -1
  85. package/dist/{chunk-K3HQAKQX.js.map → chunk-PMV53IMM.js.map} +1 -1
  86. package/dist/{chunk-YQEAA6LT.js → chunk-QHUE7E4N.js} +3 -2
  87. package/dist/{chunk-YQEAA6LT.js.map → chunk-QHUE7E4N.js.map} +1 -1
  88. package/dist/{chunk-RC6BZ44Q.js → chunk-QMMCJGFI.js} +4 -3
  89. package/dist/{chunk-RC6BZ44Q.js.map → chunk-QMMCJGFI.js.map} +1 -1
  90. package/dist/{chunk-YV4L3ILF.js → chunk-QV3DJVRZ.js} +3 -1
  91. package/dist/{chunk-YV4L3ILF.js.map → chunk-QV3DJVRZ.js.map} +1 -1
  92. package/dist/{chunk-447JLCBU.js → chunk-S2QH6EZ5.js} +3 -1
  93. package/dist/{chunk-447JLCBU.js.map → chunk-S2QH6EZ5.js.map} +1 -1
  94. package/dist/{chunk-PNY3JKBJ.js → chunk-SFH43JT2.js} +5 -4
  95. package/dist/{chunk-PNY3JKBJ.js.map → chunk-SFH43JT2.js.map} +1 -1
  96. package/dist/{chunk-ERGYJIBB.js → chunk-T46QZH3E.js} +4 -2
  97. package/dist/{chunk-ERGYJIBB.js.map → chunk-T46QZH3E.js.map} +1 -1
  98. package/dist/{chunk-5J4FOW6Q.js → chunk-TNEZNMSN.js} +3 -1
  99. package/dist/{chunk-5J4FOW6Q.js.map → chunk-TNEZNMSN.js.map} +1 -1
  100. package/dist/{chunk-22KP6LR5.js → chunk-TXS65BB2.js} +3 -1
  101. package/dist/{chunk-22KP6LR5.js.map → chunk-TXS65BB2.js.map} +1 -1
  102. package/dist/{chunk-742CZ3L3.js → chunk-UDPH5SLV.js} +19 -202
  103. package/dist/chunk-UDPH5SLV.js.map +1 -0
  104. package/dist/{chunk-HDJWDK7W.js → chunk-VCSERYXH.js} +10 -9
  105. package/dist/{chunk-HDJWDK7W.js.map → chunk-VCSERYXH.js.map} +1 -1
  106. package/dist/{chunk-VZTQ73XT.js → chunk-XEA5BCMA.js} +3 -1
  107. package/dist/{chunk-VZTQ73XT.js.map → chunk-XEA5BCMA.js.map} +1 -1
  108. package/dist/{chunk-5F4WR7X7.js → chunk-XS67YG5R.js} +3 -1
  109. package/dist/{chunk-5F4WR7X7.js.map → chunk-XS67YG5R.js.map} +1 -1
  110. package/dist/{chunk-TKOTKRWY.js → chunk-XTD7TZHW.js} +3 -1
  111. package/dist/{chunk-TKOTKRWY.js.map → chunk-XTD7TZHW.js.map} +1 -1
  112. package/dist/{chunk-A2PI6UMY.js → chunk-YSRBFJMQ.js} +3 -1
  113. package/dist/{chunk-A2PI6UMY.js.map → chunk-YSRBFJMQ.js.map} +1 -1
  114. package/dist/chunk-YVFRBXST.js +1420 -0
  115. package/dist/chunk-YVFRBXST.js.map +1 -0
  116. package/dist/{chunk-SCOTPE2I.js → chunk-YYLBXXTE.js} +3 -1
  117. package/dist/{chunk-SCOTPE2I.js.map → chunk-YYLBXXTE.js.map} +1 -1
  118. package/dist/{chunk-RHREZ7BO.js → chunk-ZUWF4NF7.js} +3 -1
  119. package/dist/{chunk-RHREZ7BO.js.map → chunk-ZUWF4NF7.js.map} +1 -1
  120. package/dist/clipboard.js +3 -2
  121. package/dist/components/composer-transcription-control.d.ts +7 -0
  122. package/dist/components/open-geni-chat.d.ts +6 -2
  123. package/dist/components/session-conversation.d.ts +31 -19
  124. package/dist/components/session-list.d.ts +4 -1
  125. package/dist/components/tool-action-review.d.ts +29 -2
  126. package/dist/components/tool-review-history.d.ts +8 -4
  127. package/dist/composer-voice-events.d.ts +8 -0
  128. package/dist/composer.js +15 -13
  129. package/dist/computer-viewer-46UIN7F3.js +20 -0
  130. package/dist/connect.js +15 -12
  131. package/dist/connect.js.map +1 -1
  132. package/dist/desktop.js +6 -5
  133. package/dist/desktop.js.map +1 -1
  134. package/dist/diffs.js +2 -1
  135. package/dist/diffs.js.map +1 -1
  136. package/dist/editor.js +4 -3
  137. package/dist/editor.js.map +1 -1
  138. package/dist/embedded-voice-NIL4RDX3.js +69 -0
  139. package/dist/embedded-voice-NIL4RDX3.js.map +1 -0
  140. package/dist/{fleet-decision-row-YZ3ZKWMM.js → fleet-decision-row-SI63O5V4.js} +3 -1
  141. package/dist/{fleet-decision-row-YZ3ZKWMM.js.map → fleet-decision-row-SI63O5V4.js.map} +1 -1
  142. package/dist/hooks/use-client-config-flags.d.ts +43 -0
  143. package/dist/hooks/use-realtime-voice-models.d.ts +9 -0
  144. package/dist/hooks/use-voice-input.d.ts +8 -1
  145. package/dist/index.d.ts +3 -3
  146. package/dist/index.js +60 -48
  147. package/dist/index.js.map +1 -1
  148. package/dist/interaction.js +14 -13
  149. package/dist/interaction.js.map +1 -1
  150. package/dist/{latest-question-YTOBHCWR.js → latest-question-2POFPYNB.js} +5 -4
  151. package/dist/{latest-question-YTOBHCWR.js.map → latest-question-2POFPYNB.js.map} +1 -1
  152. package/dist/machines.js +8 -7
  153. package/dist/{markdown-table-layout-QXSLK4BY.js → markdown-table-layout-DQ4DRNVT.js} +3 -1
  154. package/dist/{markdown-table-layout-QXSLK4BY.js.map → markdown-table-layout-DQ4DRNVT.js.map} +1 -1
  155. package/dist/model-policy-picker-menu-WRNSRFQC.js +15 -0
  156. package/dist/model-policy.js +2 -1
  157. package/dist/{noto-sans-arabic-loader-IA2T4X2A.js → noto-sans-arabic-loader-XSH2KYWJ.js} +3 -1
  158. package/dist/{noto-sans-arabic-loader-IA2T4X2A.js.map → noto-sans-arabic-loader-XSH2KYWJ.js.map} +1 -1
  159. package/dist/{noto-sans-jp-loader.generated-HFGLYBR3.js → noto-sans-jp-loader.generated-MR5L6V7K.js} +3 -1
  160. package/dist/{noto-sans-jp-loader.generated-HFGLYBR3.js.map → noto-sans-jp-loader.generated-MR5L6V7K.js.map} +1 -1
  161. package/dist/{platform-activity-row-627BFZ46.js → platform-activity-row-PJJVATTZ.js} +3 -1
  162. package/dist/{platform-activity-row-627BFZ46.js.map → platform-activity-row-PJJVATTZ.js.map} +1 -1
  163. package/dist/{queue-surface-implementation-ZHFI5JBP.js → queue-surface-implementation-67FS7FUI.js} +10 -9
  164. package/dist/{queue-surface-implementation-ZHFI5JBP.js.map → queue-surface-implementation-67FS7FUI.js.map} +1 -1
  165. package/dist/realtime/embedded-voice.d.ts +20 -0
  166. package/dist/realtime/realtime-control.d.ts +2 -0
  167. package/dist/realtime.js +22 -1332
  168. package/dist/realtime.js.map +1 -1
  169. package/dist/sandbox-files-34CJJNOY.js +21 -0
  170. package/dist/session-capability-policy.d.ts +20 -1
  171. package/dist/session-ui.js +32 -30
  172. package/dist/session-ui.js.map +1 -1
  173. package/dist/session.js +12 -11
  174. package/dist/sites.js +2 -1
  175. package/dist/terminal.js +6 -5
  176. package/dist/terminal.js.map +1 -1
  177. package/dist/timeline/index.d.ts +2 -0
  178. package/dist/timeline/retained-loaders.d.ts +27 -0
  179. package/dist/{timeline-annotation-markers-GNDHTGBD.js → timeline-annotation-markers-U3G66KUD.js} +5 -4
  180. package/dist/{timeline-annotation-markers-GNDHTGBD.js.map → timeline-annotation-markers-U3G66KUD.js.map} +1 -1
  181. package/dist/{timeline-annotation-selection-DMLMM2TX.js → timeline-annotation-selection-BI7QTPTZ.js} +4 -3
  182. package/dist/{timeline-annotation-selection-DMLMM2TX.js.map → timeline-annotation-selection-BI7QTPTZ.js.map} +1 -1
  183. package/dist/{timeline-annotations-dialog-ELRDBHTS.js → timeline-annotations-dialog-FOMHSDTE.js} +7 -6
  184. package/dist/{timeline-annotations-dialog-ELRDBHTS.js.map → timeline-annotations-dialog-FOMHSDTE.js.map} +1 -1
  185. package/dist/usage.js +6 -5
  186. package/dist/usage.js.map +1 -1
  187. package/dist/voice-recording-owner.d.ts +7 -0
  188. package/package.json +3 -3
  189. package/src/components/approval-surface.tsx +190 -79
  190. package/src/components/artifacts/session-artifact-viewer.tsx +4 -2
  191. package/src/components/composer-transcription-control.tsx +121 -25
  192. package/src/components/composer.tsx +1 -0
  193. package/src/components/markdown.tsx +7 -2
  194. package/src/components/message-timeline.tsx +10 -1
  195. package/src/components/open-geni-chat.tsx +53 -13
  196. package/src/components/session-conversation.tsx +273 -63
  197. package/src/components/session-list.tsx +9 -2
  198. package/src/components/session-proxy-scope.tsx +26 -5
  199. package/src/components/tool-action-review.tsx +502 -239
  200. package/src/components/tool-review-history.tsx +18 -43
  201. package/src/composer-voice-events.ts +12 -0
  202. package/src/connection-discovery.tsx +4 -1
  203. package/src/hooks/use-client-config-flags.ts +110 -0
  204. package/src/hooks/use-goal.ts +12 -1
  205. package/src/hooks/use-realtime-voice-models.ts +67 -0
  206. package/src/hooks/use-session-events.ts +19 -16
  207. package/src/hooks/use-transcription.ts +6 -0
  208. package/src/hooks/use-voice-input.ts +115 -18
  209. package/src/index.ts +9 -0
  210. package/src/realtime/embedded-voice.tsx +81 -0
  211. package/src/realtime/realtime-control.tsx +109 -17
  212. package/src/session-capability-policy.ts +119 -6
  213. package/src/timeline/index.ts +10 -0
  214. package/src/timeline/retained-loaders.ts +86 -0
  215. package/src/timeline/tool-display-name.ts +5 -1
  216. package/src/timeline/tool-renderers.tsx +143 -10
  217. package/src/voice-recording-owner.ts +22 -0
  218. package/styles/compiled.css +135 -37
  219. package/styles/index.css +49 -0
  220. package/styles/responsive.css +61 -4
  221. package/styles/session-capability.css +1 -1
  222. package/styles/tokens.css +1 -1
  223. package/dist/browser-viewer-SHYKQRNX.js +0 -16
  224. package/dist/chat-interactive-block-UGBTNB3L.js +0 -11
  225. package/dist/chunk-742CZ3L3.js.map +0 -1
  226. package/dist/chunk-CY7WK5FD.js.map +0 -1
  227. package/dist/chunk-I7IWRQLN.js.map +0 -1
  228. package/dist/chunk-IWIDV7SB.js +0 -1
  229. package/dist/chunk-PGSX7UCP.js.map +0 -1
  230. package/dist/chunk-TMYGL6X5.js.map +0 -1
  231. package/dist/chunk-WWEUO2PL.js.map +0 -1
  232. package/dist/computer-viewer-6SRGWRUX.js +0 -19
  233. package/dist/model-policy-picker-menu-DXHUL73H.js +0 -14
  234. package/dist/sandbox-files-GORZOUUJ.js +0 -20
  235. /package/dist/{browser-viewer-SHYKQRNX.js.map → browser-viewer-WGJ43YGI.js.map} +0 -0
  236. /package/dist/{chat-interactive-block-UGBTNB3L.js.map → chat-interactive-block-ZFHOKTMS.js.map} +0 -0
  237. /package/dist/{chunk-IWIDV7SB.js.map → chunk-M357MHH3.js.map} +0 -0
  238. /package/dist/{computer-viewer-6SRGWRUX.js.map → computer-viewer-46UIN7F3.js.map} +0 -0
  239. /package/dist/{model-policy-picker-menu-DXHUL73H.js.map → model-policy-picker-menu-WRNSRFQC.js.map} +0 -0
  240. /package/dist/{sandbox-files-GORZOUUJ.js.map → sandbox-files-34CJJNOY.js.map} +0 -0
@@ -1,36 +1,364 @@
1
- import { ArrowLeftIcon, CheckIcon, ChevronRightIcon, LoaderCircleIcon } from "lucide-react";
2
- import { useEffect, useId, useRef, useState } from "react";
1
+ import {
2
+ ArrowLeftIcon,
3
+ CheckIcon,
4
+ ChevronRightIcon,
5
+ CircleAlertIcon,
6
+ CircleSlashIcon,
7
+ LoaderCircleIcon,
8
+ ShieldCheckIcon,
9
+ } from "lucide-react";
10
+ import { useEffect, useId, useRef, useState, type HTMLAttributes, type ReactNode } from "react";
3
11
  import type { ToolActionReview, ToolReviewDetailsPage, ToolReviewStatus } from "@opengeni/sdk";
12
+ import { cn } from "../lib/cn";
4
13
 
5
14
  export type ToolReviewDetailsLoader = (
6
15
  review: ToolActionReview,
7
16
  path: string,
8
17
  offset: number,
9
18
  ) => Promise<ToolReviewDetailsPage>;
19
+
10
20
  const statusText: Record<ToolReviewStatus, string> = {
11
- pending: "Needs your approval",
12
- approved: "Approved · waiting to run",
13
- executing: "In progress",
14
- completed: "Completed",
15
- partial: "Partially completed",
16
- unknown: "Outcome uncertain",
21
+ pending: "Approval needed",
22
+ approved: "Approved",
23
+ executing: "Approved · running",
24
+ completed: "Approved · done",
25
+ partial: "Partly done",
26
+ unknown: "Outcome unknown",
17
27
  rejected: "Declined",
18
28
  cancelled: "Cancelled",
19
- expired: "Review expired",
29
+ expired: "Expired",
20
30
  revoked: "Access unavailable",
21
- stale: "Action changed",
22
- failed: "Action failed",
31
+ blocked: "Blocked",
32
+ stale: "No longer valid",
33
+ failed: "Failed",
23
34
  unavailable: "Details unavailable",
24
35
  };
25
36
  const statusDetail: Partial<Record<ToolReviewStatus, string>> = {
26
- unknown: "Execution may have happened. Check the result before trying again.",
27
- rejected: "This action was not run.",
28
- cancelled: "This action will not run.",
29
- expired: "Prepare a new action to continue.",
30
- revoked: "Restore access before preparing a new action.",
31
- stale: "The tool or account changed. A new review is needed.",
32
- partial: "Some work completed. Review the results before continuing.",
37
+ unknown: "It may have run. Check the result before trying again.",
38
+ rejected: "This action did not run.",
39
+ cancelled: "This action did not run.",
40
+ expired: "This action did not run. Ask the agent to prepare it again.",
41
+ revoked: "This action did not run. Reconnect the account, then ask the agent to try again.",
42
+ stale: "The tool or account changed after this was prepared, so it did not run.",
43
+ partial: "Some of it ran. Check the result before continuing.",
44
+ failed: "The action ran but returned an error.",
45
+ };
46
+ type Tone = "waiting" | "ok" | "failed" | "muted";
47
+ const statusTone: Record<ToolReviewStatus, Tone> = {
48
+ pending: "waiting",
49
+ approved: "ok",
50
+ executing: "ok",
51
+ completed: "ok",
52
+ partial: "failed",
53
+ unknown: "failed",
54
+ rejected: "muted",
55
+ cancelled: "muted",
56
+ expired: "muted",
57
+ revoked: "muted",
58
+ blocked: "muted",
59
+ stale: "muted",
60
+ failed: "failed",
61
+ unavailable: "muted",
62
+ };
63
+ const toneText: Record<Tone, string> = {
64
+ waiting: "text-og-status-waiting",
65
+ ok: "text-og-fg-muted",
66
+ failed: "text-og-status-failed",
67
+ muted: "text-og-fg-muted",
33
68
  };
69
+ /** Fields that only identify the selection; the count, examples and details cover them. */
70
+ const selectionPaths = new Set([
71
+ "/messageIds",
72
+ "/messageId",
73
+ "/threadId",
74
+ "/addLabelIds",
75
+ "/removeLabelIds",
76
+ "/labelIds",
77
+ ]);
78
+ /** Shown when an older request's saved values are gone: approval is impossible, decline is not. */
79
+ export const UNRECOVERABLE_TEXT =
80
+ "The details of this older request couldn't be recovered, so it can't be approved. Decline it to let the agent continue.";
81
+ /** Long argument lists stay readable; the full set is one click away in details. */
82
+ const VISIBLE_FIELDS = 6;
83
+
84
+ function StatusIcon({ status, busy }: { status: ToolReviewStatus; busy?: boolean }) {
85
+ const className = "size-4";
86
+ if (busy || status === "executing" || status === "approved")
87
+ return <LoaderCircleIcon aria-hidden="true" className={cn(className, "animate-spin")} />;
88
+ if (status === "pending") return <ShieldCheckIcon aria-hidden="true" className={className} />;
89
+ if (status === "completed") return <CheckIcon aria-hidden="true" className={className} />;
90
+ if (statusTone[status] === "failed")
91
+ return <CircleAlertIcon aria-hidden="true" className={className} />;
92
+ return <CircleSlashIcon aria-hidden="true" className={className} />;
93
+ }
94
+
95
+ /** Card chrome shared by the review, its loading state and its load failure. */
96
+ export function ToolReviewFrame({
97
+ tone,
98
+ icon,
99
+ eyebrow,
100
+ title,
101
+ titleId,
102
+ subtitle,
103
+ children,
104
+ footer,
105
+ bare = false,
106
+ ...rest
107
+ }: {
108
+ tone: Tone;
109
+ icon: ReactNode;
110
+ eyebrow: ReactNode;
111
+ title: ReactNode;
112
+ titleId?: string | undefined;
113
+ subtitle?: ReactNode;
114
+ children?: ReactNode;
115
+ footer?: ReactNode;
116
+ bare?: boolean | undefined;
117
+ } & Omit<HTMLAttributes<HTMLElement>, "title">) {
118
+ const waiting = tone === "waiting";
119
+ return (
120
+ <article
121
+ aria-labelledby={titleId}
122
+ {...rest}
123
+ className={cn(
124
+ "min-w-0 text-og-fg",
125
+ !bare &&
126
+ (waiting
127
+ ? "overflow-hidden rounded-og-lg border border-og-status-waiting/35 bg-og-status-waiting/5 shadow-og-sm"
128
+ : "overflow-hidden rounded-og-lg border border-og-border bg-og-surface-1"),
129
+ rest.className,
130
+ )}
131
+ >
132
+ <header className={cn("flex min-w-0 items-start gap-3", bare ? "pb-1" : "px-4 py-3")}>
133
+ <span
134
+ className={cn(
135
+ "mt-0.5 inline-flex size-8 shrink-0 items-center justify-center rounded-og-md",
136
+ waiting
137
+ ? "bg-og-status-waiting/12 text-og-status-waiting"
138
+ : tone === "failed"
139
+ ? "bg-og-status-failed/10 text-og-status-failed"
140
+ : "bg-og-fg/5 text-og-fg-muted",
141
+ )}
142
+ >
143
+ {icon}
144
+ </span>
145
+ <div className="min-w-0 flex-1">
146
+ <p
147
+ className={cn("m-0 text-og-xs font-medium", toneText[tone])}
148
+ role="status"
149
+ aria-live="polite"
150
+ >
151
+ {eyebrow}
152
+ </p>
153
+ <h3
154
+ tabIndex={-1}
155
+ id={titleId}
156
+ className="m-0 mt-0.5 break-words text-og-md font-semibold leading-snug text-og-fg outline-hidden [overflow-wrap:anywhere]"
157
+ >
158
+ {title}
159
+ </h3>
160
+ {subtitle ? (
161
+ <p className="m-0 mt-0.5 break-words text-og-xs text-og-fg-muted [overflow-wrap:anywhere]">
162
+ {subtitle}
163
+ </p>
164
+ ) : null}
165
+ </div>
166
+ </header>
167
+ {children ? (
168
+ <div
169
+ className={cn(
170
+ "min-w-0",
171
+ bare ? "pt-2" : "px-4 pb-3",
172
+ !bare && "sm:pl-[3.75rem]", // Aligns with the title, past the icon.
173
+ )}
174
+ >
175
+ {children}
176
+ </div>
177
+ ) : null}
178
+ {footer ? (
179
+ <footer
180
+ className={cn(
181
+ "flex flex-wrap items-center gap-x-3 gap-y-2",
182
+ bare
183
+ ? "pt-3"
184
+ : waiting
185
+ ? "border-t border-og-status-waiting/20 px-4 py-3"
186
+ : "border-t border-og-border px-4 py-3",
187
+ )}
188
+ >
189
+ {footer}
190
+ </footer>
191
+ ) : null}
192
+ </article>
193
+ );
194
+ }
195
+
196
+ const linkButton =
197
+ "focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-og-accent/40 inline-flex min-h-8 items-center gap-1 rounded-og-sm text-left text-og-sm text-og-fg underline decoration-og-border-strong underline-offset-4 hover:decoration-current [@media(pointer:coarse)]:min-h-11";
198
+
199
+ /** The facts of one saved action: consequences, selection and its key values. */
200
+ function ReviewFacts({
201
+ review,
202
+ onViewDetails,
203
+ showStatusDetail,
204
+ }: {
205
+ review: ToolActionReview;
206
+ onViewDetails?: ((path: string) => void) | undefined;
207
+ showStatusDetail: boolean;
208
+ }) {
209
+ const canOpen = Boolean(onViewDetails && review.detailsAvailable);
210
+ const selection =
211
+ review.selectionCount === undefined
212
+ ? undefined
213
+ : review.fields.find((field) =>
214
+ ["/messageIds", "/messageId", "/threadId"].includes(field.path),
215
+ );
216
+ const fields = selection
217
+ ? review.fields.filter((field) => !selectionPaths.has(field.path))
218
+ : review.fields;
219
+ const shown = fields.slice(0, VISIBLE_FIELDS);
220
+ const hiddenFields = fields.length - shown.length + review.moreFields;
221
+ // The title already names the main effect; list only the remaining ones.
222
+ const effects = review.effects.filter(
223
+ (effect) => effect !== review.approveLabel && !review.title.startsWith(effect),
224
+ );
225
+ const note =
226
+ (showStatusDetail
227
+ ? review.status === "blocked"
228
+ ? `${review.reason || "Your permission settings block this action."} It did not run.`
229
+ : statusDetail[review.status]
230
+ : undefined) ?? review.consequence;
231
+ const samples = review.samples ?? [];
232
+ const count = review.selectionCount ?? 0;
233
+ const noun =
234
+ review.selectionKind === "threads"
235
+ ? count === 1
236
+ ? "conversation"
237
+ : "conversations"
238
+ : count === 1
239
+ ? "message"
240
+ : "messages";
241
+ const hasContent =
242
+ note || effects.length || selection || shown.length || hiddenFields > 0 || samples.length;
243
+ if (!hasContent) return null;
244
+ return (
245
+ <div className="flex min-w-0 flex-col gap-3 text-og-sm">
246
+ {note ? <p className="m-0 leading-relaxed text-og-fg-muted">{note}</p> : null}
247
+ {effects.length > 0 ? (
248
+ <ul className="m-0 list-none space-y-1 p-0" aria-label="Also changes">
249
+ {effects.map((effect) => (
250
+ <li key={effect} className="flex items-start gap-2 text-og-fg">
251
+ <span
252
+ aria-hidden="true"
253
+ className="mt-[0.55em] size-1 shrink-0 rounded-full bg-og-fg-muted"
254
+ />
255
+ <span className="min-w-0 break-words">{effect}</span>
256
+ </li>
257
+ ))}
258
+ </ul>
259
+ ) : null}
260
+ {selection ? (
261
+ <div className="min-w-0">
262
+ {samples.length > 0 ? (
263
+ <>
264
+ <p className="m-0 mb-1 text-og-xs text-og-fg-muted">Includes</p>
265
+ <ul className="m-0 list-none divide-y divide-og-border rounded-og-md border border-og-border bg-og-surface-1 p-0">
266
+ {samples.map((sample) => (
267
+ <li key={sample.id} className="min-w-0 px-3 py-2">
268
+ <p className="m-0 truncate text-og-sm text-og-fg">{sample.title}</p>
269
+ {sample.subtitle ? (
270
+ <p className="m-0 mt-0.5 truncate text-og-xs text-og-fg-muted">
271
+ {sample.subtitle}
272
+ </p>
273
+ ) : null}
274
+ </li>
275
+ ))}
276
+ {count > samples.length ? (
277
+ <li className="px-3 py-2 text-og-xs text-og-fg-muted">
278
+ and {(count - samples.length).toLocaleString()} more
279
+ </li>
280
+ ) : null}
281
+ </ul>
282
+ </>
283
+ ) : null}
284
+ {canOpen && count > 0 ? (
285
+ <button
286
+ type="button"
287
+ className={cn(linkButton, samples.length > 0 && "mt-1")}
288
+ data-review-path={selection.path}
289
+ onClick={() => onViewDetails!(selection.path)}
290
+ >
291
+ {count === 1 ? `View the ${noun}` : `View all ${count.toLocaleString()} ${noun}`}
292
+ <ChevronRightIcon className="size-3.5 shrink-0" aria-hidden="true" />
293
+ </button>
294
+ ) : null}
295
+ </div>
296
+ ) : null}
297
+ {shown.length > 0 ? (
298
+ <dl className="m-0 min-w-0 space-y-2">
299
+ {shown.map((field) => (
300
+ <div
301
+ key={field.path}
302
+ className="grid min-w-0 gap-x-4 gap-y-0.5 sm:grid-cols-[minmax(0,9rem)_minmax(0,1fr)]"
303
+ >
304
+ <dt className="m-0 break-words text-og-xs text-og-fg-muted sm:pt-px sm:text-og-sm">
305
+ {field.label}
306
+ </dt>
307
+ <dd className="m-0 min-w-0 whitespace-pre-wrap break-words text-og-fg [overflow-wrap:anywhere]">
308
+ {field.protected ? (
309
+ <span className="text-og-fg-muted">Hidden</span>
310
+ ) : field.truncated && canOpen ? (
311
+ <button
312
+ type="button"
313
+ data-review-path={field.path}
314
+ aria-label={`View ${field.label}: ${field.preview}`}
315
+ onClick={() => onViewDetails!(field.path)}
316
+ className={cn(linkButton, "max-w-full")}
317
+ >
318
+ <span className="line-clamp-4 min-w-0 break-words [overflow-wrap:anywhere]">
319
+ {field.preview}
320
+ </span>
321
+ <ChevronRightIcon className="size-3.5 shrink-0" aria-hidden="true" />
322
+ </button>
323
+ ) : (
324
+ field.preview
325
+ )}
326
+ </dd>
327
+ </div>
328
+ ))}
329
+ </dl>
330
+ ) : null}
331
+ {hiddenFields > 0 ? (
332
+ <p className="m-0 text-og-xs text-og-fg-muted">
333
+ {hiddenFields === 1 ? "1 more value" : `${hiddenFields} more values`} in details
334
+ </p>
335
+ ) : null}
336
+ </div>
337
+ );
338
+ }
339
+
340
+ export function DeclineButton({
341
+ busy,
342
+ submitting,
343
+ label,
344
+ onClick,
345
+ }: {
346
+ busy: boolean;
347
+ submitting: boolean;
348
+ label?: string | undefined;
349
+ onClick: () => void;
350
+ }) {
351
+ return (
352
+ <button
353
+ type="button"
354
+ disabled={busy}
355
+ onClick={onClick}
356
+ className="inline-flex min-h-9 items-center justify-center rounded-og-md border border-og-border bg-og-surface-1 px-3 py-1.5 text-og-sm font-medium text-og-fg transition-colors hover:border-og-border-strong hover:bg-og-hover disabled:opacity-60 [@media(pointer:coarse)]:min-h-11 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-og-accent/40"
357
+ >
358
+ {submitting ? "Declining…" : (label ?? "Decline")}
359
+ </button>
360
+ );
361
+ }
34
362
 
35
363
  /** One host-neutral review. All callbacks refer to the immutable action; none can edit it. */
36
364
  export function ToolActionReviewCard({
@@ -42,6 +370,7 @@ export function ToolActionReviewCard({
42
370
  onViewDetails,
43
371
  pendingLabel,
44
372
  rejectLabel,
373
+ bare,
45
374
  }: {
46
375
  review: ToolActionReview;
47
376
  onApprove?: (() => void) | undefined;
@@ -51,183 +380,94 @@ export function ToolActionReviewCard({
51
380
  onViewDetails?: ((path: string) => void) | undefined;
52
381
  pendingLabel?: string | undefined;
53
382
  rejectLabel?: string | undefined;
383
+ /** Render without card chrome, e.g. inside an expanded timeline row. */
384
+ bare?: boolean | undefined;
54
385
  }) {
55
386
  const titleId = useId();
56
- const actionable = review.status === "pending" && review.availableActions.includes("approve");
57
- const quiet =
58
- review.status === "completed" || review.status === "rejected" || review.status === "cancelled";
387
+ const canApprove = review.availableActions.includes("approve") && !!onApprove;
388
+ const canReject = review.availableActions.includes("reject") && !!onReject;
389
+ const actionable = review.status === "pending" && (canApprove || canReject);
59
390
  const busy = Boolean(disabled || submitting);
60
- const selection =
61
- review.selectionCount === undefined
62
- ? undefined
63
- : review.fields.find(
64
- (field) =>
65
- field.path === "/messageIds" ||
66
- field.path === "/messageId" ||
67
- field.path === "/threadId",
68
- );
69
- const fields = selection
70
- ? review.fields.filter(
71
- (field) =>
72
- ![selection.path, "/addLabelIds", "/removeLabelIds", "/labelIds"].includes(field.path),
73
- )
74
- : review.fields;
75
- const extraEffects = review.effects.filter((effect) => effect !== review.approveLabel);
391
+ const tone: Tone = statusTone[review.status];
392
+ const eyebrow = submitting
393
+ ? submitting === "approve"
394
+ ? "Approving…"
395
+ : "Declining…"
396
+ : review.status === "pending"
397
+ ? (pendingLabel ?? statusText.pending)
398
+ : statusText[review.status];
399
+ const detailsLink =
400
+ review.detailsAvailable && onViewDetails ? (
401
+ <button
402
+ type="button"
403
+ className="inline-flex min-h-8 items-center rounded-og-sm text-og-sm text-og-fg-muted underline decoration-og-border-strong underline-offset-4 hover:text-og-fg hover:decoration-current [@media(pointer:coarse)]:min-h-11 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-og-accent/40"
404
+ data-review-path=""
405
+ onClick={() => onViewDetails("")}
406
+ >
407
+ Details
408
+ </button>
409
+ ) : null;
410
+ const reason = review.reason.trim();
76
411
  return (
77
- <article
78
- aria-labelledby={titleId}
412
+ <ToolReviewFrame
413
+ tone={tone}
414
+ icon={<StatusIcon status={review.status} busy={Boolean(submitting)} />}
415
+ eyebrow={eyebrow}
416
+ title={review.title}
417
+ titleId={titleId}
418
+ subtitle={review.accountLabel}
419
+ bare={bare}
79
420
  aria-busy={Boolean(submitting)}
80
- className="min-w-0 py-4"
81
421
  data-og-tool-review=""
82
422
  data-review-status={review.status}
83
- >
84
- <div className="flex min-w-0 items-center gap-2 text-og-xs text-og-fg-muted" role="status">
85
- {review.status === "completed" ? (
86
- <CheckIcon className="size-3.5" aria-hidden="true" />
87
- ) : (
88
- <span className="size-1.5 shrink-0 rounded-full bg-current" aria-hidden="true" />
89
- )}
90
- <span>
91
- {submitting
92
- ? submitting === "approve"
93
- ? "Approving…"
94
- : "Declining…"
95
- : review.status === "pending"
96
- ? (pendingLabel ?? statusText.pending)
97
- : statusText[review.status]}
98
- </span>
99
- </div>
100
- <h3
101
- tabIndex={-1}
102
- id={titleId}
103
- className="m-0 mt-1.5 outline-hidden break-words text-og-md font-semibold leading-snug text-og-fg"
104
- >
105
- {review.title}
106
- </h3>
107
- {review.accountLabel && (
108
- <p className="m-0 mt-1 break-words text-og-sm text-og-fg-muted">{review.accountLabel}</p>
109
- )}
110
- {(statusDetail[review.status] || review.consequence) && (
111
- <p className="m-0 mt-3 text-og-sm leading-relaxed text-og-fg-muted">
112
- {statusDetail[review.status] ?? review.consequence}
113
- </p>
114
- )}
115
- {!quiet && (
116
- <>
117
- {extraEffects.length > 0 && (
118
- <ul className="m-0 mt-3 list-none space-y-1 p-0 text-og-sm text-og-fg">
119
- {extraEffects.map((effect) => (
120
- <li key={effect}>{effect}</li>
121
- ))}
122
- </ul>
123
- )}
124
- {selection && (
125
- <div className="mt-4">
126
- {review.samples && review.samples.length > 0 && (
127
- <>
128
- <p className="m-0 mb-1 text-og-xs text-og-fg-muted">
129
- Examples from this selection
130
- </p>
131
- <ul className="m-0 list-none divide-y divide-og-border p-0">
132
- {review.samples.map((sample) => (
133
- <li key={sample.id} className="py-2">
134
- <p className="m-0 break-words text-og-sm text-og-fg">{sample.title}</p>
135
- {sample.subtitle && (
136
- <p className="m-0 mt-0.5 break-words text-og-xs text-og-fg-muted">
137
- {sample.subtitle}
138
- </p>
139
- )}
140
- </li>
141
- ))}
142
- </ul>
143
- </>
144
- )}
145
- {onViewDetails && review.detailsAvailable && (
423
+ footer={
424
+ actionable ? (
425
+ <>
426
+ <div className="flex min-w-0 flex-1 basis-48 flex-wrap items-center gap-x-3 gap-y-1">
427
+ {detailsLink}
428
+ {reason ? (
429
+ <span className="min-w-0 text-og-xs leading-relaxed text-og-fg-subtle">
430
+ {reason}
431
+ </span>
432
+ ) : null}
433
+ </div>
434
+ <div className="ml-auto flex shrink-0 flex-wrap items-center justify-end gap-2">
435
+ {canReject ? (
436
+ <DeclineButton
437
+ busy={busy}
438
+ submitting={submitting === "reject"}
439
+ label={rejectLabel}
440
+ onClick={onReject!}
441
+ />
442
+ ) : null}
443
+ {canApprove ? (
146
444
  <button
147
445
  type="button"
148
- className="inline-flex min-h-11 items-center gap-1 text-og-sm text-og-fg underline decoration-og-border-strong underline-offset-4 hover:decoration-current"
149
- data-review-path={selection.path}
150
- onClick={() => onViewDetails(selection.path)}
446
+ disabled={busy}
447
+ onClick={onApprove}
448
+ className="inline-flex min-h-9 items-center justify-center gap-2 rounded-og-md border border-og-primary-border bg-og-primary px-3.5 py-1.5 text-og-sm font-medium text-og-primary-fg transition-colors hover:bg-og-primary-hover disabled:opacity-60 [@media(pointer:coarse)]:min-h-11 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-og-accent/40"
151
449
  >
152
- View all {review.selectionCount?.toLocaleString()}{" "}
153
- {review.selectionKind ?? "messages"}
154
- <ChevronRightIcon className="size-3.5" aria-hidden="true" />
450
+ {submitting === "approve" ? (
451
+ <LoaderCircleIcon className="size-3.5 animate-spin" aria-hidden="true" />
452
+ ) : null}
453
+ {submitting === "approve" ? "Approving…" : review.approveLabel}
155
454
  </button>
156
- )}
455
+ ) : null}
157
456
  </div>
158
- )}
159
- <dl className="m-0 mt-3 divide-y divide-og-border text-og-sm">
160
- {fields.map((field) => (
161
- <div
162
- key={field.path}
163
- className="grid min-w-0 grid-cols-[minmax(0,1fr)_minmax(0,2fr)] gap-x-4 py-2"
164
- >
165
- <dt className="m-0 break-words text-og-fg-muted">{field.label}</dt>
166
- <dd className="m-0 min-w-0 whitespace-pre-wrap break-words text-og-fg [overflow-wrap:anywhere]">
167
- {field.truncated &&
168
- !field.protected &&
169
- onViewDetails &&
170
- review.detailsAvailable ? (
171
- <button
172
- type="button"
173
- data-review-path={field.path}
174
- onClick={() => onViewDetails(field.path)}
175
- className="inline-flex min-h-8 max-w-full items-center gap-1 text-left underline decoration-og-border-strong underline-offset-4 hover:decoration-current [@media(pointer:coarse)]:min-h-11"
176
- >
177
- <span className="min-w-0 break-words">{field.preview}</span>
178
- <ChevronRightIcon className="size-3.5 shrink-0" aria-hidden="true" />
179
- </button>
180
- ) : (
181
- field.preview
182
- )}
183
- </dd>
184
- </div>
185
- ))}
186
- </dl>
187
- {review.moreFields > 0 && (
188
- <p className="m-0 mt-2 text-og-xs text-og-fg-muted">
189
- {review.moreFields} more fields in the full details
190
- </p>
191
- )}
192
- </>
193
- )}
194
- {review.detailsAvailable && onViewDetails && (
195
- <button
196
- type="button"
197
- className="mt-1 min-h-8 text-og-xs text-og-fg-muted underline underline-offset-4 hover:text-og-fg [@media(pointer:coarse)]:min-h-11"
198
- data-review-path=""
199
- onClick={() => onViewDetails("")}
200
- >
201
- View full details
202
- </button>
203
- )}
204
- {actionable && (
205
- <>
206
- <p className="m-0 mt-3 text-og-xs leading-relaxed text-og-fg-muted">{review.reason}</p>
207
- <div className="mt-4 flex flex-wrap items-center gap-2">
208
- <button
209
- type="button"
210
- disabled={busy}
211
- onClick={onApprove}
212
- className="inline-flex min-h-9 items-center justify-center gap-2 rounded-og-md border border-og-primary-border bg-og-primary px-4 py-2 text-og-sm font-medium text-og-primary-fg transition-colors hover:bg-og-primary-hover disabled:opacity-60 [@media(pointer:coarse)]:min-h-11"
213
- >
214
- {submitting === "approve" && (
215
- <LoaderCircleIcon className="size-3.5 animate-spin" aria-hidden="true" />
216
- )}
217
- {submitting === "approve" ? "Approving…" : review.approveLabel}
218
- </button>
219
- <button
220
- type="button"
221
- disabled={busy}
222
- onClick={onReject}
223
- className="min-h-9 rounded-og-md px-3 py-2 text-og-sm text-og-fg-muted transition-colors hover:bg-og-fg/5 hover:text-og-fg disabled:opacity-60 [@media(pointer:coarse)]:min-h-11"
224
- >
225
- {submitting === "reject" ? "Declining…" : (rejectLabel ?? "Decline")}
226
- </button>
227
- </div>
228
- </>
229
- )}
230
- </article>
457
+ </>
458
+ ) : undefined
459
+ }
460
+ >
461
+ {actionable && !canApprove ? (
462
+ <p className="m-0 mb-3 text-og-sm leading-relaxed text-og-fg">{UNRECOVERABLE_TEXT}</p>
463
+ ) : null}
464
+ <ReviewFacts
465
+ review={review}
466
+ onViewDetails={onViewDetails}
467
+ showStatusDetail={review.status !== "pending"}
468
+ />
469
+ {!actionable && detailsLink ? <div className="mt-2">{detailsLink}</div> : null}
470
+ </ToolReviewFrame>
231
471
  );
232
472
  }
233
473
 
@@ -237,24 +477,30 @@ export function ToolActionReviewDetails({
237
477
  path: initialPath = "",
238
478
  load,
239
479
  onBack,
480
+ manageFocus = true,
240
481
  }: {
241
482
  review: ToolActionReview;
242
483
  path?: string;
243
484
  load: ToolReviewDetailsLoader;
244
485
  onBack: () => void;
486
+ /** Hosts rendering simultaneous previews can retain their own focus. */
487
+ manageFocus?: boolean;
245
488
  }) {
246
489
  const [path, setPath] = useState(initialPath);
490
+ const [trail, setTrail] = useState<Array<{ path: string; label: string }>>([]);
247
491
  const [offset, setOffset] = useState(0);
248
492
  const [page, setPage] = useState<ToolReviewDetailsPage | null>(null);
249
493
  const [error, setError] = useState(false);
250
494
  const [retry, setRetry] = useState(0);
251
495
  const heading = useRef<HTMLHeadingElement>(null);
496
+ const focusedOnce = useRef(false);
252
497
  useEffect(() => {
253
- if (page) {
498
+ if (page && manageFocus && !focusedOnce.current) {
499
+ focusedOnce.current = true;
254
500
  heading.current?.focus({ preventScroll: true });
255
501
  heading.current?.scrollIntoView({ block: "start" });
256
502
  }
257
- }, [page]);
503
+ }, [page, manageFocus]);
258
504
  useEffect(() => {
259
505
  let active = true;
260
506
  setPage(null);
@@ -280,12 +526,26 @@ export function ToolActionReviewDetails({
280
526
  active = false;
281
527
  };
282
528
  }, [review, path, offset, load, retry]);
529
+ const open = (next: string, label: string) => {
530
+ setTrail((value) => [...value, { path, label }]);
531
+ setPath(next);
532
+ setOffset(0);
533
+ };
534
+ const up = () => {
535
+ const previous = trail.at(-1);
536
+ setTrail((value) => value.slice(0, -1));
537
+ setPath(previous?.path ?? initialPath);
538
+ setOffset(0);
539
+ };
283
540
  return (
284
- <section className="og-root mx-auto w-full max-w-3xl p-6 text-og-fg" data-og-review-details="">
541
+ <section
542
+ className="og-root mx-auto w-full max-w-3xl px-4 py-5 text-og-fg sm:px-6"
543
+ data-og-review-details=""
544
+ >
285
545
  <button
286
546
  type="button"
287
547
  onClick={onBack}
288
- className="mb-5 inline-flex min-h-11 items-center gap-2 text-og-sm text-og-fg-muted hover:text-og-fg"
548
+ className="mb-4 inline-flex min-h-11 items-center gap-2 rounded-og-md pr-2 text-og-sm text-og-fg-muted hover:text-og-fg focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-og-accent/40"
289
549
  >
290
550
  <ArrowLeftIcon className="size-4" aria-hidden="true" />
291
551
  Back to review
@@ -293,92 +553,95 @@ export function ToolActionReviewDetails({
293
553
  <h2
294
554
  ref={heading}
295
555
  tabIndex={-1}
296
- className="m-0 break-words text-og-lg font-semibold outline-hidden"
556
+ className="m-0 break-words text-og-lg font-semibold outline-hidden [overflow-wrap:anywhere]"
297
557
  >
298
558
  {review.title}
299
559
  </h2>
300
- {review.accountLabel && <p className="text-og-sm text-og-fg-muted">{review.accountLabel}</p>}
301
- <p className="text-og-sm text-og-fg-muted">
302
- Exact saved selection and values. Protected credentials stay hidden.
560
+ {review.accountLabel ? (
561
+ <p className="m-0 mt-1 text-og-sm text-og-fg-muted">{review.accountLabel}</p>
562
+ ) : null}
563
+ <p className="m-0 mt-2 text-og-xs text-og-fg-subtle">
564
+ The exact values saved for this action. Passwords and keys stay hidden.
303
565
  </p>
304
- {path !== initialPath && (
566
+ {trail.length > 0 ? (
305
567
  <button
306
568
  type="button"
307
- className="min-h-11 text-og-sm underline"
308
- onClick={() => {
309
- setPath(initialPath);
310
- setOffset(0);
311
- }}
569
+ className="mt-3 inline-flex min-h-11 items-center gap-1.5 text-og-sm text-og-fg-muted underline underline-offset-4 hover:text-og-fg focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-og-accent/40"
570
+ onClick={up}
312
571
  >
313
- Back to all fields
572
+ <ArrowLeftIcon className="size-3.5" aria-hidden="true" />
573
+ Back to {trail.at(-1)?.label ?? "all values"}
314
574
  </button>
315
- )}
575
+ ) : null}
316
576
  {error ? (
317
- <div role="alert" className="py-6 text-og-sm">
318
- Details could not be loaded.{" "}
577
+ <div role="alert" className="mt-4 py-4 text-og-sm">
578
+ Couldn't load these details.{" "}
319
579
  <button
320
580
  type="button"
321
581
  onClick={() => setRetry((value) => value + 1)}
322
- className="min-h-11 underline"
582
+ className="min-h-11 underline underline-offset-4 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-og-accent/40"
323
583
  >
324
584
  Try again
325
585
  </button>
326
586
  </div>
327
587
  ) : !page ? (
328
- <p role="status" className="py-6 text-og-sm text-og-fg-muted">
588
+ <p role="status" className="mt-4 py-4 text-og-sm text-og-fg-muted">
329
589
  Loading details…
330
590
  </p>
331
591
  ) : (
332
592
  <>
333
- <dl className="m-0 divide-y divide-og-border">
593
+ <dl className="m-0 mt-4 divide-y divide-og-border border-y border-og-border">
334
594
  {page.items.map((item) => (
335
- <div key={`${page.path}:${item.label}`} className="py-4">
595
+ <div key={`${page.path}:${item.label}`} className="py-3">
336
596
  <dt className="m-0 text-og-xs text-og-fg-muted">{item.label}</dt>
337
597
  <dd className="m-0 mt-1 whitespace-pre-wrap break-words text-og-sm leading-relaxed [overflow-wrap:anywhere]">
338
- {item.value}
339
- {item.truncated && item.path && (
598
+ {item.truncated && item.path ? (
340
599
  <button
341
600
  type="button"
342
- aria-label={`Open complete ${item.label}`}
343
- className="mt-2 block min-h-11 text-og-sm underline underline-offset-4"
344
- onClick={() => {
345
- setPath(item.path!);
346
- setOffset(0);
347
- }}
601
+ aria-label={`Open ${item.label}`}
602
+ className={cn(linkButton, "max-w-full")}
603
+ onClick={() => open(item.path!, item.label)}
348
604
  >
349
- Open complete field
605
+ <span className="min-w-0 break-words focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-og-accent/40">
606
+ {item.value}
607
+ </span>
608
+ <ChevronRightIcon className="size-3.5 shrink-0" aria-hidden="true" />
350
609
  </button>
610
+ ) : (
611
+ item.value
351
612
  )}
352
613
  </dd>
353
614
  </div>
354
615
  ))}
355
616
  </dl>
356
- <nav
357
- aria-label="Review detail pages"
358
- className="mt-6 flex flex-wrap items-center justify-between gap-3 border-t border-og-border pt-4 text-og-sm"
359
- >
360
- <button
361
- type="button"
362
- disabled={offset === 0}
363
- onClick={() => setOffset(Math.max(0, offset - 25))}
364
- className="min-h-11 rounded-og-md border border-og-border px-3 disabled:opacity-40"
365
- >
366
- Previous
367
- </button>
368
- <span className="text-og-fg-muted">
369
- {page.total === 0
370
- ? "No items"
371
- : `${offset + 1}–${offset + page.items.length} of ${page.total.toLocaleString()}`}
372
- </span>
373
- <button
374
- type="button"
375
- disabled={page.nextOffset === null}
376
- onClick={() => setOffset(page.nextOffset ?? offset)}
377
- className="min-h-11 rounded-og-md border border-og-border px-3 disabled:opacity-40"
617
+ {page.total > page.items.length || offset > 0 ? (
618
+ <nav
619
+ aria-label="Detail pages"
620
+ className="mt-4 flex flex-wrap items-center justify-between gap-3 text-og-sm"
378
621
  >
379
- Next
380
- </button>
381
- </nav>
622
+ <button
623
+ type="button"
624
+ disabled={offset === 0}
625
+ onClick={() => setOffset(Math.max(0, offset - 25))}
626
+ className="min-h-11 rounded-og-md border border-og-border px-3 hover:bg-og-hover disabled:opacity-40 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-og-accent/40"
627
+ >
628
+ Previous
629
+ </button>
630
+ <span className="text-og-fg-muted tabular-nums">
631
+ {page.total === 0
632
+ ? "No items"
633
+ : `${(offset + 1).toLocaleString()}–${(offset + page.items.length).toLocaleString()} of ${page.total.toLocaleString()}`}
634
+ </span>
635
+ <button
636
+ type="button"
637
+ disabled={page.nextOffset === null}
638
+ onClick={() => setOffset(page.nextOffset ?? offset)}
639
+ className="min-h-11 rounded-og-md border border-og-border px-3 hover:bg-og-hover disabled:opacity-40 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-og-accent/40"
640
+ >
641
+ Next
642
+ </button>
643
+ </nav>
644
+ ) : null}
382
645
  </>
383
646
  )}
384
647
  </section>