@agent-native/dispatch 0.23.5 → 0.24.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 (224) hide show
  1. package/dist/actions/list-connected-agents.d.ts +1 -0
  2. package/dist/actions/list-connected-agents.js +5 -1
  3. package/dist/actions/list-connected-agents.js.map +1 -1
  4. package/dist/actions/list-dispatch-usage-metrics.d.ts +2 -0
  5. package/dist/actions/list-dispatch-usage-metrics.js +11 -1
  6. package/dist/actions/list-dispatch-usage-metrics.js.map +1 -1
  7. package/dist/actions/provider-api-register.d.ts +12 -12
  8. package/dist/actions/remix-workspace-template.js +4 -3
  9. package/dist/actions/remix-workspace-template.js.map +1 -1
  10. package/dist/actions/scaffold-workspace-app.js +3 -2
  11. package/dist/actions/scaffold-workspace-app.js.map +1 -1
  12. package/dist/actions/start-workspace-app-creation.js +3 -2
  13. package/dist/actions/start-workspace-app-creation.js.map +1 -1
  14. package/dist/actions/view-screen.js +11 -1
  15. package/dist/actions/view-screen.js.map +1 -1
  16. package/dist/components/admin-navigation.d.ts.map +1 -1
  17. package/dist/components/admin-navigation.js +21 -5
  18. package/dist/components/admin-navigation.js.map +1 -1
  19. package/dist/components/app-icon.js +1 -1
  20. package/dist/components/app-icon.js.map +1 -1
  21. package/dist/components/app-list-row.d.ts +1 -1
  22. package/dist/components/app-list-row.d.ts.map +1 -1
  23. package/dist/components/app-list-row.js +1 -1
  24. package/dist/components/app-list-row.js.map +1 -1
  25. package/dist/components/app-open-actions.d.ts +1 -17
  26. package/dist/components/app-open-actions.d.ts.map +1 -1
  27. package/dist/components/app-open-actions.js +1 -20
  28. package/dist/components/app-open-actions.js.map +1 -1
  29. package/dist/components/app-row-settings.d.ts +6 -0
  30. package/dist/components/app-row-settings.d.ts.map +1 -0
  31. package/dist/components/app-row-settings.js +11 -0
  32. package/dist/components/app-row-settings.js.map +1 -0
  33. package/dist/components/automation-run-history-dialog.d.ts.map +1 -1
  34. package/dist/components/automation-run-history-dialog.js +3 -2
  35. package/dist/components/automation-run-history-dialog.js.map +1 -1
  36. package/dist/components/connected-app-card.d.ts.map +1 -1
  37. package/dist/components/connected-app-card.js +3 -2
  38. package/dist/components/connected-app-card.js.map +1 -1
  39. package/dist/components/create-app-popover.js +2 -2
  40. package/dist/components/create-app-popover.js.map +1 -1
  41. package/dist/components/dispatch-control-plane.d.ts.map +1 -1
  42. package/dist/components/dispatch-control-plane.js +23 -7
  43. package/dist/components/dispatch-control-plane.js.map +1 -1
  44. package/dist/components/dispatch-shell.d.ts.map +1 -1
  45. package/dist/components/dispatch-shell.js +5 -1
  46. package/dist/components/dispatch-shell.js.map +1 -1
  47. package/dist/components/docs-link.d.ts +16 -0
  48. package/dist/components/docs-link.d.ts.map +1 -0
  49. package/dist/components/docs-link.js +34 -0
  50. package/dist/components/docs-link.js.map +1 -0
  51. package/dist/components/layout/Layout.d.ts +1 -0
  52. package/dist/components/layout/Layout.d.ts.map +1 -1
  53. package/dist/components/layout/Layout.js +43 -35
  54. package/dist/components/layout/Layout.js.map +1 -1
  55. package/dist/components/messaging-setup-panel.d.ts.map +1 -1
  56. package/dist/components/messaging-setup-panel.js +3 -2
  57. package/dist/components/messaging-setup-panel.js.map +1 -1
  58. package/dist/components/usage-alerts-panel.d.ts +11 -0
  59. package/dist/components/usage-alerts-panel.d.ts.map +1 -0
  60. package/dist/components/usage-alerts-panel.js +125 -0
  61. package/dist/components/usage-alerts-panel.js.map +1 -0
  62. package/dist/components/workspace-app-card.d.ts +3 -1
  63. package/dist/components/workspace-app-card.d.ts.map +1 -1
  64. package/dist/components/workspace-app-card.js +70 -29
  65. package/dist/components/workspace-app-card.js.map +1 -1
  66. package/dist/components/workspace-app-search.d.ts +10 -0
  67. package/dist/components/workspace-app-search.d.ts.map +1 -0
  68. package/dist/components/workspace-app-search.js +27 -0
  69. package/dist/components/workspace-app-search.js.map +1 -0
  70. package/dist/components/workspace-resource-effective-stack.d.ts.map +1 -1
  71. package/dist/components/workspace-resource-effective-stack.js +3 -2
  72. package/dist/components/workspace-resource-effective-stack.js.map +1 -1
  73. package/dist/components/workspace-template-card.d.ts.map +1 -1
  74. package/dist/components/workspace-template-card.js +10 -15
  75. package/dist/components/workspace-template-card.js.map +1 -1
  76. package/dist/hooks/use-navigation-state.d.ts +2 -0
  77. package/dist/hooks/use-navigation-state.d.ts.map +1 -1
  78. package/dist/hooks/use-navigation-state.js +10 -0
  79. package/dist/hooks/use-navigation-state.js.map +1 -1
  80. package/dist/lib/embed-session-recovery.d.ts +3 -0
  81. package/dist/lib/embed-session-recovery.d.ts.map +1 -0
  82. package/dist/lib/embed-session-recovery.js +18 -0
  83. package/dist/lib/embed-session-recovery.js.map +1 -0
  84. package/dist/lib/other-apps.d.ts +2 -0
  85. package/dist/lib/other-apps.d.ts.map +1 -1
  86. package/dist/lib/other-apps.js.map +1 -1
  87. package/dist/lib/workspace-app-layout.d.ts +19 -0
  88. package/dist/lib/workspace-app-layout.d.ts.map +1 -0
  89. package/dist/lib/workspace-app-layout.js +75 -0
  90. package/dist/lib/workspace-app-layout.js.map +1 -0
  91. package/dist/lib/workspace-apps.d.ts +14 -0
  92. package/dist/lib/workspace-apps.d.ts.map +1 -1
  93. package/dist/lib/workspace-apps.js +66 -0
  94. package/dist/lib/workspace-apps.js.map +1 -1
  95. package/dist/lib/workspace-apps.test.d.ts +2 -0
  96. package/dist/lib/workspace-apps.test.d.ts.map +1 -0
  97. package/dist/lib/workspace-apps.test.js +33 -0
  98. package/dist/lib/workspace-apps.test.js.map +1 -0
  99. package/dist/routes/pages/$appId.d.ts.map +1 -1
  100. package/dist/routes/pages/$appId.js +4 -2
  101. package/dist/routes/pages/$appId.js.map +1 -1
  102. package/dist/routes/pages/agents.d.ts.map +1 -1
  103. package/dist/routes/pages/agents.js +2 -3
  104. package/dist/routes/pages/agents.js.map +1 -1
  105. package/dist/routes/pages/apps.$appId.d.ts.map +1 -1
  106. package/dist/routes/pages/apps.$appId.js +17 -13
  107. package/dist/routes/pages/apps.$appId.js.map +1 -1
  108. package/dist/routes/pages/apps.d.ts.map +1 -1
  109. package/dist/routes/pages/apps.js +31 -16
  110. package/dist/routes/pages/apps.js.map +1 -1
  111. package/dist/routes/pages/audit.d.ts.map +1 -1
  112. package/dist/routes/pages/audit.js +2 -1
  113. package/dist/routes/pages/audit.js.map +1 -1
  114. package/dist/routes/pages/chat.d.ts.map +1 -1
  115. package/dist/routes/pages/chat.js +7 -2
  116. package/dist/routes/pages/chat.js.map +1 -1
  117. package/dist/routes/pages/dreams.d.ts.map +1 -1
  118. package/dist/routes/pages/dreams.js +14 -18
  119. package/dist/routes/pages/dreams.js.map +1 -1
  120. package/dist/routes/pages/integrations.d.ts.map +1 -1
  121. package/dist/routes/pages/integrations.js +51 -14
  122. package/dist/routes/pages/integrations.js.map +1 -1
  123. package/dist/routes/pages/metrics.d.ts.map +1 -1
  124. package/dist/routes/pages/metrics.js +146 -17
  125. package/dist/routes/pages/metrics.js.map +1 -1
  126. package/dist/routes/pages/operations.js +1 -1
  127. package/dist/routes/pages/operations.js.map +1 -1
  128. package/dist/routes/pages/settings.d.ts.map +1 -1
  129. package/dist/routes/pages/settings.js +19 -2
  130. package/dist/routes/pages/settings.js.map +1 -1
  131. package/dist/routes/pages/thread-debug.d.ts.map +1 -1
  132. package/dist/routes/pages/thread-debug.js +5 -8
  133. package/dist/routes/pages/thread-debug.js.map +1 -1
  134. package/dist/routes/pages/vault.d.ts.map +1 -1
  135. package/dist/routes/pages/vault.js +1 -1
  136. package/dist/routes/pages/vault.js.map +1 -1
  137. package/dist/routes/pages/workspace.d.ts.map +1 -1
  138. package/dist/routes/pages/workspace.js +13 -10
  139. package/dist/routes/pages/workspace.js.map +1 -1
  140. package/dist/server/index.d.ts.map +1 -1
  141. package/dist/server/index.js +3 -0
  142. package/dist/server/index.js.map +1 -1
  143. package/dist/server/lib/app-creation-store.d.ts.map +1 -1
  144. package/dist/server/lib/app-creation-store.js +12 -52
  145. package/dist/server/lib/app-creation-store.js.map +1 -1
  146. package/dist/server/lib/usage-metrics-store.d.ts +17 -0
  147. package/dist/server/lib/usage-metrics-store.d.ts.map +1 -1
  148. package/dist/server/lib/usage-metrics-store.js +187 -30
  149. package/dist/server/lib/usage-metrics-store.js.map +1 -1
  150. package/dist/server/plugins/agent-chat.js +2 -0
  151. package/dist/server/plugins/agent-chat.js.map +1 -1
  152. package/dist/server/plugins/integrations.d.ts.map +1 -1
  153. package/dist/server/plugins/integrations.js +1 -0
  154. package/dist/server/plugins/integrations.js.map +1 -1
  155. package/dist/shared/app-roles.d.ts +9 -0
  156. package/dist/shared/app-roles.d.ts.map +1 -0
  157. package/dist/shared/app-roles.js +7 -0
  158. package/dist/shared/app-roles.js.map +1 -0
  159. package/package.json +4 -3
  160. package/src/actions/list-connected-agents.spec.ts +73 -0
  161. package/src/actions/list-connected-agents.ts +6 -2
  162. package/src/actions/list-dispatch-usage-metrics.ts +11 -1
  163. package/src/actions/remix-workspace-template.spec.ts +29 -0
  164. package/src/actions/remix-workspace-template.ts +13 -6
  165. package/src/actions/scaffold-workspace-app.ts +12 -5
  166. package/src/actions/start-workspace-app-creation.ts +8 -2
  167. package/src/actions/view-screen.ts +13 -1
  168. package/src/components/admin-navigation.tsx +57 -11
  169. package/src/components/app-icon.spec.tsx +34 -0
  170. package/src/components/app-icon.tsx +1 -1
  171. package/src/components/app-list-row.tsx +1 -2
  172. package/src/components/app-open-actions.tsx +5 -114
  173. package/src/components/app-row-settings.tsx +54 -0
  174. package/src/components/automation-run-history-dialog.tsx +8 -4
  175. package/src/components/connected-app-card.tsx +3 -2
  176. package/src/components/create-app-popover.spec.tsx +1 -1
  177. package/src/components/create-app-popover.tsx +2 -2
  178. package/src/components/dispatch-control-plane.spec.tsx +44 -3
  179. package/src/components/dispatch-control-plane.tsx +83 -27
  180. package/src/components/dispatch-shell.tsx +7 -1
  181. package/src/components/docs-link.tsx +52 -0
  182. package/src/components/layout/Layout.spec.tsx +13 -0
  183. package/src/components/layout/Layout.tsx +63 -32
  184. package/src/components/messaging-setup-panel.tsx +10 -3
  185. package/src/components/usage-alerts-panel.tsx +445 -0
  186. package/src/components/workspace-app-card.spec.tsx +130 -9
  187. package/src/components/workspace-app-card.tsx +128 -71
  188. package/src/components/workspace-app-search.tsx +96 -0
  189. package/src/components/workspace-resource-effective-stack.tsx +10 -5
  190. package/src/components/workspace-template-card.spec.tsx +53 -24
  191. package/src/components/workspace-template-card.tsx +16 -26
  192. package/src/hooks/use-navigation-state.ts +12 -0
  193. package/src/lib/embed-session-recovery.spec.ts +63 -0
  194. package/src/lib/embed-session-recovery.ts +26 -0
  195. package/src/lib/other-apps.ts +2 -0
  196. package/src/lib/workspace-app-layout.spec.ts +47 -0
  197. package/src/lib/workspace-app-layout.ts +121 -0
  198. package/src/lib/workspace-apps.test.ts +64 -0
  199. package/src/lib/workspace-apps.ts +82 -0
  200. package/src/routes/pages/$appId.tsx +3 -6
  201. package/src/routes/pages/agents.tsx +6 -3
  202. package/src/routes/pages/apps.$appId.tsx +16 -17
  203. package/src/routes/pages/apps.tsx +116 -62
  204. package/src/routes/pages/audit.tsx +28 -8
  205. package/src/routes/pages/chat.spec.tsx +10 -0
  206. package/src/routes/pages/chat.tsx +8 -0
  207. package/src/routes/pages/dreams.tsx +92 -142
  208. package/src/routes/pages/integrations.tsx +147 -68
  209. package/src/routes/pages/metrics.tsx +460 -102
  210. package/src/routes/pages/operations.tsx +2 -2
  211. package/src/routes/pages/settings.tsx +27 -10
  212. package/src/routes/pages/thread-debug.tsx +18 -36
  213. package/src/routes/pages/vault.tsx +7 -2
  214. package/src/routes/pages/workspace.tsx +92 -56
  215. package/src/server/index.ts +4 -0
  216. package/src/server/lib/app-creation-store.spec.ts +90 -43
  217. package/src/server/lib/app-creation-store.ts +15 -61
  218. package/src/server/lib/usage-metrics-store.spec.ts +117 -0
  219. package/src/server/lib/usage-metrics-store.ts +249 -30
  220. package/src/server/plugins/agent-chat.spec.ts +4 -0
  221. package/src/server/plugins/agent-chat.ts +2 -0
  222. package/src/server/plugins/integrations.ts +1 -0
  223. package/src/shared/app-roles.ts +8 -0
  224. package/src/styles/dispatch.css +9 -0
@@ -10,6 +10,7 @@ import {
10
10
  SettingsTabsPage,
11
11
  useAgentSettingsTabs,
12
12
  } from "@agent-native/core/client/settings";
13
+ import { IconShield } from "@tabler/icons-react";
13
14
  import { useState } from "react";
14
15
  import { Link } from "react-router";
15
16
 
@@ -25,6 +26,7 @@ import {
25
26
  } from "../../components/ui/card";
26
27
  import { Label } from "../../components/ui/label";
27
28
  import { Switch } from "../../components/ui/switch";
29
+ import { dispatchAccessDescriptor } from "../../shared/app-roles.js";
28
30
 
29
31
  export function meta() {
30
32
  return [{ title: "Settings - Dispatch" }];
@@ -32,7 +34,30 @@ export function meta() {
32
34
 
33
35
  export default function SettingsRoute() {
34
36
  const t = useT();
35
- const agentSettingsTabs = useAgentSettingsTabs();
37
+ const agentSettingsTabs = useAgentSettingsTabs({
38
+ usageAppId: "dispatch",
39
+ usageViewAllHref: "/admin/metrics",
40
+ organizationContent: (
41
+ <div className="mx-auto w-full max-w-3xl">
42
+ <TeamPage
43
+ showTitle={false}
44
+ appRoles={dispatchAccessDescriptor}
45
+ createOrgDescription="Set up a team to share dispatch destinations and approvals with your colleagues."
46
+ />
47
+ </div>
48
+ ),
49
+ });
50
+ const settingsTabs = [
51
+ ...agentSettingsTabs.filter((tab) => tab.id !== "integrations"),
52
+ {
53
+ id: "admin",
54
+ label: t("dispatch.nav.admin", { defaultValue: "Admin" }),
55
+ icon: IconShield,
56
+ group: "Admin",
57
+ href: "/admin",
58
+ content: null,
59
+ },
60
+ ];
36
61
  const [chatFirstModeState] = useState(() => readChatFirstModeState());
37
62
  const [chatFirstMode, setChatFirstMode] = useState(
38
63
  () => chatFirstModeState.enabled,
@@ -68,7 +93,7 @@ export default function SettingsRoute() {
68
93
  description={t("settings.description")}
69
94
  >
70
95
  <SettingsTabsPage
71
- extraTabs={agentSettingsTabs.filter((tab) => tab.id !== "integrations")}
96
+ extraTabs={settingsTabs}
72
97
  general={
73
98
  <div className="mx-auto w-full max-w-3xl space-y-6">
74
99
  <Card>
@@ -160,14 +185,6 @@ export default function SettingsRoute() {
160
185
  </Card>
161
186
  </div>
162
187
  }
163
- team={
164
- <div className="mx-auto w-full max-w-3xl">
165
- <TeamPage
166
- showTitle={false}
167
- createOrgDescription="Set up a team to share dispatch destinations and approvals with your colleagues."
168
- />
169
- </div>
170
- }
171
188
  whatsNew={
172
189
  <div className="mx-auto w-full max-w-3xl">
173
190
  <ChangelogSettingsCard markdown={changelog} />
@@ -520,20 +520,18 @@ function ResultCard({
520
520
  selected ? "bg-accent/70" : "hover:bg-muted/50",
521
521
  )}
522
522
  >
523
- <div className="flex items-start justify-between gap-3">
524
- <div className="min-w-0">
525
- <div className="truncate text-sm font-medium text-foreground">
526
- {title}
527
- </div>
528
- </div>
529
- <Badge variant="outline" className="shrink-0">
523
+ <div className="truncate text-sm font-medium text-foreground">
524
+ {title}
525
+ </div>
526
+ <div className="mt-1 flex flex-wrap items-center gap-x-2 gap-y-1 text-[11px] text-muted-foreground">
527
+ <span>
530
528
  {result.messageCount}{" "}
531
529
  {result.messageCount === 1 ? "message" : "messages"}
532
- </Badge>
533
- </div>
534
- <div className="mt-2 flex items-center justify-between gap-3 text-[11px] text-muted-foreground">
530
+ </span>
531
+ <span aria-hidden="true">·</span>
535
532
  <span className="min-w-0 truncate">{result.ownerEmail}</span>
536
- <span className="shrink-0">{formatRelativeDate(result.updatedAt)}</span>
533
+ <span aria-hidden="true">·</span>
534
+ <span>{formatRelativeDate(result.updatedAt)}</span>
537
535
  </div>
538
536
  </button>
539
537
  );
@@ -1621,31 +1619,6 @@ export default function ThreadDebugRoute() {
1621
1619
  </SelectContent>
1622
1620
  </Select>
1623
1621
  </div>
1624
- <div className="mt-3 flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
1625
- <span className="font-medium text-foreground">
1626
- {failures.length} loaded{" "}
1627
- {failures.length === 1 ? "run" : "runs"}
1628
- </span>
1629
- <span>·</span>
1630
- <span>
1631
- {failuresData?.access?.scope ??
1632
- t("dispatch.pages.threadDebugCurrentScope", {
1633
- defaultValue: "current scope",
1634
- })}
1635
- </span>
1636
- {failurePatterns.slice(0, 3).map((pattern) => (
1637
- <Badge key={pattern.label} variant="outline">
1638
- {pattern.count} {pattern.label}
1639
- </Badge>
1640
- ))}
1641
- {failuresData?.partial ? (
1642
- <Badge variant="outline">
1643
- {t("dispatch.pages.threadDebugPartialResults", {
1644
- defaultValue: "Partial results",
1645
- })}
1646
- </Badge>
1647
- ) : null}
1648
- </div>
1649
1622
  {unavailableFailureSources.length > 0 ? (
1650
1623
  <div className="mt-2 flex items-start gap-1.5 text-xs text-muted-foreground">
1651
1624
  <IconAlertTriangle className="mt-0.5 size-3.5 shrink-0" />
@@ -1662,6 +1635,15 @@ export default function ThreadDebugRoute() {
1662
1635
  </span>
1663
1636
  </div>
1664
1637
  ) : null}
1638
+ {failuresData?.partial ? (
1639
+ <div className="mt-2">
1640
+ <Badge variant="outline">
1641
+ {t("dispatch.pages.threadDebugPartialResults", {
1642
+ defaultValue: "Partial results",
1643
+ })}
1644
+ </Badge>
1645
+ </div>
1646
+ ) : null}
1665
1647
  </section>
1666
1648
 
1667
1649
  {failuresError ? (
@@ -829,8 +829,13 @@ export default function VaultRoute() {
829
829
 
830
830
  <TabsContent value="secrets" className="mt-4 space-y-3">
831
831
  {!accessReady ? (
832
- <div className="rounded-2xl border border-dashed px-6 py-12 text-center text-sm text-muted-foreground">
833
- Checking workspace access…
832
+ <div className="space-y-3">
833
+ {Array.from({ length: 3 }).map((_, index) => (
834
+ <div key={index} className="rounded-lg bg-card px-4 py-4">
835
+ <Skeleton className="h-4 w-1/3" />
836
+ <Skeleton className="mt-2 h-3 w-2/3" />
837
+ </div>
838
+ ))}
834
839
  </div>
835
840
  ) : !canManageVault ? (
836
841
  <div className="rounded-2xl border border-dashed px-6 py-12 text-center">
@@ -67,7 +67,7 @@ import {
67
67
  } from "../../components/workspace-resource-impact-preview";
68
68
 
69
69
  export function meta() {
70
- return [{ title: "Agent Resources — Dispatch" }];
70
+ return [{ title: "Resources — Dispatch" }];
71
71
  }
72
72
 
73
73
  const KIND_CONFIG = {
@@ -600,7 +600,11 @@ function EffectiveContextPreview({ resource }: { resource: any }) {
600
600
  organization/app override, then personal override.
601
601
  </p>
602
602
  </div>
603
- <Badge variant="outline">{availabilityLabel(availability)}</Badge>
603
+ {isLoading ? (
604
+ <Skeleton className="h-6 w-24 rounded-full" />
605
+ ) : (
606
+ <Badge variant="outline">{availabilityLabel(availability)}</Badge>
607
+ )}
604
608
  </div>
605
609
 
606
610
  <div className="mt-3 grid gap-3 md:grid-cols-2">
@@ -881,14 +885,13 @@ function ResourceRow({ resource, grants }: { resource: any; grants: any[] }) {
881
885
  );
882
886
  }
883
887
 
884
- function GlobalContextSection({ resources }: { resources: any[] }) {
885
- const byPath = new Map(
886
- resources.map((resource) => [resource.path, resource]),
887
- );
888
- const missingPaths = STARTER_GLOBAL_CONTEXT.filter(
889
- (item) => !byPath.has(item.path),
890
- ).map((item) => item.path);
891
- const presentCount = STARTER_GLOBAL_CONTEXT.length - missingPaths.length;
888
+ function GlobalContextSection({
889
+ resources,
890
+ isLoading,
891
+ }: {
892
+ resources: any[];
893
+ isLoading: boolean;
894
+ }) {
892
895
  const restoreStarter = useActionMutation(
893
896
  "restore-starter-workspace-resources",
894
897
  {
@@ -905,6 +908,44 @@ function GlobalContextSection({ resources }: { resources: any[] }) {
905
908
  },
906
909
  );
907
910
 
911
+ if (isLoading && resources.length === 0) {
912
+ return (
913
+ <section className="space-y-3">
914
+ <div>
915
+ <Skeleton className="h-4 w-28" />
916
+ <Skeleton className="mt-2 h-3 w-full max-w-xl" />
917
+ <Skeleton className="mt-1.5 h-3 w-2/3 max-w-lg" />
918
+ </div>
919
+ <div className="overflow-hidden rounded-lg bg-card">
920
+ {Array.from({ length: STARTER_GLOBAL_CONTEXT.length }).map(
921
+ (_, index) => (
922
+ <div
923
+ key={index}
924
+ className="flex items-center gap-3 border-t px-4 py-3 first:border-t-0"
925
+ >
926
+ <Skeleton className="size-4 rounded-full" />
927
+ <div className="min-w-0 flex-1 space-y-1.5">
928
+ <Skeleton className="h-4 w-28" />
929
+ <Skeleton className="h-3 w-64 max-w-full" />
930
+ <Skeleton className="h-3 w-40 max-w-full" />
931
+ </div>
932
+ <Skeleton className="h-6 w-16 rounded-full" />
933
+ </div>
934
+ ),
935
+ )}
936
+ </div>
937
+ </section>
938
+ );
939
+ }
940
+
941
+ const byPath = new Map(
942
+ resources.map((resource) => [resource.path, resource]),
943
+ );
944
+ const missingPaths = STARTER_GLOBAL_CONTEXT.filter(
945
+ (item) => !byPath.has(item.path),
946
+ ).map((item) => item.path);
947
+ const presentCount = STARTER_GLOBAL_CONTEXT.length - missingPaths.length;
948
+
908
949
  return (
909
950
  <section className="space-y-3">
910
951
  <div className="flex items-center justify-between gap-3">
@@ -938,35 +979,44 @@ function GlobalContextSection({ resources }: { resources: any[] }) {
938
979
  </Badge>
939
980
  </div>
940
981
  </div>
941
- <div className="grid gap-3 md:grid-cols-2 xl:grid-cols-5">
982
+ <div className="overflow-hidden rounded-lg bg-card">
942
983
  {STARTER_GLOBAL_CONTEXT.map((item) => {
943
984
  const resource = byPath.get(item.path);
944
985
  const exists = !!resource;
945
986
  const global = resource?.scope === "all";
946
987
  return (
947
- <div key={item.path} className="rounded-lg bg-card p-4">
948
- <div className="flex items-start justify-between gap-3">
949
- <div className="min-w-0">
950
- <div className="flex items-center gap-2">
951
- {exists ? (
952
- <IconCircleCheck
953
- size={15}
954
- className="shrink-0 text-green-600 dark:text-green-400"
955
- />
956
- ) : (
957
- <IconAlertCircle
958
- size={15}
959
- className="shrink-0 text-amber-600 dark:text-amber-400"
960
- />
961
- )}
962
- <h3 className="truncate text-sm font-medium text-foreground">
963
- {item.label}
964
- </h3>
965
- </div>
966
- <p className="mt-1 line-clamp-2 text-xs text-muted-foreground">
967
- {item.description}
968
- </p>
988
+ <div
989
+ key={item.path}
990
+ className="flex flex-wrap items-center gap-3 border-t px-4 py-3 first:border-t-0"
991
+ >
992
+ {exists ? (
993
+ <IconCircleCheck size={16} className="shrink-0 text-primary" />
994
+ ) : (
995
+ <IconAlertCircle
996
+ size={16}
997
+ className="shrink-0 text-amber-600 dark:text-amber-400"
998
+ />
999
+ )}
1000
+ <div className="min-w-0 flex-1">
1001
+ <div className="truncate text-sm font-medium text-foreground">
1002
+ {item.label}
1003
+ </div>
1004
+ <div className="mt-0.5 truncate text-xs text-muted-foreground">
1005
+ {item.description}
1006
+ </div>
1007
+ <div className="mt-1 truncate font-mono text-[11px] text-muted-foreground/80">
1008
+ {item.path}
969
1009
  </div>
1010
+ </div>
1011
+ <div className="flex items-center gap-1.5">
1012
+ {exists ? (
1013
+ <Badge variant="outline">
1014
+ {global ? "All apps" : "Selected"}
1015
+ </Badge>
1016
+ ) : null}
1017
+ {resource && isAutoLoadedInstruction(resource) ? (
1018
+ <Badge variant="outline">Auto-loaded</Badge>
1019
+ ) : null}
970
1020
  {resource ? (
971
1021
  <EditResourceDialog
972
1022
  resource={resource}
@@ -992,24 +1042,6 @@ function GlobalContextSection({ resources }: { resources: any[] }) {
992
1042
  </Button>
993
1043
  )}
994
1044
  </div>
995
- <div className="mt-3 space-y-2">
996
- <div className="truncate font-mono text-[11px] text-muted-foreground">
997
- {item.path}
998
- </div>
999
- <div className="flex flex-wrap gap-1.5">
1000
- <Badge variant={exists ? "secondary" : "outline"}>
1001
- {exists ? "Present" : "Missing"}
1002
- </Badge>
1003
- {exists ? (
1004
- <Badge variant="outline">
1005
- {global ? "All apps" : "Selected"}
1006
- </Badge>
1007
- ) : null}
1008
- {resource && isAutoLoadedInstruction(resource) ? (
1009
- <Badge variant="outline">Auto-loaded</Badge>
1010
- ) : null}
1011
- </div>
1012
- </div>
1013
1045
  </div>
1014
1046
  );
1015
1047
  })}
@@ -1086,7 +1118,7 @@ export default function WorkspaceRoute() {
1086
1118
 
1087
1119
  return (
1088
1120
  <DispatchShell
1089
- title="Agent Resources"
1121
+ title="Resources"
1090
1122
  description="Manage inherited skills, guardrail instructions, agent profiles, reference resources, and MCP servers. All-app resources are available to every app without syncing."
1091
1123
  >
1092
1124
  {resourcesQuery.isError || grantsQuery.isError ? (
@@ -1111,13 +1143,10 @@ export default function WorkspaceRoute() {
1111
1143
  </div>
1112
1144
  </div>
1113
1145
 
1114
- {!resourcesQuery.isError ? (
1115
- <GlobalContextSection resources={resources || []} />
1116
- ) : null}
1117
-
1118
1146
  {!resourcesQuery.isError && !grantsQuery.isError ? (
1119
1147
  <Tabs defaultValue="skills">
1120
1148
  <TabsList>
1149
+ <TabsTrigger value="context">Global context</TabsTrigger>
1121
1150
  <TabsTrigger value="skills">
1122
1151
  Skills {skills.length > 0 && `(${skills.length})`}
1123
1152
  </TabsTrigger>
@@ -1136,6 +1165,13 @@ export default function WorkspaceRoute() {
1136
1165
  </TabsTrigger>
1137
1166
  </TabsList>
1138
1167
 
1168
+ <TabsContent value="context" className="mt-4">
1169
+ <GlobalContextSection
1170
+ resources={resources || []}
1171
+ isLoading={isLoading}
1172
+ />
1173
+ </TabsContent>
1174
+
1139
1175
  <TabsContent value="skills" className="mt-4">
1140
1176
  <ResourceList
1141
1177
  items={skills}
@@ -1,3 +1,4 @@
1
+ import { defineAppRoles } from "@agent-native/core/org";
1
2
  import {
2
3
  registerPackageActions,
3
4
  type NitroPluginDef,
@@ -5,6 +6,9 @@ import {
5
6
 
6
7
  import { dispatchActions } from "../actions/index.js";
7
8
  import type { DispatchConfig } from "../config.js";
9
+ import { dispatchAccessDescriptor } from "../shared/app-roles.js";
10
+
11
+ defineAppRoles(dispatchAccessDescriptor);
8
12
 
9
13
  /**
10
14
  * Register dispatch's package-contributed actions on import. The framework's
@@ -3,6 +3,7 @@ import { afterEach, describe, expect, it, vi } from "vitest";
3
3
 
4
4
  import {
5
5
  generateWorkspaceAppDescription,
6
+ getAppCreationSettings,
6
7
  listAvailableWorkspaceTemplates,
7
8
  listWorkspaceApps,
8
9
  setAppCreationSettings,
@@ -46,7 +47,6 @@ const mocks = vi.hoisted(() => {
46
47
  })),
47
48
  ensureBuilderProject: vi.fn(),
48
49
  runBuilderAgent: vi.fn(),
49
- resolveBuilderBranchProjectId: vi.fn(async () => ""),
50
50
  getBuilderBranchProjectId: vi.fn(() => ""),
51
51
  writeAppSecret: vi.fn(async () => "secret-id"),
52
52
  deleteAppSecret: vi.fn(async () => true),
@@ -86,8 +86,6 @@ vi.mock("@agent-native/core/server", async (importOriginal) => {
86
86
  ensureBuilderProject: (...args: any[]) =>
87
87
  mocks.ensureBuilderProject(...args),
88
88
  runBuilderAgent: (...args: any[]) => mocks.runBuilderAgent(...args),
89
- resolveBuilderBranchProjectId: (...args: any[]) =>
90
- mocks.resolveBuilderBranchProjectId(...args),
91
89
  getBuilderBranchProjectId: (...args: any[]) =>
92
90
  mocks.getBuilderBranchProjectId(...args),
93
91
  };
@@ -121,12 +119,50 @@ afterEach(() => {
121
119
  source: null,
122
120
  lookupFailed: false,
123
121
  });
124
- mocks.resolveBuilderBranchProjectId.mockResolvedValue("");
125
122
  mocks.getBuilderBranchProjectId.mockReturnValue("");
126
123
  mocks.ensureBuilderProject.mockReset();
127
124
  globalThis.fetch = originalFetch;
128
125
  });
129
126
 
127
+ describe("getAppCreationSettings", () => {
128
+ it("treats the Dispatch project as authoritative over the environment", async () => {
129
+ mocks.settings.set("dispatch-app-creation-settings:org:builder_io", {
130
+ builderProjectId: "dispatch-project",
131
+ });
132
+ vi.stubEnv("DISPATCH_BUILDER_PROJECT_ID", "stale-env-project");
133
+
134
+ const result = await runWithRequestContext(
135
+ { userEmail: "dev@example.test", orgId: "builder_io" },
136
+ () => getAppCreationSettings(),
137
+ );
138
+
139
+ expect(result).toMatchObject({
140
+ builderProjectId: "dispatch-project",
141
+ builderProjectIdSource: "dispatch",
142
+ envBuilderProjectId: "stale-env-project",
143
+ });
144
+ });
145
+
146
+ it("treats an explicit Dispatch null as disabled over the environment", async () => {
147
+ mocks.settings.set("dispatch-app-creation-settings:org:builder_io", {
148
+ builderProjectId: null,
149
+ });
150
+ vi.stubEnv("DISPATCH_BUILDER_PROJECT_ID", "stale-env-project");
151
+
152
+ const result = await runWithRequestContext(
153
+ { userEmail: "dev@example.test", orgId: "builder_io" },
154
+ () => getAppCreationSettings(),
155
+ );
156
+
157
+ expect(result).toMatchObject({
158
+ builderProjectId: null,
159
+ builderProjectIdSource: "dispatch",
160
+ envBuilderProjectId: "stale-env-project",
161
+ builderBranchingEnabled: false,
162
+ });
163
+ });
164
+ });
165
+
130
166
  describe("listWorkspaceApps", () => {
131
167
  function stubNoPendingContext() {
132
168
  for (const key of [
@@ -399,9 +435,11 @@ describe("listWorkspaceApps", () => {
399
435
  name: "Todo Board",
400
436
  }),
401
437
  ),
402
- ).rejects.toThrow(
403
- "Only organization owners and admins can update app creation settings.",
404
- );
438
+ ).rejects.toMatchObject({
439
+ message:
440
+ "Only organization owners and admins can update app creation settings.",
441
+ statusCode: 403,
442
+ });
405
443
  expect(
406
444
  mocks.settings.get("workspace-app-metadata:org:org-123"),
407
445
  ).toBeUndefined();
@@ -603,12 +641,11 @@ describe("startWorkspaceAppCreation", () => {
603
641
  expect(mocks.runBuilderAgent).toHaveBeenCalledWith(
604
642
  expect.objectContaining({ projectId: "project-provisioned" }),
605
643
  );
606
- expect(mocks.writeAppSecret).toHaveBeenCalledWith(
607
- expect.objectContaining({
608
- key: "BUILDER_BRANCH_PROJECT_ID",
609
- value: "project-provisioned",
610
- }),
611
- );
644
+ expect(mocks.settings.get(settingsKey)).toMatchObject({
645
+ builderProjectId: "project-provisioned",
646
+ });
647
+ expect(mocks.writeAppSecret).not.toHaveBeenCalled();
648
+ expect(mocks.deleteAppSecret).not.toHaveBeenCalled();
612
649
  });
613
650
 
614
651
  it("does not let an organization member persist an auto-provisioned project", async () => {
@@ -653,58 +690,68 @@ describe("setAppCreationSettings", () => {
653
690
  );
654
691
  }
655
692
 
656
- it("stores the project id as an org-scoped credential so member apps resolve it", async () => {
693
+ it("stores the project id in the org-scoped Dispatch settings row", async () => {
657
694
  await save(projectId);
658
695
 
659
- expect(mocks.writeAppSecret).toHaveBeenCalledWith(
660
- expect.objectContaining({
661
- key: "BUILDER_BRANCH_PROJECT_ID",
662
- value: projectId,
663
- scope: "org",
664
- scopeId: orgId,
665
- }),
696
+ expect(
697
+ mocks.settings.get("dispatch-app-creation-settings:org:builder_io"),
698
+ ).toEqual({ builderProjectId: projectId });
699
+ expect(mocks.putSetting).toHaveBeenCalledWith(
700
+ "dispatch-app-creation-settings:org:builder_io",
701
+ { builderProjectId: projectId },
666
702
  );
667
703
  expect(mocks.deleteAppSecret).not.toHaveBeenCalled();
704
+ expect(mocks.writeAppSecret).not.toHaveBeenCalled();
668
705
  });
669
706
 
670
- it("scopes the credential to one organization rather than every tenant", async () => {
707
+ it("scopes the settings row to one organization rather than every tenant", async () => {
671
708
  await save(projectId);
672
709
 
673
- const [args] = mocks.writeAppSecret.mock.calls.at(-1) as [
674
- { scope: string; scopeId: string },
675
- ];
676
- expect(args.scope).not.toBe("user");
677
- expect(args.scopeId).toBe(orgId);
710
+ expect(mocks.putSetting.mock.calls.at(-1)?.[0]).toBe(
711
+ "dispatch-app-creation-settings:org:builder_io",
712
+ );
713
+ expect(mocks.putSetting.mock.calls.at(-1)?.[0]).not.toContain(":user:");
714
+ expect(mocks.writeAppSecret).not.toHaveBeenCalled();
678
715
  });
679
716
 
680
- it("falls back to a solo workspace scope when there is no active org", async () => {
717
+ it("uses a user-scoped settings row when there is no active org", async () => {
681
718
  await save(projectId, { userEmail: "dev@example.test" });
682
719
 
683
- expect(mocks.writeAppSecret).toHaveBeenCalledWith(
684
- expect.objectContaining({
685
- scope: "workspace",
686
- scopeId: "solo:dev@example.test",
687
- }),
688
- );
720
+ expect(mocks.settings.get(settingsKey)).toEqual({
721
+ builderProjectId: projectId,
722
+ });
723
+ expect(mocks.putSetting).toHaveBeenCalledWith(settingsKey, {
724
+ builderProjectId: projectId,
725
+ });
726
+ expect(mocks.writeAppSecret).not.toHaveBeenCalled();
689
727
  });
690
728
 
691
- it("removes the credential when the project id is cleared", async () => {
729
+ it("persists an explicit null when the project id is cleared", async () => {
692
730
  await save(null);
693
731
 
694
- expect(mocks.deleteAppSecret).toHaveBeenCalledWith({
695
- key: "BUILDER_BRANCH_PROJECT_ID",
696
- scope: "org",
697
- scopeId: orgId,
698
- });
732
+ expect(
733
+ mocks.settings.get("dispatch-app-creation-settings:org:builder_io"),
734
+ ).toEqual({ builderProjectId: null });
735
+ expect(mocks.putSetting).toHaveBeenCalledWith(
736
+ "dispatch-app-creation-settings:org:builder_io",
737
+ { builderProjectId: null },
738
+ );
739
+ expect(mocks.deleteAppSecret).not.toHaveBeenCalled();
699
740
  expect(mocks.writeAppSecret).not.toHaveBeenCalled();
700
741
  });
701
742
 
702
- it("does not save the setting when the credential write fails", async () => {
743
+ it("never consults the project secret store when saving settings", async () => {
703
744
  mocks.writeAppSecret.mockRejectedValueOnce(
704
745
  new Error("credential store down"),
705
746
  );
706
747
 
707
- await expect(save(projectId)).rejects.toThrow("credential store down");
708
- expect(mocks.putSetting).not.toHaveBeenCalled();
748
+ await expect(save(projectId)).resolves.toMatchObject({
749
+ builderProjectId: projectId,
750
+ });
751
+ expect(
752
+ mocks.settings.get("dispatch-app-creation-settings:org:builder_io"),
753
+ ).toEqual({ builderProjectId: projectId });
754
+ expect(mocks.writeAppSecret).not.toHaveBeenCalled();
755
+ expect(mocks.deleteAppSecret).not.toHaveBeenCalled();
709
756
  });
710
757
  });