@agent-native/core 0.107.2 → 0.108.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 (228) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +6 -0
  3. package/corpus/core/docs/content/actions.mdx +48 -0
  4. package/corpus/core/docs/content/locales/ar-SA/actions.mdx +38 -0
  5. package/corpus/core/docs/content/locales/de-DE/actions.mdx +38 -0
  6. package/corpus/core/docs/content/locales/es-ES/actions.mdx +38 -0
  7. package/corpus/core/docs/content/locales/fr-FR/actions.mdx +38 -0
  8. package/corpus/core/docs/content/locales/hi-IN/actions.mdx +38 -0
  9. package/corpus/core/docs/content/locales/ja-JP/actions.mdx +38 -0
  10. package/corpus/core/docs/content/locales/ko-KR/actions.mdx +38 -0
  11. package/corpus/core/docs/content/locales/pt-BR/actions.mdx +38 -0
  12. package/corpus/core/docs/content/locales/zh-CN/actions.mdx +38 -0
  13. package/corpus/core/docs/content/locales/zh-TW/actions.mdx +38 -0
  14. package/corpus/core/package.json +5 -1
  15. package/corpus/core/src/a2a-claims.ts +52 -0
  16. package/corpus/core/src/action.ts +6 -2
  17. package/corpus/core/src/audit/record.ts +6 -0
  18. package/corpus/core/src/client/feature-flags/FeatureFlagsPanel.tsx +519 -0
  19. package/corpus/core/src/client/feature-flags/helpers.ts +77 -0
  20. package/corpus/core/src/client/feature-flags/index.ts +13 -0
  21. package/corpus/core/src/client/feature-flags/types.ts +29 -0
  22. package/corpus/core/src/client/feature-flags/use-feature-flag.ts +26 -0
  23. package/corpus/core/src/client/index.ts +11 -0
  24. package/corpus/core/src/extensions/routes.ts +1 -1
  25. package/corpus/core/src/feature-flags/a2a-action-route.ts +56 -0
  26. package/corpus/core/src/feature-flags/actions/get-feature-flags.ts +24 -0
  27. package/corpus/core/src/feature-flags/actions/list-feature-flags.ts +69 -0
  28. package/corpus/core/src/feature-flags/actions/set-feature-flag.ts +89 -0
  29. package/corpus/core/src/feature-flags/index.ts +21 -0
  30. package/corpus/core/src/feature-flags/permissions.ts +53 -0
  31. package/corpus/core/src/feature-flags/plugin.ts +44 -0
  32. package/corpus/core/src/feature-flags/registry.ts +96 -0
  33. package/corpus/core/src/feature-flags/store.ts +176 -0
  34. package/corpus/core/src/index.ts +15 -0
  35. package/corpus/core/src/localization/default-messages.ts +47 -0
  36. package/corpus/core/src/server/action-discovery.ts +12 -0
  37. package/corpus/core/src/server/action-routes.ts +47 -5
  38. package/corpus/core/src/server/index.ts +1 -0
  39. package/corpus/core/src/settings/org-settings.ts +13 -0
  40. package/corpus/core/src/settings/store.ts +57 -0
  41. package/corpus/core/src/templates/default/.agents/skills/feature-flags/SKILL.md +169 -0
  42. package/corpus/core/src/templates/default/AGENTS.md +1 -0
  43. package/corpus/core/src/templates/headless/.agents/skills/feature-flags/SKILL.md +169 -0
  44. package/corpus/core/src/templates/workspace-core/.agents/skills/feature-flags/SKILL.md +169 -0
  45. package/corpus/core/src/templates/workspace-core/AGENTS.md +1 -1
  46. package/corpus/core/src/templates/workspace-root/AGENTS.md +1 -1
  47. package/corpus/core/src/vite/action-types-plugin.ts +12 -0
  48. package/corpus/templates/analytics/.agents/skills/feature-flags/SKILL.md +169 -0
  49. package/corpus/templates/analytics/AGENTS.md +15 -3
  50. package/corpus/templates/analytics/actions/list-workspace-feature-flags.ts +15 -0
  51. package/corpus/templates/analytics/actions/navigate.ts +2 -2
  52. package/corpus/templates/analytics/actions/set-workspace-feature-flag.ts +45 -0
  53. package/corpus/templates/analytics/actions/view-screen.ts +13 -0
  54. package/corpus/templates/analytics/app/components/agents/FeatureFlagsFleetPanel.tsx +243 -0
  55. package/corpus/templates/analytics/app/components/layout/CommandPalette.tsx +11 -3
  56. package/corpus/templates/analytics/app/hooks/use-navigation-state.ts +3 -1
  57. package/corpus/templates/analytics/app/i18n/zh-TW.ts +17 -0
  58. package/corpus/templates/analytics/app/i18n-data.ts +251 -0
  59. package/corpus/templates/analytics/app/pages/Agents.tsx +22 -2
  60. package/corpus/templates/analytics/changelog/2026-07-17-manage-feature-flag-rollouts-across-your-organization-s-apps.md +6 -0
  61. package/corpus/templates/analytics/server/lib/db-admin-connections.ts +13 -5
  62. package/corpus/templates/analytics/server/lib/workspace-feature-flags.ts +347 -0
  63. package/corpus/templates/assets/.agents/skills/feature-flags/SKILL.md +169 -0
  64. package/corpus/templates/brain/.agents/skills/feature-flags/SKILL.md +169 -0
  65. package/corpus/templates/calendar/.agents/skills/feature-flags/SKILL.md +169 -0
  66. package/corpus/templates/chat/.agents/skills/feature-flags/SKILL.md +169 -0
  67. package/corpus/templates/clips/.agents/skills/feature-flags/SKILL.md +169 -0
  68. package/corpus/templates/clips/server/plugins/feature-flags.ts +11 -0
  69. package/corpus/templates/clips/shared/feature-flags.ts +30 -37
  70. package/corpus/templates/content/.agents/skills/feature-flags/SKILL.md +169 -0
  71. package/corpus/templates/design/.agents/skills/feature-flags/SKILL.md +169 -0
  72. package/corpus/templates/design/AGENTS.md +5 -3
  73. package/corpus/templates/design/actions/add-fusion-screens.ts +4 -6
  74. package/corpus/templates/design/actions/apply-fusion-edits.ts +4 -6
  75. package/corpus/templates/design/actions/create-fusion-app.ts +5 -4
  76. package/corpus/templates/design/actions/deploy-fusion-app.ts +4 -3
  77. package/corpus/templates/design/actions/get-fusion-deploy-status.ts +4 -6
  78. package/corpus/templates/design/actions/list-fusion-edits.ts +4 -3
  79. package/corpus/templates/design/actions/push-fusion-app.ts +4 -6
  80. package/corpus/templates/design/actions/queue-fusion-edit.ts +4 -3
  81. package/corpus/templates/design/actions/send-fusion-message.ts +4 -6
  82. package/corpus/templates/design/actions/sync-fusion-app.ts +4 -3
  83. package/corpus/templates/design/app/components/editor/PromptDialog.tsx +1 -1
  84. package/corpus/templates/design/app/pages/Index.tsx +8 -6
  85. package/corpus/templates/design/server/plugins/feature-flags.ts +5 -0
  86. package/corpus/templates/design/shared/full-app.ts +10 -7
  87. package/corpus/templates/dispatch/.agents/skills/feature-flags/SKILL.md +169 -0
  88. package/corpus/templates/forms/.agents/skills/feature-flags/SKILL.md +169 -0
  89. package/corpus/templates/macros/.agents/skills/feature-flags/SKILL.md +169 -0
  90. package/corpus/templates/mail/.agents/skills/feature-flags/SKILL.md +169 -0
  91. package/corpus/templates/plan/.agents/skills/feature-flags/SKILL.md +169 -0
  92. package/corpus/templates/slides/.agents/skills/feature-flags/SKILL.md +169 -0
  93. package/corpus/templates/tasks/.agents/skills/feature-flags/SKILL.md +169 -0
  94. package/dist/a2a-claims.d.ts +10 -0
  95. package/dist/a2a-claims.d.ts.map +1 -0
  96. package/dist/a2a-claims.js +41 -0
  97. package/dist/a2a-claims.js.map +1 -0
  98. package/dist/action.d.ts +6 -2
  99. package/dist/action.d.ts.map +1 -1
  100. package/dist/action.js.map +1 -1
  101. package/dist/audit/record.d.ts +3 -0
  102. package/dist/audit/record.d.ts.map +1 -1
  103. package/dist/audit/record.js +3 -0
  104. package/dist/audit/record.js.map +1 -1
  105. package/dist/client/feature-flags/FeatureFlagsPanel.d.ts +10 -0
  106. package/dist/client/feature-flags/FeatureFlagsPanel.d.ts.map +1 -0
  107. package/dist/client/feature-flags/FeatureFlagsPanel.js +143 -0
  108. package/dist/client/feature-flags/FeatureFlagsPanel.js.map +1 -0
  109. package/dist/client/feature-flags/helpers.d.ts +16 -0
  110. package/dist/client/feature-flags/helpers.d.ts.map +1 -0
  111. package/dist/client/feature-flags/helpers.js +50 -0
  112. package/dist/client/feature-flags/helpers.js.map +1 -0
  113. package/dist/client/feature-flags/index.d.ts +5 -0
  114. package/dist/client/feature-flags/index.d.ts.map +1 -0
  115. package/dist/client/feature-flags/index.js +4 -0
  116. package/dist/client/feature-flags/index.js.map +1 -0
  117. package/dist/client/feature-flags/types.d.ts +27 -0
  118. package/dist/client/feature-flags/types.d.ts.map +1 -0
  119. package/dist/client/feature-flags/types.js +2 -0
  120. package/dist/client/feature-flags/types.js.map +1 -0
  121. package/dist/client/feature-flags/use-feature-flag.d.ts +8 -0
  122. package/dist/client/feature-flags/use-feature-flag.d.ts.map +1 -0
  123. package/dist/client/feature-flags/use-feature-flag.js +15 -0
  124. package/dist/client/feature-flags/use-feature-flag.js.map +1 -0
  125. package/dist/client/index.d.ts +1 -0
  126. package/dist/client/index.d.ts.map +1 -1
  127. package/dist/client/index.js +1 -0
  128. package/dist/client/index.js.map +1 -1
  129. package/dist/collab/routes.d.ts +1 -1
  130. package/dist/extensions/routes.d.ts.map +1 -1
  131. package/dist/extensions/routes.js +1 -1
  132. package/dist/extensions/routes.js.map +1 -1
  133. package/dist/feature-flags/a2a-action-route.d.ts +14 -0
  134. package/dist/feature-flags/a2a-action-route.d.ts.map +1 -0
  135. package/dist/feature-flags/a2a-action-route.js +50 -0
  136. package/dist/feature-flags/a2a-action-route.js.map +1 -0
  137. package/dist/feature-flags/actions/get-feature-flags.d.ts +3 -0
  138. package/dist/feature-flags/actions/get-feature-flags.d.ts.map +1 -0
  139. package/dist/feature-flags/actions/get-feature-flags.js +18 -0
  140. package/dist/feature-flags/actions/get-feature-flags.js.map +1 -0
  141. package/dist/feature-flags/actions/list-feature-flags.d.ts +28 -0
  142. package/dist/feature-flags/actions/list-feature-flags.d.ts.map +1 -0
  143. package/dist/feature-flags/actions/list-feature-flags.js +60 -0
  144. package/dist/feature-flags/actions/list-feature-flags.js.map +1 -0
  145. package/dist/feature-flags/actions/set-feature-flag.d.ts +26 -0
  146. package/dist/feature-flags/actions/set-feature-flag.d.ts.map +1 -0
  147. package/dist/feature-flags/actions/set-feature-flag.js +78 -0
  148. package/dist/feature-flags/actions/set-feature-flag.js.map +1 -0
  149. package/dist/feature-flags/index.d.ts +5 -0
  150. package/dist/feature-flags/index.d.ts.map +1 -0
  151. package/dist/feature-flags/index.js +5 -0
  152. package/dist/feature-flags/index.js.map +1 -0
  153. package/dist/feature-flags/permissions.d.ts +13 -0
  154. package/dist/feature-flags/permissions.d.ts.map +1 -0
  155. package/dist/feature-flags/permissions.js +37 -0
  156. package/dist/feature-flags/permissions.js.map +1 -0
  157. package/dist/feature-flags/plugin.d.ts +17 -0
  158. package/dist/feature-flags/plugin.d.ts.map +1 -0
  159. package/dist/feature-flags/plugin.js +29 -0
  160. package/dist/feature-flags/plugin.js.map +1 -0
  161. package/dist/feature-flags/registry.d.ts +23 -0
  162. package/dist/feature-flags/registry.d.ts.map +1 -0
  163. package/dist/feature-flags/registry.js +59 -0
  164. package/dist/feature-flags/registry.js.map +1 -0
  165. package/dist/feature-flags/store.d.ts +31 -0
  166. package/dist/feature-flags/store.d.ts.map +1 -0
  167. package/dist/feature-flags/store.js +124 -0
  168. package/dist/feature-flags/store.js.map +1 -0
  169. package/dist/index.d.ts +1 -0
  170. package/dist/index.d.ts.map +1 -1
  171. package/dist/index.js +1 -0
  172. package/dist/index.js.map +1 -1
  173. package/dist/localization/default-messages.d.ts +42 -0
  174. package/dist/localization/default-messages.d.ts.map +1 -1
  175. package/dist/localization/default-messages.js +42 -0
  176. package/dist/localization/default-messages.js.map +1 -1
  177. package/dist/observability/routes.d.ts +1 -1
  178. package/dist/progress/routes.d.ts +1 -1
  179. package/dist/resources/handlers.d.ts +1 -1
  180. package/dist/server/action-discovery.d.ts.map +1 -1
  181. package/dist/server/action-discovery.js +12 -0
  182. package/dist/server/action-discovery.js.map +1 -1
  183. package/dist/server/action-routes.d.ts +3 -0
  184. package/dist/server/action-routes.d.ts.map +1 -1
  185. package/dist/server/action-routes.js +45 -5
  186. package/dist/server/action-routes.js.map +1 -1
  187. package/dist/server/index.d.ts +1 -0
  188. package/dist/server/index.d.ts.map +1 -1
  189. package/dist/server/index.js +1 -0
  190. package/dist/server/index.js.map +1 -1
  191. package/dist/settings/org-settings.d.ts +2 -0
  192. package/dist/settings/org-settings.d.ts.map +1 -1
  193. package/dist/settings/org-settings.js +5 -1
  194. package/dist/settings/org-settings.js.map +1 -1
  195. package/dist/settings/store.d.ts +8 -0
  196. package/dist/settings/store.d.ts.map +1 -1
  197. package/dist/settings/store.js +48 -0
  198. package/dist/settings/store.js.map +1 -1
  199. package/dist/templates/chat/.agents/skills/feature-flags/SKILL.md +169 -0
  200. package/dist/templates/default/.agents/skills/feature-flags/SKILL.md +169 -0
  201. package/dist/templates/default/AGENTS.md +1 -0
  202. package/dist/templates/headless/.agents/skills/feature-flags/SKILL.md +169 -0
  203. package/dist/templates/workspace-core/.agents/skills/feature-flags/SKILL.md +169 -0
  204. package/dist/templates/workspace-core/AGENTS.md +1 -1
  205. package/dist/templates/workspace-root/AGENTS.md +1 -1
  206. package/dist/vite/action-types-plugin.d.ts.map +1 -1
  207. package/dist/vite/action-types-plugin.js +12 -0
  208. package/dist/vite/action-types-plugin.js.map +1 -1
  209. package/docs/content/actions.mdx +48 -0
  210. package/docs/content/locales/ar-SA/actions.mdx +38 -0
  211. package/docs/content/locales/de-DE/actions.mdx +38 -0
  212. package/docs/content/locales/es-ES/actions.mdx +38 -0
  213. package/docs/content/locales/fr-FR/actions.mdx +38 -0
  214. package/docs/content/locales/hi-IN/actions.mdx +38 -0
  215. package/docs/content/locales/ja-JP/actions.mdx +38 -0
  216. package/docs/content/locales/ko-KR/actions.mdx +38 -0
  217. package/docs/content/locales/pt-BR/actions.mdx +38 -0
  218. package/docs/content/locales/zh-CN/actions.mdx +38 -0
  219. package/docs/content/locales/zh-TW/actions.mdx +38 -0
  220. package/package.json +5 -1
  221. package/src/templates/chat/.agents/skills/feature-flags/SKILL.md +169 -0
  222. package/src/templates/default/.agents/skills/feature-flags/SKILL.md +169 -0
  223. package/src/templates/default/AGENTS.md +1 -0
  224. package/src/templates/headless/.agents/skills/feature-flags/SKILL.md +169 -0
  225. package/src/templates/workspace-core/.agents/skills/feature-flags/SKILL.md +169 -0
  226. package/src/templates/workspace-core/AGENTS.md +1 -1
  227. package/src/templates/workspace-root/AGENTS.md +1 -1
  228. package/corpus/templates/clips/actions/get-feature-flags.ts +0 -30
@@ -0,0 +1,519 @@
1
+ import {
2
+ AlertDialog,
3
+ AlertDialogAction,
4
+ AlertDialogCancel,
5
+ AlertDialogContent,
6
+ AlertDialogDescription,
7
+ AlertDialogFooter,
8
+ AlertDialogHeader,
9
+ AlertDialogTitle,
10
+ } from "@agent-native/toolkit/ui/alert-dialog";
11
+ import { Badge } from "@agent-native/toolkit/ui/badge";
12
+ import { Button } from "@agent-native/toolkit/ui/button";
13
+ import {
14
+ DropdownMenu,
15
+ DropdownMenuContent,
16
+ DropdownMenuGroup,
17
+ DropdownMenuItem,
18
+ DropdownMenuTrigger,
19
+ } from "@agent-native/toolkit/ui/dropdown-menu";
20
+ import { Input } from "@agent-native/toolkit/ui/input";
21
+ import { Label } from "@agent-native/toolkit/ui/label";
22
+ import {
23
+ Select,
24
+ SelectContent,
25
+ SelectGroup,
26
+ SelectItem,
27
+ SelectTrigger,
28
+ SelectValue,
29
+ } from "@agent-native/toolkit/ui/select";
30
+ import { Textarea } from "@agent-native/toolkit/ui/textarea";
31
+ import {
32
+ IconCheck,
33
+ IconDots,
34
+ IconSettings,
35
+ IconLoader2,
36
+ IconUserCheck,
37
+ } from "@tabler/icons-react";
38
+ import { useMemo, useState } from "react";
39
+
40
+ import {
41
+ Dialog,
42
+ DialogContent,
43
+ DialogDescription,
44
+ DialogFooter,
45
+ DialogHeader,
46
+ DialogTitle,
47
+ } from "../components/ui/dialog.js";
48
+ import { useT } from "../i18n.js";
49
+ import {
50
+ normalizeFeatureFlagPercentage,
51
+ normalizeFeatureFlagRules,
52
+ } from "./helpers.js";
53
+ import type {
54
+ FeatureFlagMetadata,
55
+ FeatureFlagRules,
56
+ SetFeatureFlagInput,
57
+ } from "./types.js";
58
+
59
+ function formatActor(actor: FeatureFlagRules["updatedBy"]): string | null {
60
+ if (!actor) return null;
61
+ if (typeof actor === "string") return actor;
62
+ return actor.name ?? actor.email ?? null;
63
+ }
64
+
65
+ function formatWhen(value: number | null | undefined): string | null {
66
+ if (!value) return null;
67
+ const date = new Date(value);
68
+ if (Number.isNaN(date.valueOf())) return null;
69
+ return new Intl.DateTimeFormat(undefined, {
70
+ dateStyle: "medium",
71
+ timeStyle: "short",
72
+ }).format(date);
73
+ }
74
+
75
+ function rolloutLabel(
76
+ rules: FeatureFlagRules,
77
+ t: ReturnType<typeof useT>,
78
+ ): string {
79
+ if (rules.mode === "off") return t("featureFlags.off");
80
+ if (rules.mode === "on") return t("featureFlags.everyone");
81
+ const parts = [
82
+ rules.emails.length
83
+ ? t("featureFlags.emailCount", { count: rules.emails.length })
84
+ : null,
85
+ rules.orgIds.length
86
+ ? t("featureFlags.organizationCount", { count: rules.orgIds.length })
87
+ : null,
88
+ rules.percentage
89
+ ? t("featureFlags.percentageRollout", { count: rules.percentage })
90
+ : null,
91
+ ].filter(Boolean);
92
+ return parts.join(" · ") || t("featureFlags.inherited");
93
+ }
94
+
95
+ function audienceLabel(
96
+ rules: FeatureFlagRules,
97
+ t: ReturnType<typeof useT>,
98
+ ): string {
99
+ if (rules.mode === "off") return t("featureFlags.off");
100
+ if (rules.mode === "on") return t("featureFlags.everyoneAudience");
101
+ return rolloutLabel(rules, t);
102
+ }
103
+
104
+ function modeLabel(rules: FeatureFlagRules, t: ReturnType<typeof useT>) {
105
+ if (rules.mode === "off") return t("featureFlags.off");
106
+ if (rules.mode === "on") return t("featureFlags.everyone");
107
+ return t("featureFlags.targeted");
108
+ }
109
+
110
+ function listText(values: string[]): string {
111
+ return values.join("\n");
112
+ }
113
+
114
+ function parseList(value: string): string[] {
115
+ return [
116
+ ...new Set(
117
+ value
118
+ .split(/[\n,]/)
119
+ .map((entry) => entry.trim())
120
+ .filter(Boolean),
121
+ ),
122
+ ];
123
+ }
124
+
125
+ function TargetingDialog({
126
+ flag,
127
+ open,
128
+ onOpenChange,
129
+ onMutate,
130
+ isPending,
131
+ }: {
132
+ flag: FeatureFlagMetadata;
133
+ open: boolean;
134
+ onOpenChange: (open: boolean) => void;
135
+ onMutate: (input: SetFeatureFlagInput) => void;
136
+ isPending?: boolean;
137
+ }) {
138
+ const t = useT();
139
+ const modeId = `feature-flag-${flag.key}-mode`;
140
+ const emailsId = `feature-flag-${flag.key}-emails`;
141
+ const orgIdsId = `feature-flag-${flag.key}-org-ids`;
142
+ const percentageId = `feature-flag-${flag.key}-percentage`;
143
+ const [mode, setMode] = useState(flag.rules.mode);
144
+ const [emails, setEmails] = useState(() => listText(flag.rules.emails));
145
+ const [orgIds, setOrgIds] = useState(() => listText(flag.rules.orgIds));
146
+ const [percentage, setPercentage] = useState(String(flag.rules.percentage));
147
+
148
+ const save = () => {
149
+ const nextPercentage = normalizeFeatureFlagPercentage(percentage);
150
+ onMutate({
151
+ key: flag.key,
152
+ operation: "replace-rules",
153
+ rules: {
154
+ version: 1,
155
+ mode,
156
+ emails: parseList(emails),
157
+ orgIds: parseList(orgIds),
158
+ percentage: nextPercentage,
159
+ },
160
+ });
161
+ };
162
+
163
+ return (
164
+ <Dialog open={open} onOpenChange={onOpenChange}>
165
+ <DialogContent className="max-w-xl">
166
+ <DialogHeader>
167
+ <DialogTitle>
168
+ {t("featureFlags.targetingTitle", {
169
+ name: flag.displayName ?? flag.key,
170
+ })}
171
+ </DialogTitle>
172
+ <DialogDescription>
173
+ {t("featureFlags.targetingDescription")}
174
+ </DialogDescription>
175
+ </DialogHeader>
176
+ <div className="grid gap-2">
177
+ <Label htmlFor={modeId}>{t("featureFlags.modeLabel")}</Label>
178
+ <Select
179
+ value={mode}
180
+ onValueChange={(value) =>
181
+ setMode(value as FeatureFlagRules["mode"])
182
+ }
183
+ >
184
+ <SelectTrigger id={modeId}>
185
+ <SelectValue />
186
+ </SelectTrigger>
187
+ <SelectContent>
188
+ <SelectGroup>
189
+ <SelectItem value="off">{t("featureFlags.off")}</SelectItem>
190
+ <SelectItem value="rules">
191
+ {t("featureFlags.targeted")}
192
+ </SelectItem>
193
+ <SelectItem value="on">{t("featureFlags.everyone")}</SelectItem>
194
+ </SelectGroup>
195
+ </SelectContent>
196
+ </Select>
197
+ </div>
198
+ {mode === "rules" ? (
199
+ <div className="grid gap-4">
200
+ <div className="grid gap-2">
201
+ <Label htmlFor={emailsId}>{t("featureFlags.emailsLabel")}</Label>
202
+ <Textarea
203
+ id={emailsId}
204
+ className="min-h-20"
205
+ value={emails}
206
+ onChange={(event) => setEmails(event.target.value)}
207
+ placeholder="one@example.com"
208
+ />
209
+ </div>
210
+ <div className="grid gap-2">
211
+ <Label htmlFor={orgIdsId}>{t("featureFlags.orgIdsLabel")}</Label>
212
+ <Textarea
213
+ id={orgIdsId}
214
+ className="min-h-20"
215
+ value={orgIds}
216
+ onChange={(event) => setOrgIds(event.target.value)}
217
+ placeholder="org_123"
218
+ />
219
+ </div>
220
+ <div className="grid gap-2">
221
+ <Label htmlFor={percentageId}>
222
+ {t("featureFlags.percentageLabel")}
223
+ </Label>
224
+ <Input
225
+ id={percentageId}
226
+ type="number"
227
+ min="0"
228
+ max="100"
229
+ step="1"
230
+ value={percentage}
231
+ onChange={(event) => setPercentage(event.target.value)}
232
+ />
233
+ </div>
234
+ <p className="text-xs text-muted-foreground">
235
+ {t("featureFlags.targetingRuleHelp")}
236
+ </p>
237
+ </div>
238
+ ) : null}
239
+ <div className="rounded-md bg-muted/50 px-3 py-2 text-sm">
240
+ {t("featureFlags.targetingSummary", {
241
+ audience: audienceLabel(
242
+ normalizeFeatureFlagRules({
243
+ mode,
244
+ emails: parseList(emails),
245
+ orgIds: parseList(orgIds),
246
+ percentage: normalizeFeatureFlagPercentage(percentage),
247
+ }),
248
+ t,
249
+ ),
250
+ })}
251
+ <span className="ms-2 text-xs text-muted-foreground">
252
+ ·{" "}
253
+ {flag.defaultValue
254
+ ? t("featureFlags.defaultOn")
255
+ : t("featureFlags.defaultOff")}
256
+ </span>
257
+ </div>
258
+ <DialogFooter>
259
+ <Button
260
+ type="button"
261
+ variant="ghost"
262
+ onClick={() => onOpenChange(false)}
263
+ >
264
+ {t("featureFlags.cancel")}
265
+ </Button>
266
+ <Button type="button" disabled={isPending} onClick={save}>
267
+ {isPending ? <IconLoader2 className="animate-spin" /> : null}
268
+ {t("featureFlags.saveRules")}
269
+ </Button>
270
+ </DialogFooter>
271
+ </DialogContent>
272
+ </Dialog>
273
+ );
274
+ }
275
+
276
+ function FeatureFlagRow({
277
+ flag,
278
+ onMutate,
279
+ isPending,
280
+ error,
281
+ }: {
282
+ flag: FeatureFlagMetadata;
283
+ onMutate: (input: SetFeatureFlagInput) => void;
284
+ isPending?: boolean;
285
+ error?: Error | null;
286
+ }) {
287
+ const t = useT();
288
+ const [targetingOpen, setTargetingOpen] = useState(false);
289
+ const [disableOpen, setDisableOpen] = useState(false);
290
+ const actor = formatActor(flag.rules.updatedBy);
291
+ const when = formatWhen(flag.rules.updatedAt);
292
+ const metadata = [actor, when].filter(Boolean).join(" · ");
293
+
294
+ return (
295
+ <article
296
+ id={`feature-flag-${flag.key}`}
297
+ className="grid gap-4 scroll-mt-24 border-b border-border py-5 last:border-b-0 md:grid-cols-[minmax(0,1fr)_auto] md:items-center"
298
+ >
299
+ <div className="min-w-0">
300
+ <div className="flex min-w-0 items-center gap-2">
301
+ <h3 className="truncate text-sm font-medium text-foreground">
302
+ {flag.displayName ?? flag.key}
303
+ </h3>
304
+ {flag.displayName && flag.displayName !== flag.key ? (
305
+ <code className="truncate text-xs text-muted-foreground">
306
+ {flag.key}
307
+ </code>
308
+ ) : null}
309
+ <Badge
310
+ variant={flag.rules.mode === "on" ? "secondary" : "outline"}
311
+ className="ms-auto shrink-0"
312
+ >
313
+ {modeLabel(flag.rules, t)}
314
+ </Badge>
315
+ </div>
316
+ {flag.description ? (
317
+ <p className="mt-1 text-sm text-muted-foreground">
318
+ {flag.description}
319
+ </p>
320
+ ) : null}
321
+ <div className="mt-3 flex flex-wrap gap-x-3 gap-y-1 text-xs text-muted-foreground">
322
+ <span>
323
+ {t("featureFlags.enabledFor")}: {audienceLabel(flag.rules, t)}
324
+ </span>
325
+ {metadata ? (
326
+ <span>{t("featureFlags.lastChanged", { metadata })}</span>
327
+ ) : null}
328
+ </div>
329
+ <div className="mt-2 flex items-center gap-1.5 text-xs font-medium text-foreground">
330
+ {flag.enabledForCurrentUser === true ? (
331
+ <>
332
+ <IconCheck className="size-4 text-emerald-500" />
333
+ {t("featureFlags.youHaveAccess")}
334
+ </>
335
+ ) : flag.enabledForCurrentUser === false ? (
336
+ <>{t("featureFlags.youDoNotHaveAccess")}</>
337
+ ) : (
338
+ <>{t("featureFlags.currentUserUnknown")}</>
339
+ )}
340
+ </div>
341
+ {error ? (
342
+ <p className="mt-3 text-sm text-destructive">
343
+ {t("featureFlags.mutationUnverified", {
344
+ name: flag.displayName ?? flag.key,
345
+ })}
346
+ </p>
347
+ ) : null}
348
+ </div>
349
+ <div className="flex flex-wrap items-center gap-2 md:justify-end">
350
+ <Button
351
+ type="button"
352
+ variant="outline"
353
+ size="sm"
354
+ className="text-xs"
355
+ disabled={isPending || flag.enabledForCurrentUser === true}
356
+ onClick={() =>
357
+ onMutate({
358
+ key: flag.key,
359
+ operation: "enable-for-current-user",
360
+ })
361
+ }
362
+ >
363
+ {flag.enabledForCurrentUser === true ? (
364
+ <IconCheck />
365
+ ) : (
366
+ <IconUserCheck />
367
+ )}
368
+ {flag.enabledForCurrentUser === true
369
+ ? t("featureFlags.enabledForMe")
370
+ : t("featureFlags.enableForMe")}
371
+ </Button>
372
+ <Button
373
+ type="button"
374
+ variant="outline"
375
+ size="sm"
376
+ className="text-xs"
377
+ disabled={isPending}
378
+ onClick={() => setTargetingOpen(true)}
379
+ >
380
+ <IconSettings />
381
+ {t("featureFlags.editTargeting")}
382
+ </Button>
383
+ <DropdownMenu>
384
+ <DropdownMenuTrigger asChild>
385
+ <Button
386
+ type="button"
387
+ variant="ghost"
388
+ size="icon"
389
+ className="size-8 text-muted-foreground"
390
+ disabled={isPending}
391
+ aria-label={t("featureFlags.moreActions", {
392
+ name: flag.displayName ?? flag.key,
393
+ })}
394
+ >
395
+ <IconDots />
396
+ </Button>
397
+ </DropdownMenuTrigger>
398
+ <DropdownMenuContent align="end">
399
+ <DropdownMenuGroup>
400
+ <DropdownMenuItem
401
+ className="text-destructive focus:text-destructive"
402
+ disabled={flag.rules.mode === "off"}
403
+ onSelect={() => setDisableOpen(true)}
404
+ >
405
+ {t("featureFlags.disableForEveryone")}
406
+ </DropdownMenuItem>
407
+ </DropdownMenuGroup>
408
+ </DropdownMenuContent>
409
+ </DropdownMenu>
410
+ </div>
411
+ {targetingOpen ? (
412
+ <TargetingDialog
413
+ flag={flag}
414
+ open
415
+ onOpenChange={setTargetingOpen}
416
+ onMutate={onMutate}
417
+ isPending={isPending}
418
+ />
419
+ ) : null}
420
+ <AlertDialog open={disableOpen} onOpenChange={setDisableOpen}>
421
+ <AlertDialogContent>
422
+ <AlertDialogHeader>
423
+ <AlertDialogTitle>
424
+ {t("featureFlags.disableForEveryoneTitle", {
425
+ name: flag.displayName ?? flag.key,
426
+ })}
427
+ </AlertDialogTitle>
428
+ <AlertDialogDescription>
429
+ {t("featureFlags.disableForEveryoneDescription")}
430
+ </AlertDialogDescription>
431
+ </AlertDialogHeader>
432
+ <AlertDialogFooter>
433
+ <AlertDialogCancel>{t("featureFlags.cancel")}</AlertDialogCancel>
434
+ <AlertDialogAction
435
+ className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
436
+ onClick={() => onMutate({ key: flag.key, operation: "off" })}
437
+ >
438
+ {t("featureFlags.disableForEveryone")}
439
+ </AlertDialogAction>
440
+ </AlertDialogFooter>
441
+ </AlertDialogContent>
442
+ </AlertDialog>
443
+ </article>
444
+ );
445
+ }
446
+
447
+ export function FeatureFlagsEditor({
448
+ flags,
449
+ onMutate,
450
+ isPending,
451
+ error,
452
+ errorFlagKey,
453
+ showHeader = true,
454
+ }: {
455
+ flags: FeatureFlagMetadata[];
456
+ onMutate: (input: SetFeatureFlagInput) => void;
457
+ isPending?: boolean;
458
+ error?: Error | null;
459
+ errorFlagKey?: string | null;
460
+ showHeader?: boolean;
461
+ }) {
462
+ const t = useT();
463
+ const sortedFlags = useMemo(
464
+ () =>
465
+ flags
466
+ .map((flag) => ({
467
+ ...flag,
468
+ rules: normalizeFeatureFlagRules(flag.rules),
469
+ }))
470
+ .sort((left, right) => left.key.localeCompare(right.key)),
471
+ [flags],
472
+ );
473
+
474
+ return (
475
+ <section
476
+ className={
477
+ showHeader ? "mx-auto w-full max-w-2xl" : "mx-auto w-full max-w-4xl"
478
+ }
479
+ aria-labelledby={showHeader ? "feature-flags-title" : undefined}
480
+ aria-label={showHeader ? undefined : t("featureFlags.title")}
481
+ >
482
+ {showHeader ? (
483
+ <header className="border-b border-border pb-5">
484
+ <h2
485
+ id="feature-flags-title"
486
+ className="text-base font-semibold text-foreground"
487
+ >
488
+ {t("featureFlags.title")}
489
+ </h2>
490
+ <p className="mt-1 text-sm text-muted-foreground">
491
+ {t("featureFlags.description")}
492
+ </p>
493
+ </header>
494
+ ) : null}
495
+ {sortedFlags.length ? (
496
+ <div>
497
+ {sortedFlags.map((flag) => (
498
+ <FeatureFlagRow
499
+ key={flag.key}
500
+ flag={flag}
501
+ onMutate={onMutate}
502
+ isPending={isPending}
503
+ error={errorFlagKey === flag.key ? error : null}
504
+ />
505
+ ))}
506
+ </div>
507
+ ) : (
508
+ <p className="py-8 text-sm text-muted-foreground">
509
+ {t("featureFlags.noFlags")}
510
+ </p>
511
+ )}
512
+ {error && !errorFlagKey ? (
513
+ <p className="pt-3 text-sm text-destructive">
514
+ {t("featureFlags.mutationUnverifiedGeneric")}
515
+ </p>
516
+ ) : null}
517
+ </section>
518
+ );
519
+ }
@@ -0,0 +1,77 @@
1
+ import type { FeatureFlagRules } from "./types.js";
2
+
3
+ export type EvaluatedFeatureFlags =
4
+ | Record<string, boolean>
5
+ | { flags?: Record<string, boolean>; values?: Record<string, boolean> };
6
+
7
+ export function evaluatedFeatureFlagValues(
8
+ result: EvaluatedFeatureFlags | undefined,
9
+ ): Record<string, boolean> {
10
+ if (!result) return {};
11
+ const envelope = result as {
12
+ flags?: unknown;
13
+ values?: unknown;
14
+ };
15
+ if (
16
+ envelope.flags &&
17
+ typeof envelope.flags === "object" &&
18
+ !Array.isArray(envelope.flags)
19
+ ) {
20
+ return envelope.flags as Record<string, boolean>;
21
+ }
22
+ if (
23
+ envelope.values &&
24
+ typeof envelope.values === "object" &&
25
+ !Array.isArray(envelope.values)
26
+ ) {
27
+ return envelope.values as Record<string, boolean>;
28
+ }
29
+ return result as Record<string, boolean>;
30
+ }
31
+
32
+ export function featureFlagValue(
33
+ values: Record<string, boolean>,
34
+ key: string,
35
+ ): boolean {
36
+ return values[key] === true;
37
+ }
38
+
39
+ export function normalizeFeatureFlagPercentage(value: unknown): number {
40
+ const percentage = Number(value);
41
+ return Number.isFinite(percentage)
42
+ ? Math.floor(Math.max(0, Math.min(100, percentage)))
43
+ : 0;
44
+ }
45
+
46
+ /**
47
+ * Keep the shared editor safe while a remote app is upgrading or returning an
48
+ * optimistic/transient rule envelope. The fleet contract still validates the
49
+ * authoritative response; this only prevents absent collection fields from
50
+ * crashing the operator UI between refreshes.
51
+ */
52
+ export function normalizeFeatureFlagRules(
53
+ rules: Partial<FeatureFlagRules> | null | undefined,
54
+ ): FeatureFlagRules {
55
+ const mode =
56
+ rules?.mode === "off" || rules?.mode === "on" || rules?.mode === "rules"
57
+ ? rules.mode
58
+ : "rules";
59
+ const percentage = normalizeFeatureFlagPercentage(rules?.percentage);
60
+
61
+ return {
62
+ ...rules,
63
+ version: 1,
64
+ mode,
65
+ emails: Array.isArray(rules?.emails)
66
+ ? rules.emails.filter(
67
+ (value): value is string => typeof value === "string",
68
+ )
69
+ : [],
70
+ orgIds: Array.isArray(rules?.orgIds)
71
+ ? rules.orgIds.filter(
72
+ (value): value is string => typeof value === "string",
73
+ )
74
+ : [],
75
+ percentage,
76
+ };
77
+ }
@@ -0,0 +1,13 @@
1
+ export { FeatureFlagsEditor } from "./FeatureFlagsPanel.js";
2
+ export {
3
+ evaluatedFeatureFlagValues,
4
+ featureFlagValue,
5
+ type EvaluatedFeatureFlags,
6
+ } from "./helpers.js";
7
+ export { useFeatureFlag, useFeatureFlags } from "./use-feature-flag.js";
8
+ export type {
9
+ FeatureFlagActor,
10
+ FeatureFlagMetadata,
11
+ FeatureFlagRules,
12
+ SetFeatureFlagInput,
13
+ } from "./types.js";
@@ -0,0 +1,29 @@
1
+ export interface FeatureFlagActor {
2
+ name?: string | null;
3
+ email?: string | null;
4
+ }
5
+
6
+ export interface FeatureFlagMetadata {
7
+ key: string;
8
+ displayName?: string | null;
9
+ description?: string | null;
10
+ defaultValue: boolean;
11
+ rules: FeatureFlagRules;
12
+ enabledForCurrentUser?: boolean;
13
+ }
14
+
15
+ export interface FeatureFlagRules {
16
+ version?: 1;
17
+ mode: "off" | "on" | "rules";
18
+ emails: string[];
19
+ orgIds: string[];
20
+ percentage: number;
21
+ updatedAt?: number | null;
22
+ updatedBy?: FeatureFlagActor | string | null;
23
+ }
24
+
25
+ export interface SetFeatureFlagInput {
26
+ key: string;
27
+ operation: "off" | "enable-for-current-user" | "replace-rules";
28
+ rules?: FeatureFlagRules;
29
+ }
@@ -0,0 +1,26 @@
1
+ import { useActionQuery } from "../use-action.js";
2
+ import {
3
+ evaluatedFeatureFlagValues,
4
+ featureFlagValue,
5
+ type EvaluatedFeatureFlags,
6
+ } from "./helpers.js";
7
+
8
+ export type { EvaluatedFeatureFlags } from "./helpers.js";
9
+
10
+ /**
11
+ * Returns the current user's evaluated value for a registered feature flag.
12
+ * Flags that have not been registered evaluate to false.
13
+ */
14
+ export function useFeatureFlag(key: string): boolean {
15
+ const query = useActionQuery<EvaluatedFeatureFlags>(
16
+ "get-feature-flags" as never,
17
+ );
18
+ return featureFlagValue(evaluatedFeatureFlagValues(query.data), key);
19
+ }
20
+
21
+ export function useFeatureFlags(): Record<string, boolean> {
22
+ const query = useActionQuery<EvaluatedFeatureFlags>(
23
+ "get-feature-flags" as never,
24
+ );
25
+ return evaluatedFeatureFlagValues(query.data);
26
+ }
@@ -698,6 +698,17 @@ export {
698
698
  type DataWidgetResultMetadata,
699
699
  } from "./chat/widgets/data-widget-types.js";
700
700
  export { AgentNativeIcon } from "./components/icons/AgentNativeIcon.js";
701
+ export {
702
+ FeatureFlagsEditor,
703
+ evaluatedFeatureFlagValues,
704
+ featureFlagValue,
705
+ useFeatureFlag,
706
+ useFeatureFlags,
707
+ type FeatureFlagActor,
708
+ type FeatureFlagMetadata,
709
+ type FeatureFlagRules,
710
+ type SetFeatureFlagInput,
711
+ } from "./feature-flags/index.js";
701
712
  export {
702
713
  SettingsPanel,
703
714
  SettingsTabsPage,
@@ -982,7 +982,7 @@ export const DESTRUCTIVE_SQL_RE =
982
982
  // integrations, notifications, scheduling, sharing/orgs), and Postgres
983
983
  // catalogs that would let a extension enumerate or read internals.
984
984
  export const SENSITIVE_SQL_RE =
985
- /\b(app_secrets|user|users|session|sessions|account|accounts|verification|oauth_tokens|tools|extensions|tool_shares|tool_slots|tool_slot_installs|tool_hidden_extensions|tool_history|member|organization|invitation|jwks|agent_trace_spans|agent_trace_summaries|agent_feedback|agent_satisfaction_scores|agent_evals|agent_runs|agent_run_events|notifications|progress_runs|integration_configs|integration_pending_tasks|integration_thread_mappings|resources|org_members|org_invitations|bigquery_cache|dashboard_views|pg_catalog|information_schema|pg_class|pg_proc|pg_namespace|pg_user|pg_roles|pg_authid|pg_shadow)\b/i;
985
+ /\b(app_secrets|settings|user|users|session|sessions|account|accounts|verification|oauth_tokens|tools|extensions|tool_shares|tool_slots|tool_slot_installs|tool_hidden_extensions|tool_history|member|organization|invitation|jwks|agent_trace_spans|agent_trace_summaries|agent_feedback|agent_satisfaction_scores|agent_evals|agent_runs|agent_run_events|notifications|progress_runs|integration_configs|integration_pending_tasks|integration_thread_mappings|resources|org_members|org_invitations|bigquery_cache|dashboard_views|pg_catalog|information_schema|pg_class|pg_proc|pg_namespace|pg_user|pg_roles|pg_authid|pg_shadow)\b/i;
986
986
 
987
987
  // Refuses positional INSERTs (no column list). `INSERT INTO recordings VALUES
988
988
  // (...)` would let a extension stuff arbitrary owner_email values into a row.