@agent-native/toolkit 0.201.1 → 0.202.0-nightly-20261006033736

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 (229) hide show
  1. package/agent-native.eject.json +39 -12
  2. package/dist/app/agentkit/react/components.d.ts.map +1 -1
  3. package/dist/app/agentkit/react/components.js +292 -23
  4. package/dist/app/agentkit/react/components.js.map +1 -1
  5. package/dist/app/agentkit/react/context.d.ts +5 -0
  6. package/dist/app/agentkit/react/context.d.ts.map +1 -1
  7. package/dist/app/agentkit/react/context.js +5 -0
  8. package/dist/app/agentkit/react/context.js.map +1 -1
  9. package/dist/app/agentkit/react/styles.css +78 -3
  10. package/dist/app/auth/AuthPage.d.ts.map +1 -1
  11. package/dist/app/auth/AuthPage.js +7 -14
  12. package/dist/app/auth/AuthPage.js.map +1 -1
  13. package/dist/app/auth/server.d.ts +6 -0
  14. package/dist/app/auth/server.d.ts.map +1 -0
  15. package/dist/app/auth/server.js +19 -0
  16. package/dist/app/auth/server.js.map +1 -0
  17. package/dist/app/chat/AgentKitAssistantChat.d.ts.map +1 -1
  18. package/dist/app/chat/AgentKitAssistantChat.js +164 -83
  19. package/dist/app/chat/AgentKitAssistantChat.js.map +1 -1
  20. package/dist/app/chat/AgentSidebar.d.ts +2 -1
  21. package/dist/app/chat/AgentSidebar.d.ts.map +1 -1
  22. package/dist/app/chat/AgentSidebar.js +2 -2
  23. package/dist/app/chat/AgentSidebar.js.map +1 -1
  24. package/dist/app/chat/agentkit-chat/guided-questions.d.ts.map +1 -1
  25. package/dist/app/chat/agentkit-chat/guided-questions.js +13 -3
  26. package/dist/app/chat/agentkit-chat/guided-questions.js.map +1 -1
  27. package/dist/app/chat/chat/surface-types.d.ts +2 -0
  28. package/dist/app/chat/chat/surface-types.d.ts.map +1 -1
  29. package/dist/app/i18n/catalogs/ar-SA.d.ts.map +1 -1
  30. package/dist/app/i18n/catalogs/ar-SA.js +41 -0
  31. package/dist/app/i18n/catalogs/ar-SA.js.map +1 -1
  32. package/dist/app/i18n/catalogs/de-DE.d.ts.map +1 -1
  33. package/dist/app/i18n/catalogs/de-DE.js +41 -0
  34. package/dist/app/i18n/catalogs/de-DE.js.map +1 -1
  35. package/dist/app/i18n/catalogs/en-US.d.ts +41 -0
  36. package/dist/app/i18n/catalogs/en-US.d.ts.map +1 -1
  37. package/dist/app/i18n/catalogs/en-US.js +41 -0
  38. package/dist/app/i18n/catalogs/en-US.js.map +1 -1
  39. package/dist/app/i18n/catalogs/es-ES.d.ts.map +1 -1
  40. package/dist/app/i18n/catalogs/es-ES.js +41 -0
  41. package/dist/app/i18n/catalogs/es-ES.js.map +1 -1
  42. package/dist/app/i18n/catalogs/fr-FR.d.ts.map +1 -1
  43. package/dist/app/i18n/catalogs/fr-FR.js +41 -0
  44. package/dist/app/i18n/catalogs/fr-FR.js.map +1 -1
  45. package/dist/app/i18n/catalogs/hi-IN.d.ts.map +1 -1
  46. package/dist/app/i18n/catalogs/hi-IN.js +41 -0
  47. package/dist/app/i18n/catalogs/hi-IN.js.map +1 -1
  48. package/dist/app/i18n/catalogs/ja-JP.d.ts.map +1 -1
  49. package/dist/app/i18n/catalogs/ja-JP.js +41 -0
  50. package/dist/app/i18n/catalogs/ja-JP.js.map +1 -1
  51. package/dist/app/i18n/catalogs/ko-KR.d.ts.map +1 -1
  52. package/dist/app/i18n/catalogs/ko-KR.js +41 -0
  53. package/dist/app/i18n/catalogs/ko-KR.js.map +1 -1
  54. package/dist/app/i18n/catalogs/pt-BR.d.ts.map +1 -1
  55. package/dist/app/i18n/catalogs/pt-BR.js +41 -0
  56. package/dist/app/i18n/catalogs/pt-BR.js.map +1 -1
  57. package/dist/app/i18n/catalogs/zh-CN.d.ts.map +1 -1
  58. package/dist/app/i18n/catalogs/zh-CN.js +41 -0
  59. package/dist/app/i18n/catalogs/zh-CN.js.map +1 -1
  60. package/dist/app/i18n/catalogs/zh-TW.d.ts.map +1 -1
  61. package/dist/app/i18n/catalogs/zh-TW.js +41 -0
  62. package/dist/app/i18n/catalogs/zh-TW.js.map +1 -1
  63. package/dist/app/shared/StarfieldBackground.d.ts +1 -6
  64. package/dist/app/shared/StarfieldBackground.d.ts.map +1 -1
  65. package/dist/app/shared/StarfieldBackground.js +1 -368
  66. package/dist/app/shared/StarfieldBackground.js.map +1 -1
  67. package/dist/app/shared/WaveBackground.d.ts +5 -0
  68. package/dist/app/shared/WaveBackground.d.ts.map +1 -0
  69. package/dist/app/shared/WaveBackground.js +34 -0
  70. package/dist/app/shared/WaveBackground.js.map +1 -0
  71. package/dist/app/shared/WebGlWaveBackground.d.ts +9 -0
  72. package/dist/app/shared/WebGlWaveBackground.d.ts.map +1 -0
  73. package/dist/app/shared/WebGlWaveBackground.js +433 -0
  74. package/dist/app/shared/WebGlWaveBackground.js.map +1 -0
  75. package/dist/app/shared/index.d.ts +1 -0
  76. package/dist/app/shared/index.d.ts.map +1 -1
  77. package/dist/app/shared/index.js +1 -0
  78. package/dist/app/shared/index.js.map +1 -1
  79. package/dist/app/shared/ocean/bloom-blur.wgsl +43 -0
  80. package/dist/app/shared/ocean/bloom-blur.wgsl.js +1 -0
  81. package/dist/app/shared/ocean/bloom-bright.wgsl +21 -0
  82. package/dist/app/shared/ocean/bloom-bright.wgsl.js +1 -0
  83. package/dist/app/shared/ocean/bloom-composite.wgsl +40 -0
  84. package/dist/app/shared/ocean/bloom-composite.wgsl.js +1 -0
  85. package/dist/app/shared/ocean/brand-colors.d.ts +9 -0
  86. package/dist/app/shared/ocean/brand-colors.d.ts.map +1 -0
  87. package/dist/app/shared/ocean/brand-colors.js +45 -0
  88. package/dist/app/shared/ocean/brand-colors.js.map +1 -0
  89. package/dist/app/shared/ocean/camera.d.ts +5 -0
  90. package/dist/app/shared/ocean/camera.d.ts.map +1 -0
  91. package/dist/app/shared/ocean/camera.js +24 -0
  92. package/dist/app/shared/ocean/camera.js.map +1 -0
  93. package/dist/app/shared/ocean/hero-ocean-background.d.ts +8 -0
  94. package/dist/app/shared/ocean/hero-ocean-background.d.ts.map +1 -0
  95. package/dist/app/shared/ocean/hero-ocean-background.js +186 -0
  96. package/dist/app/shared/ocean/hero-ocean-background.js.map +1 -0
  97. package/dist/app/shared/ocean/ifft-stage.wgsl +36 -0
  98. package/dist/app/shared/ocean/ifft-stage.wgsl.js +1 -0
  99. package/dist/app/shared/ocean/initial-spectrum.wgsl +36 -0
  100. package/dist/app/shared/ocean/initial-spectrum.wgsl.js +1 -0
  101. package/dist/app/shared/ocean/noise.wgsl +24 -0
  102. package/dist/app/shared/ocean/noise.wgsl.js +1 -0
  103. package/dist/app/shared/ocean/normal-foam.wgsl +32 -0
  104. package/dist/app/shared/ocean/normal-foam.wgsl.js +1 -0
  105. package/dist/app/shared/ocean/ocean-colors.d.ts +6 -0
  106. package/dist/app/shared/ocean/ocean-colors.d.ts.map +1 -0
  107. package/dist/app/shared/ocean/ocean-colors.js +6 -0
  108. package/dist/app/shared/ocean/ocean-colors.js.map +1 -0
  109. package/dist/app/shared/ocean/ocean-common.wgsl +15 -0
  110. package/dist/app/shared/ocean/ocean-common.wgsl.js +1 -0
  111. package/dist/app/shared/ocean/ocean-graph.d.ts +12 -0
  112. package/dist/app/shared/ocean/ocean-graph.d.ts.map +1 -0
  113. package/dist/app/shared/ocean/ocean-graph.js +16 -0
  114. package/dist/app/shared/ocean/ocean-graph.js.map +1 -0
  115. package/dist/app/shared/ocean/particles.wgsl +139 -0
  116. package/dist/app/shared/ocean/particles.wgsl.js +1 -0
  117. package/dist/app/shared/ocean/present.wgsl +40 -0
  118. package/dist/app/shared/ocean/present.wgsl.js +1 -0
  119. package/dist/app/shared/ocean/renderer.d.ts +66 -0
  120. package/dist/app/shared/ocean/renderer.d.ts.map +1 -0
  121. package/dist/app/shared/ocean/renderer.js +535 -0
  122. package/dist/app/shared/ocean/renderer.js.map +1 -0
  123. package/dist/app/shared/ocean/spectrum.wgsl +35 -0
  124. package/dist/app/shared/ocean/spectrum.wgsl.js +1 -0
  125. package/dist/app/shared/ocean/tuning.d.ts +48 -0
  126. package/dist/app/shared/ocean/tuning.d.ts.map +1 -0
  127. package/dist/app/shared/ocean/tuning.js +74 -0
  128. package/dist/app/shared/ocean/tuning.js.map +1 -0
  129. package/dist/app/shared/ocean/webgpu-support.d.ts +3 -0
  130. package/dist/app/shared/ocean/webgpu-support.d.ts.map +1 -0
  131. package/dist/app/shared/ocean/webgpu-support.js +13 -0
  132. package/dist/app/shared/ocean/webgpu-support.js.map +1 -0
  133. package/dist/app/sharing/AccessRequestApprovalPage.d.ts +23 -0
  134. package/dist/app/sharing/AccessRequestApprovalPage.d.ts.map +1 -0
  135. package/dist/app/sharing/AccessRequestApprovalPage.js +95 -0
  136. package/dist/app/sharing/AccessRequestApprovalPage.js.map +1 -0
  137. package/dist/app/sharing/AccessRequestsSection.d.ts +5 -0
  138. package/dist/app/sharing/AccessRequestsSection.d.ts.map +1 -0
  139. package/dist/app/sharing/AccessRequestsSection.js +47 -0
  140. package/dist/app/sharing/AccessRequestsSection.js.map +1 -0
  141. package/dist/app/sharing/ResourceAccessScreen.d.ts +53 -0
  142. package/dist/app/sharing/ResourceAccessScreen.d.ts.map +1 -0
  143. package/dist/app/sharing/ResourceAccessScreen.js +132 -0
  144. package/dist/app/sharing/ResourceAccessScreen.js.map +1 -0
  145. package/dist/app/sharing/ShareButton.d.ts.map +1 -1
  146. package/dist/app/sharing/ShareButton.js +4 -2
  147. package/dist/app/sharing/ShareButton.js.map +1 -1
  148. package/dist/app/sharing/ShareDialog.d.ts.map +1 -1
  149. package/dist/app/sharing/ShareDialog.js +7 -5
  150. package/dist/app/sharing/ShareDialog.js.map +1 -1
  151. package/dist/app/sharing/index.d.ts +2 -0
  152. package/dist/app/sharing/index.d.ts.map +1 -1
  153. package/dist/app/sharing/index.js +2 -0
  154. package/dist/app/sharing/index.js.map +1 -1
  155. package/dist/app/styles/agent-native.css +21 -1
  156. package/dist/app/styles/tokens/agent-kit.css +3 -1
  157. package/dist/composer/AgentSuggestionBar.d.ts +1 -1
  158. package/dist/composer/AgentSuggestionBar.d.ts.map +1 -1
  159. package/dist/composer/AgentSuggestionBar.js +75 -6
  160. package/dist/composer/AgentSuggestionBar.js.map +1 -1
  161. package/package.json +14 -4
  162. package/src/app/agentkit/react/components.tsx +492 -60
  163. package/src/app/agentkit/react/context.tsx +10 -0
  164. package/src/app/agentkit/react/lifecycle.spec.tsx +487 -1
  165. package/src/app/agentkit/react/styles.css +78 -3
  166. package/src/app/auth/AuthPage.spec.tsx +11 -7
  167. package/src/app/auth/AuthPage.tsx +16 -35
  168. package/src/app/auth/server.spec.ts +68 -0
  169. package/src/app/auth/server.ts +29 -0
  170. package/src/app/chat/AgentKitAssistantChat.spec.tsx +287 -290
  171. package/src/app/chat/AgentKitAssistantChat.tsx +219 -116
  172. package/src/app/chat/AgentSidebar.tsx +3 -0
  173. package/src/app/chat/agentkit-chat/guided-questions.flow.spec.tsx +85 -0
  174. package/src/app/chat/agentkit-chat/guided-questions.tsx +12 -5
  175. package/src/app/chat/chat/surface-types.ts +2 -0
  176. package/src/app/i18n/catalogs/ar-SA.ts +44 -0
  177. package/src/app/i18n/catalogs/de-DE.ts +55 -0
  178. package/src/app/i18n/catalogs/en-US.ts +46 -0
  179. package/src/app/i18n/catalogs/es-ES.ts +53 -0
  180. package/src/app/i18n/catalogs/fr-FR.ts +53 -0
  181. package/src/app/i18n/catalogs/hi-IN.ts +47 -0
  182. package/src/app/i18n/catalogs/ja-JP.ts +54 -0
  183. package/src/app/i18n/catalogs/ko-KR.ts +50 -0
  184. package/src/app/i18n/catalogs/pt-BR.ts +52 -0
  185. package/src/app/i18n/catalogs/zh-CN.ts +42 -0
  186. package/src/app/i18n/catalogs/zh-TW.ts +43 -0
  187. package/src/app/shared/StarfieldBackground.tsx +4 -423
  188. package/src/app/shared/WaveBackground.spec.tsx +104 -0
  189. package/src/app/shared/WaveBackground.tsx +60 -0
  190. package/src/app/shared/WebGlWaveBackground.spec.tsx +106 -0
  191. package/src/app/shared/WebGlWaveBackground.tsx +499 -0
  192. package/src/app/shared/index.ts +1 -0
  193. package/src/app/shared/ocean/bloom-blur.wgsl +43 -0
  194. package/src/app/shared/ocean/bloom-bright.wgsl +21 -0
  195. package/src/app/shared/ocean/bloom-composite.wgsl +40 -0
  196. package/src/app/shared/ocean/brand-colors.ts +49 -0
  197. package/src/app/shared/ocean/camera.ts +28 -0
  198. package/src/app/shared/ocean/hero-ocean-background.spec.tsx +369 -0
  199. package/src/app/shared/ocean/hero-ocean-background.tsx +238 -0
  200. package/src/app/shared/ocean/ifft-stage.wgsl +36 -0
  201. package/src/app/shared/ocean/initial-spectrum.wgsl +36 -0
  202. package/src/app/shared/ocean/noise.wgsl +24 -0
  203. package/src/app/shared/ocean/normal-foam.wgsl +32 -0
  204. package/src/app/shared/ocean/ocean-colors.ts +11 -0
  205. package/src/app/shared/ocean/ocean-common.wgsl +15 -0
  206. package/src/app/shared/ocean/ocean-graph.ts +29 -0
  207. package/src/app/shared/ocean/particles.wgsl +139 -0
  208. package/src/app/shared/ocean/present.wgsl +40 -0
  209. package/src/app/shared/ocean/renderer.spec.ts +100 -0
  210. package/src/app/shared/ocean/renderer.ts +693 -0
  211. package/src/app/shared/ocean/spectrum.wgsl +35 -0
  212. package/src/app/shared/ocean/tuning.ts +131 -0
  213. package/src/app/shared/ocean/webgpu-support.ts +12 -0
  214. package/src/app/sharing/AccessRequestApprovalPage.spec.tsx +276 -0
  215. package/src/app/sharing/AccessRequestApprovalPage.tsx +307 -0
  216. package/src/app/sharing/AccessRequestsSection.tsx +163 -0
  217. package/src/app/sharing/ResourceAccessScreen.spec.tsx +235 -0
  218. package/src/app/sharing/ResourceAccessScreen.tsx +331 -0
  219. package/src/app/sharing/ShareButton.spec.tsx +171 -5
  220. package/src/app/sharing/ShareButton.tsx +15 -2
  221. package/src/app/sharing/ShareDialog.spec.tsx +71 -2
  222. package/src/app/sharing/ShareDialog.tsx +24 -2
  223. package/src/app/sharing/index.ts +2 -0
  224. package/src/app/styles/agent-native.css +21 -1
  225. package/src/app/styles/agent-native.spec.ts +16 -3
  226. package/src/app/styles/tokens/agent-kit.css +3 -1
  227. package/src/composer/AgentSuggestionBar.overflow.spec.tsx +277 -0
  228. package/src/composer/AgentSuggestionBar.tsx +113 -8
  229. package/src/wgsl-env.d.ts +6 -0
@@ -0,0 +1,307 @@
1
+ import { signOut, useSession } from "@agent-native/core/client/hooks";
2
+ import { useT } from "@agent-native/core/client/i18n";
3
+ import {
4
+ useAccessRequestReview,
5
+ type AccessRequestReview,
6
+ type AccessRequestRole,
7
+ } from "@agent-native/core/client/sharing/useAccessRequestReview";
8
+ import {
9
+ ActionButton,
10
+ Avatar,
11
+ Picker,
12
+ Skeleton,
13
+ Status,
14
+ } from "@agent-native/toolkit/design-system";
15
+ import { cn } from "@agent-native/toolkit/utils";
16
+ import { useEffect, useId, useRef, useState, type ReactNode } from "react";
17
+ import { Link, useParams } from "react-router";
18
+
19
+ import { ResourceAccessScreen } from "./ResourceAccessScreen.js";
20
+
21
+ export interface AccessRequestApprovalPageProps {
22
+ /**
23
+ * The request id from the link in the owner's email or notification.
24
+ * Defaults to the route's `requestId` param.
25
+ */
26
+ requestId?: string;
27
+ /** Replaces Switch account, which by default signs out. */
28
+ onSwitchAccount?: () => void;
29
+ /** Rendered above the page, such as a sidebar toggle. */
30
+ header?: ReactNode;
31
+ /** Render as the page's `main` landmark when the app shell has none. */
32
+ landmark?: boolean;
33
+ className?: string;
34
+ }
35
+
36
+ const ROLES = ["viewer", "commenter", "editor", "admin"] as const;
37
+
38
+ /**
39
+ * Where an access request's email and notification lead: who is asking, for
40
+ * what, and Allow or Decline. Opening it only reads; the decision is always an
41
+ * explicit press. Someone who can't manage the resource sees the same
42
+ * unavailable screen whether or not the request exists.
43
+ */
44
+ export function AccessRequestApprovalPage({
45
+ requestId,
46
+ onSwitchAccount,
47
+ header,
48
+ landmark = false,
49
+ className,
50
+ }: AccessRequestApprovalPageProps) {
51
+ const t = useT();
52
+ const params = useParams();
53
+ const { session } = useSession();
54
+ const controller = useAccessRequestReview(
55
+ requestId ?? params.requestId ?? "",
56
+ );
57
+ const Root = landmark ? "main" : "div";
58
+
59
+ if (controller.isSignedOut) {
60
+ return (
61
+ <ResourceAccessScreen
62
+ state="signed-out"
63
+ header={header}
64
+ landmark={landmark}
65
+ className={className}
66
+ />
67
+ );
68
+ }
69
+
70
+ if (controller.isUnavailable) {
71
+ return (
72
+ <ResourceAccessScreen
73
+ state="denied"
74
+ title={t("agentChat.accessRequest.unavailableTitle", {
75
+ defaultValue: "You can't review this request",
76
+ })}
77
+ description={t("agentChat.accessRequest.unavailableDescription", {
78
+ defaultValue:
79
+ "It may have been withdrawn, or this account can't manage access.",
80
+ })}
81
+ signedInEmail={session?.email ?? null}
82
+ onSwitchAccount={onSwitchAccount ?? (() => void signOut())}
83
+ header={header}
84
+ landmark={landmark}
85
+ className={className}
86
+ />
87
+ );
88
+ }
89
+
90
+ return (
91
+ <Root className={cn("flex min-h-0 flex-1 flex-col", className)}>
92
+ {header}
93
+ <div className="flex min-h-0 flex-1 items-center justify-center bg-background px-6 py-10">
94
+ {controller.review ? (
95
+ <ReviewPanel
96
+ review={controller.review}
97
+ decisions={controller.decisions}
98
+ />
99
+ ) : controller.isError ? (
100
+ <section className="flex w-full max-w-sm flex-col items-center text-center">
101
+ <p role="alert" className="text-sm text-muted-foreground">
102
+ {t("agentChat.accessRequest.loadFailed", {
103
+ defaultValue: "Couldn't load this request.",
104
+ })}
105
+ </p>
106
+ <ActionButton
107
+ emphasis="outline"
108
+ className="mt-4"
109
+ onPress={() => void controller.refetch()}
110
+ >
111
+ {t("agentChat.accessRequest.retry", { defaultValue: "Retry" })}
112
+ </ActionButton>
113
+ </section>
114
+ ) : (
115
+ <ReviewSkeleton />
116
+ )}
117
+ </div>
118
+ </Root>
119
+ );
120
+ }
121
+
122
+ function ReviewSkeleton() {
123
+ return (
124
+ <div aria-hidden="true" className="flex w-full max-w-md flex-col gap-6">
125
+ <Skeleton height={32} width="80%" />
126
+ <div className="flex items-center gap-3">
127
+ <Skeleton shape="circle" width={36} height={36} />
128
+ <div className="flex flex-1 flex-col gap-2">
129
+ <Skeleton height={14} width="40%" />
130
+ <Skeleton height={12} width="60%" />
131
+ </div>
132
+ </div>
133
+ <Skeleton height={14} width="50%" />
134
+ <div className="flex gap-2">
135
+ <Skeleton height={36} width={128} />
136
+ <Skeleton height={36} width={80} />
137
+ <Skeleton height={36} width={80} />
138
+ </div>
139
+ </div>
140
+ );
141
+ }
142
+
143
+ function ReviewPanel({
144
+ review,
145
+ decisions,
146
+ }: {
147
+ review: AccessRequestReview;
148
+ decisions: ReturnType<typeof useAccessRequestReview>["decisions"];
149
+ }) {
150
+ const t = useT();
151
+ const headingId = useId();
152
+ const headingRef = useRef<HTMLHeadingElement>(null);
153
+ const [role, setRole] = useState<AccessRequestRole>("viewer");
154
+ const name = review.requester.name?.trim() || review.requester.email;
155
+ const deciding = decisions.pendingId === review.id;
156
+ const roleLabel = (value: AccessRequestRole) =>
157
+ ({
158
+ viewer: t("agentChat.share.viewer", { defaultValue: "Viewer" }),
159
+ commenter: t("agentChat.share.commenter", { defaultValue: "Commenter" }),
160
+ editor: t("agentChat.share.editor", { defaultValue: "Editor" }),
161
+ admin: t("agentChat.share.admin", { defaultValue: "Admin" }),
162
+ })[value];
163
+
164
+ // A decision removes the buttons that held focus, so the outcome is read
165
+ // from the heading it moves to.
166
+ useEffect(() => {
167
+ headingRef.current?.focus({ preventScroll: true });
168
+ }, [review.state]);
169
+
170
+ // A failure shows as `decisions.error`, and a stale one reloads the request.
171
+ const decide = (run: () => Promise<void>) =>
172
+ void run().catch(() => undefined);
173
+
174
+ const heading =
175
+ review.state === "approved"
176
+ ? t("agentChat.accessRequest.approvedTitle", {
177
+ defaultValue: "Access allowed",
178
+ })
179
+ : review.state === "declined"
180
+ ? t("agentChat.accessRequest.declinedTitle", {
181
+ defaultValue: "Request declined",
182
+ })
183
+ : t("agentChat.accessRequest.title", {
184
+ name,
185
+ defaultValue: "{{name}} is asking for access",
186
+ });
187
+
188
+ const error = decisions.error;
189
+ const errorText = error
190
+ ? error.errorCode === "access_request_stale"
191
+ ? t("agentChat.accessRequest.stale", {
192
+ defaultValue: "Someone already handled this request, or it changed.",
193
+ })
194
+ : (error.message ??
195
+ t("agentChat.accessRequest.decisionFailed", {
196
+ defaultValue: "Couldn't save your decision. Try again.",
197
+ }))
198
+ : null;
199
+
200
+ return (
201
+ <section
202
+ aria-labelledby={headingId}
203
+ className="flex w-full max-w-md flex-col gap-6"
204
+ data-access-request-state={review.state}
205
+ >
206
+ <h1
207
+ ref={headingRef}
208
+ id={headingId}
209
+ tabIndex={-1}
210
+ className="break-words text-2xl font-semibold tracking-normal text-foreground outline-none"
211
+ >
212
+ {heading}
213
+ </h1>
214
+
215
+ <div className="flex items-center gap-3">
216
+ <Avatar name={name} size="default" className="shrink-0" />
217
+ <div className="min-w-0 flex-1">
218
+ <div className="truncate text-sm font-medium text-foreground">
219
+ {name}
220
+ </div>
221
+ {review.requester.name ? (
222
+ <div className="truncate text-sm text-muted-foreground">
223
+ {review.requester.email}
224
+ </div>
225
+ ) : null}
226
+ </div>
227
+ {review.state === "approved" && review.grantedRole ? (
228
+ <Status tone="neutral" size="compact">
229
+ {roleLabel(review.grantedRole)}
230
+ </Status>
231
+ ) : null}
232
+ </div>
233
+
234
+ <div className="text-sm text-muted-foreground">
235
+ <span>{review.resource.label}</span>{" "}
236
+ {review.resource.path ? (
237
+ <Link
238
+ to={review.resource.path}
239
+ className="font-medium text-foreground underline decoration-border underline-offset-4 hover:decoration-foreground"
240
+ >
241
+ {review.resource.title}
242
+ </Link>
243
+ ) : (
244
+ <span className="font-medium text-foreground">
245
+ {review.resource.title}
246
+ </span>
247
+ )}
248
+ </div>
249
+
250
+ {review.note ? (
251
+ <blockquote className="whitespace-pre-wrap break-words border-s-2 border-border ps-3 text-sm text-foreground">
252
+ {review.note}
253
+ </blockquote>
254
+ ) : null}
255
+
256
+ {review.state === "pending" ? (
257
+ <div className="flex flex-wrap items-center gap-2">
258
+ <Picker
259
+ mode="select"
260
+ options={ROLES.map((value) => ({
261
+ value,
262
+ label: roleLabel(value),
263
+ }))}
264
+ value={role}
265
+ onChange={(value) => {
266
+ if (value && (ROLES as readonly string[]).includes(value)) {
267
+ setRole(value as AccessRequestRole);
268
+ }
269
+ }}
270
+ disabled={deciding}
271
+ aria-label={t("agentChat.share.role", { defaultValue: "Role" })}
272
+ className="w-auto"
273
+ />
274
+ <ActionButton
275
+ pending={deciding}
276
+ disabled={deciding}
277
+ onPress={() => decide(() => decisions.approve(review, role))}
278
+ >
279
+ {t("agentChat.accessRequest.allow", { defaultValue: "Allow" })}
280
+ </ActionButton>
281
+ <ActionButton
282
+ emphasis="outline"
283
+ disabled={deciding}
284
+ onPress={() => decide(() => decisions.decline(review))}
285
+ >
286
+ {t("agentChat.accessRequest.decline", { defaultValue: "Decline" })}
287
+ </ActionButton>
288
+ </div>
289
+ ) : null}
290
+
291
+ {decisions.unemailed?.id === review.id ? (
292
+ <p role="status" className="text-sm text-muted-foreground">
293
+ {t("agentChat.accessRequest.emailFailed", {
294
+ name,
295
+ defaultValue: "{{name}} has access, but we couldn't email them.",
296
+ })}
297
+ </p>
298
+ ) : null}
299
+
300
+ {errorText ? (
301
+ <p role="alert" className="text-sm text-destructive">
302
+ {errorText}
303
+ </p>
304
+ ) : null}
305
+ </section>
306
+ );
307
+ }
@@ -0,0 +1,163 @@
1
+ import { useT } from "@agent-native/core/client/i18n";
2
+ import {
3
+ useResourceAccessRequests,
4
+ type AccessRequestReview,
5
+ } from "@agent-native/core/client/sharing/useAccessRequestReview";
6
+ import {
7
+ ActionButton,
8
+ Avatar as DesignSystemAvatar,
9
+ } from "@agent-native/toolkit/design-system";
10
+
11
+ // Allow here grants Viewer; the request's own page offers every role.
12
+ export function AccessRequestsSection({
13
+ resourceType,
14
+ resourceId,
15
+ }: {
16
+ resourceType: string;
17
+ resourceId: string;
18
+ }) {
19
+ const t = useT();
20
+ const { requests, hasMore, isError, refetch, decisions } =
21
+ useResourceAccessRequests({ resourceType, resourceId });
22
+ const error = decisions.error;
23
+ const unemailed = decisions.unemailed;
24
+ // A decision's outcome stays readable after its row is gone, even when the
25
+ // list's reload fails: a stale decision, or an email that didn't send.
26
+ if (!isError && !requests.length && !error && !unemailed) return null;
27
+ // A failure shows as `decisions.error`, and a stale one reloads the list.
28
+ const decide = (run: () => Promise<void>) =>
29
+ void run().catch(() => undefined);
30
+ return (
31
+ <div className="space-y-2">
32
+ {isError ? (
33
+ <div className="flex items-center gap-2 text-sm text-muted-foreground">
34
+ <span role="alert">
35
+ {t("agentChat.share.accessRequestsLoadFailed", {
36
+ defaultValue: "Couldn't load access requests.",
37
+ })}
38
+ </span>
39
+ <ActionButton
40
+ emphasis="ghost"
41
+ size="compact"
42
+ onPress={() => void refetch()}
43
+ >
44
+ {t("agentChat.accessRequest.retry", { defaultValue: "Retry" })}
45
+ </ActionButton>
46
+ </div>
47
+ ) : (
48
+ <>
49
+ <div className="text-sm font-semibold">
50
+ {t("agentChat.share.accessRequests", {
51
+ defaultValue: "Access requests",
52
+ })}
53
+ </div>
54
+ {requests.length ? (
55
+ <ul className="m-0 flex list-none flex-col gap-1 p-0">
56
+ {requests.map((request) => (
57
+ <AccessRequestRow
58
+ key={request.id}
59
+ request={request}
60
+ deciding={decisions.pendingId === request.id}
61
+ onAllow={() =>
62
+ decide(() => decisions.approve(request, "viewer"))
63
+ }
64
+ onDecline={() => decide(() => decisions.decline(request))}
65
+ />
66
+ ))}
67
+ </ul>
68
+ ) : null}
69
+ {hasMore ? (
70
+ <p className="text-xs text-muted-foreground">
71
+ {t("agentChat.share.accessRequestsNewest", {
72
+ count: requests.length,
73
+ defaultValue: "Showing the {{count}} newest requests.",
74
+ })}
75
+ </p>
76
+ ) : null}
77
+ </>
78
+ )}
79
+ {unemailed ? (
80
+ <p role="status" className="text-xs text-muted-foreground">
81
+ {t("agentChat.accessRequest.emailFailed", {
82
+ name: unemailed.requester.name?.trim() || unemailed.requester.email,
83
+ defaultValue: "{{name}} has access, but we couldn't email them.",
84
+ })}
85
+ </p>
86
+ ) : null}
87
+ {error ? (
88
+ <p role="alert" className="text-xs text-destructive">
89
+ {error.errorCode === "access_request_stale"
90
+ ? t("agentChat.accessRequest.stale", {
91
+ defaultValue:
92
+ "Someone already handled this request, or it changed.",
93
+ })
94
+ : (error.message ??
95
+ t("agentChat.accessRequest.decisionFailed", {
96
+ defaultValue: "Couldn't save your decision. Try again.",
97
+ }))}
98
+ </p>
99
+ ) : null}
100
+ </div>
101
+ );
102
+ }
103
+
104
+ function AccessRequestRow({
105
+ request,
106
+ deciding,
107
+ onAllow,
108
+ onDecline,
109
+ }: {
110
+ request: AccessRequestReview;
111
+ deciding: boolean;
112
+ onAllow: () => void;
113
+ onDecline: () => void;
114
+ }) {
115
+ const t = useT();
116
+ const name = request.requester.name?.trim() || request.requester.email;
117
+ return (
118
+ <li className="flex items-start gap-3 px-1 py-1.5 text-sm">
119
+ <DesignSystemAvatar
120
+ name={name}
121
+ size="compact"
122
+ className="mt-0.5 inline-flex h-7 w-7 shrink-0 text-[11px] font-semibold"
123
+ />
124
+ <div className="min-w-0 flex-1">
125
+ <div className="truncate">{name}</div>
126
+ {request.requester.name ? (
127
+ <div className="truncate text-xs text-muted-foreground">
128
+ {request.requester.email}
129
+ </div>
130
+ ) : null}
131
+ {request.note ? (
132
+ <div className="line-clamp-2 break-words text-xs text-muted-foreground">
133
+ {request.note}
134
+ </div>
135
+ ) : null}
136
+ </div>
137
+ <ActionButton
138
+ size="compact"
139
+ pending={deciding}
140
+ disabled={deciding}
141
+ onPress={onAllow}
142
+ aria-label={t("agentChat.share.allowRequestFrom", {
143
+ name,
144
+ defaultValue: "Allow {{name}}",
145
+ })}
146
+ >
147
+ {t("agentChat.accessRequest.allow", { defaultValue: "Allow" })}
148
+ </ActionButton>
149
+ <ActionButton
150
+ size="compact"
151
+ emphasis="ghost"
152
+ disabled={deciding}
153
+ onPress={onDecline}
154
+ aria-label={t("agentChat.share.declineRequestFrom", {
155
+ name,
156
+ defaultValue: "Decline {{name}}",
157
+ })}
158
+ >
159
+ {t("agentChat.accessRequest.decline", { defaultValue: "Decline" })}
160
+ </ActionButton>
161
+ </li>
162
+ );
163
+ }
@@ -0,0 +1,235 @@
1
+ // @vitest-environment happy-dom
2
+ import { AgentNativeI18nProvider } from "@agent-native/core/client/i18n";
3
+ import { act } from "react";
4
+ import { createRoot, type Root } from "react-dom/client";
5
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
6
+
7
+ import { createToolkitI18nCatalog } from "../i18n.js";
8
+ import {
9
+ ResourceAccessScreen,
10
+ type ResourceAccessScreenProps,
11
+ } from "./ResourceAccessScreen.js";
12
+
13
+ const signInHref = vi.hoisted(() => vi.fn(() => "/_agent-native/sign-in"));
14
+
15
+ vi.mock("@agent-native/core/client/sign-in-return", () => ({
16
+ buildSignInReturnHref: signInHref,
17
+ }));
18
+
19
+ const catalog = createToolkitI18nCatalog({ messages: {} });
20
+
21
+ describe("ResourceAccessScreen", () => {
22
+ let container: HTMLDivElement;
23
+ let root: Root;
24
+
25
+ beforeEach(() => {
26
+ vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
27
+ container = document.createElement("div");
28
+ document.body.appendChild(container);
29
+ root = createRoot(container);
30
+ });
31
+
32
+ afterEach(() => {
33
+ act(() => root.unmount());
34
+ container.remove();
35
+ vi.unstubAllGlobals();
36
+ });
37
+
38
+ function render(props: ResourceAccessScreenProps, locale = "en-US") {
39
+ act(() => {
40
+ root.render(
41
+ <AgentNativeI18nProvider
42
+ catalog={catalog}
43
+ initialLocale={locale}
44
+ initialPreference={locale}
45
+ persistPreference={false}
46
+ >
47
+ <ResourceAccessScreen {...props} />
48
+ </AgentNativeI18nProvider>,
49
+ );
50
+ });
51
+ }
52
+
53
+ function buttons() {
54
+ return [...container.querySelectorAll("button")].map(
55
+ (button) => button.textContent,
56
+ );
57
+ }
58
+
59
+ it("says the viewer has no access, names their account, and focuses the heading", () => {
60
+ const onSwitchAccount = vi.fn();
61
+ render({
62
+ state: "denied",
63
+ signedInEmail: "outsider@example.test",
64
+ actions: <a href="/home">Go to my pages</a>,
65
+ onSwitchAccount,
66
+ });
67
+
68
+ const heading = container.querySelector("h1");
69
+ expect(heading?.textContent).toBe("You don't have access");
70
+ expect(document.activeElement).toBe(heading);
71
+ expect(
72
+ container.querySelector("section")?.getAttribute("aria-labelledby"),
73
+ ).toBe(heading?.id);
74
+ expect(container.textContent).toContain(
75
+ "You're signed in as outsider@example.test",
76
+ );
77
+ expect(container.querySelector('[role="status"], [aria-live]')).toBeNull();
78
+ expect(container.querySelector("main")).toBeNull();
79
+
80
+ const switchAccount = [...container.querySelectorAll("button")].find(
81
+ (button) => button.textContent === "Switch account",
82
+ );
83
+ act(() => switchAccount?.click());
84
+ expect(onSwitchAccount).toHaveBeenCalledTimes(1);
85
+ });
86
+
87
+ it("puts the app's actions before Switch account", () => {
88
+ render({
89
+ state: "denied",
90
+ actions: <button type="button">Go to my pages</button>,
91
+ onSwitchAccount: () => {},
92
+ });
93
+
94
+ expect(buttons()).toEqual(["Go to my pages", "Switch account"]);
95
+ });
96
+
97
+ it("uses the app's heading and description when given", () => {
98
+ render({
99
+ state: "missing",
100
+ title: "This page doesn't exist",
101
+ description: "Custom description",
102
+ });
103
+
104
+ expect(container.querySelector("h1")?.textContent).toBe(
105
+ "This page doesn't exist",
106
+ );
107
+ expect(container.textContent).toContain("Custom description");
108
+ expect(buttons()).toEqual([]);
109
+ });
110
+
111
+ it("sends a signed-out visitor to sign-in, which returns to the link", () => {
112
+ const assign = vi.fn();
113
+ vi.stubGlobal("location", { ...window.location, assign });
114
+ render({ state: "signed-out" });
115
+
116
+ expect(container.querySelector("h1")?.textContent).toBe(
117
+ "Sign in to continue",
118
+ );
119
+ const signIn = [...container.querySelectorAll("button")].find(
120
+ (button) => button.textContent === "Sign in",
121
+ );
122
+ act(() => signIn?.click());
123
+ expect(assign).toHaveBeenCalledWith("/_agent-native/sign-in");
124
+ });
125
+
126
+ it("can be the page's main landmark", () => {
127
+ render({ state: "trashed", landmark: true });
128
+
129
+ expect(container.querySelector("main")?.dataset.accessState).toBe(
130
+ "trashed",
131
+ );
132
+ expect(container.querySelector("h1")?.textContent).toBe(
133
+ "This is in the trash",
134
+ );
135
+ });
136
+
137
+ function typeInto(element: HTMLTextAreaElement, value: string) {
138
+ const setter = Object.getOwnPropertyDescriptor(
139
+ HTMLTextAreaElement.prototype,
140
+ "value",
141
+ )?.set;
142
+ act(() => {
143
+ setter?.call(element, value);
144
+ element.dispatchEvent(new Event("input", { bubbles: true }));
145
+ });
146
+ }
147
+
148
+ function bodyButton(label: string) {
149
+ return [...document.body.querySelectorAll("button")].find(
150
+ (button) => button.textContent === label,
151
+ );
152
+ }
153
+
154
+ it("offers Request access with an optional note, announced politely", async () => {
155
+ const onRequest = vi.fn(async () => undefined);
156
+ render({
157
+ state: "denied",
158
+ signedInEmail: "outsider@example.test",
159
+ request: { sent: false, onRequest },
160
+ onSwitchAccount: () => {},
161
+ });
162
+
163
+ expect(buttons()).toEqual(["Request access", "Switch account"]);
164
+ expect(container.querySelector('[role="status"]')?.textContent).toBe(
165
+ "Request access and the owner will be notified.",
166
+ );
167
+
168
+ act(() => bodyButton("Request access")?.click());
169
+ const note = document.body.querySelector("textarea");
170
+ expect(note).not.toBeNull();
171
+ typeInto(note!, "Need this for the launch review.");
172
+ await act(async () => bodyButton("Send request")?.click());
173
+
174
+ expect(onRequest).toHaveBeenCalledWith("Need this for the launch review.");
175
+ expect(document.body.querySelector("textarea")).toBeNull();
176
+ await vi.waitFor(() => {
177
+ expect(document.activeElement).toBe(container.querySelector("h1"));
178
+ });
179
+ });
180
+
181
+ it("says the request was sent and stops offering another", () => {
182
+ render({
183
+ state: "denied",
184
+ request: { sent: true, onRequest: vi.fn() },
185
+ onSwitchAccount: () => {},
186
+ });
187
+
188
+ expect(container.querySelector('[role="status"]')?.textContent).toBe(
189
+ "Request sent. The owner has been notified.",
190
+ );
191
+ expect(buttons()).toEqual(["Switch account"]);
192
+ });
193
+
194
+ it("explains a rate limit apart from other failures", async () => {
195
+ const request = {
196
+ sent: false,
197
+ onRequest: vi.fn(async () => {
198
+ throw new Error("failed");
199
+ }),
200
+ error: { errorCode: "access_request_rate_limited" },
201
+ };
202
+ render({ state: "denied", request });
203
+
204
+ act(() => bodyButton("Request access")?.click());
205
+ await act(async () => bodyButton("Send request")?.click());
206
+ expect(document.body.textContent).toContain(
207
+ "Too many requests right now. Try again later.",
208
+ );
209
+
210
+ render({ state: "denied", request: { ...request, error: null } });
211
+ await act(async () => bodyButton("Send request")?.click());
212
+ expect(document.body.textContent).toContain(
213
+ "Couldn't send your request. Try again.",
214
+ );
215
+ });
216
+
217
+ it("tells a signed-out visitor that signing in is how to ask", () => {
218
+ render({ state: "signed-out", acceptsRequests: true });
219
+
220
+ expect(container.textContent).toContain("Sign in to request access.");
221
+ });
222
+
223
+ it("translates its own strings", async () => {
224
+ render({ state: "denied", signedInEmail: "a@example.test" }, "de-DE");
225
+
226
+ await vi.waitFor(() => {
227
+ expect(container.querySelector("h1")?.textContent).toBe(
228
+ "Du hast keinen Zugriff",
229
+ );
230
+ });
231
+ expect(container.textContent).toContain(
232
+ "Du bist als a@example.test angemeldet",
233
+ );
234
+ });
235
+ });