@agent-native/dispatch 0.24.0 → 0.24.3

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 (165) hide show
  1. package/dist/actions/provider-api-register.d.ts +12 -12
  2. package/dist/components/admin-navigation.d.ts.map +1 -1
  3. package/dist/components/admin-navigation.js +21 -5
  4. package/dist/components/admin-navigation.js.map +1 -1
  5. package/dist/components/agents-panel.d.ts.map +1 -1
  6. package/dist/components/agents-panel.js +7 -2
  7. package/dist/components/agents-panel.js.map +1 -1
  8. package/dist/components/app-list-row.d.ts +1 -1
  9. package/dist/components/app-list-row.d.ts.map +1 -1
  10. package/dist/components/app-list-row.js +1 -1
  11. package/dist/components/app-list-row.js.map +1 -1
  12. package/dist/components/app-open-actions.d.ts +1 -17
  13. package/dist/components/app-open-actions.d.ts.map +1 -1
  14. package/dist/components/app-open-actions.js +1 -20
  15. package/dist/components/app-open-actions.js.map +1 -1
  16. package/dist/components/app-row-settings.d.ts +6 -0
  17. package/dist/components/app-row-settings.d.ts.map +1 -0
  18. package/dist/components/app-row-settings.js +11 -0
  19. package/dist/components/app-row-settings.js.map +1 -0
  20. package/dist/components/automation-run-history-dialog.d.ts.map +1 -1
  21. package/dist/components/automation-run-history-dialog.js +3 -2
  22. package/dist/components/automation-run-history-dialog.js.map +1 -1
  23. package/dist/components/connected-app-card.d.ts.map +1 -1
  24. package/dist/components/connected-app-card.js +1 -1
  25. package/dist/components/connected-app-card.js.map +1 -1
  26. package/dist/components/create-app-popover.js +2 -2
  27. package/dist/components/create-app-popover.js.map +1 -1
  28. package/dist/components/dispatch-control-plane.d.ts.map +1 -1
  29. package/dist/components/dispatch-control-plane.js +23 -7
  30. package/dist/components/dispatch-control-plane.js.map +1 -1
  31. package/dist/components/dispatch-shell.d.ts.map +1 -1
  32. package/dist/components/dispatch-shell.js +5 -1
  33. package/dist/components/dispatch-shell.js.map +1 -1
  34. package/dist/components/docs-link.d.ts +16 -0
  35. package/dist/components/docs-link.d.ts.map +1 -0
  36. package/dist/components/docs-link.js +34 -0
  37. package/dist/components/docs-link.js.map +1 -0
  38. package/dist/components/layout/Layout.d.ts +1 -0
  39. package/dist/components/layout/Layout.d.ts.map +1 -1
  40. package/dist/components/layout/Layout.js +37 -28
  41. package/dist/components/layout/Layout.js.map +1 -1
  42. package/dist/components/messaging-setup-panel.d.ts.map +1 -1
  43. package/dist/components/messaging-setup-panel.js +3 -2
  44. package/dist/components/messaging-setup-panel.js.map +1 -1
  45. package/dist/components/usage-alerts-panel.d.ts +11 -0
  46. package/dist/components/usage-alerts-panel.d.ts.map +1 -0
  47. package/dist/components/usage-alerts-panel.js +125 -0
  48. package/dist/components/usage-alerts-panel.js.map +1 -0
  49. package/dist/components/workspace-app-card.d.ts +3 -1
  50. package/dist/components/workspace-app-card.d.ts.map +1 -1
  51. package/dist/components/workspace-app-card.js +70 -29
  52. package/dist/components/workspace-app-card.js.map +1 -1
  53. package/dist/components/workspace-app-search.d.ts +10 -0
  54. package/dist/components/workspace-app-search.d.ts.map +1 -0
  55. package/dist/components/workspace-app-search.js +27 -0
  56. package/dist/components/workspace-app-search.js.map +1 -0
  57. package/dist/components/workspace-resource-effective-stack.d.ts.map +1 -1
  58. package/dist/components/workspace-resource-effective-stack.js +3 -2
  59. package/dist/components/workspace-resource-effective-stack.js.map +1 -1
  60. package/dist/components/workspace-template-card.d.ts.map +1 -1
  61. package/dist/components/workspace-template-card.js +2 -1
  62. package/dist/components/workspace-template-card.js.map +1 -1
  63. package/dist/lib/workspace-app-layout.d.ts +19 -0
  64. package/dist/lib/workspace-app-layout.d.ts.map +1 -0
  65. package/dist/lib/workspace-app-layout.js +75 -0
  66. package/dist/lib/workspace-app-layout.js.map +1 -0
  67. package/dist/lib/workspace-apps.d.ts +10 -0
  68. package/dist/lib/workspace-apps.d.ts.map +1 -1
  69. package/dist/lib/workspace-apps.js +59 -0
  70. package/dist/lib/workspace-apps.js.map +1 -1
  71. package/dist/lib/workspace-apps.test.js +18 -1
  72. package/dist/lib/workspace-apps.test.js.map +1 -1
  73. package/dist/routes/pages/$appId.d.ts.map +1 -1
  74. package/dist/routes/pages/$appId.js +4 -2
  75. package/dist/routes/pages/$appId.js.map +1 -1
  76. package/dist/routes/pages/agents.d.ts.map +1 -1
  77. package/dist/routes/pages/agents.js +2 -3
  78. package/dist/routes/pages/agents.js.map +1 -1
  79. package/dist/routes/pages/apps.$appId.d.ts.map +1 -1
  80. package/dist/routes/pages/apps.$appId.js +8 -3
  81. package/dist/routes/pages/apps.$appId.js.map +1 -1
  82. package/dist/routes/pages/apps.d.ts.map +1 -1
  83. package/dist/routes/pages/apps.js +31 -16
  84. package/dist/routes/pages/apps.js.map +1 -1
  85. package/dist/routes/pages/audit.d.ts.map +1 -1
  86. package/dist/routes/pages/audit.js +2 -1
  87. package/dist/routes/pages/audit.js.map +1 -1
  88. package/dist/routes/pages/dreams.d.ts.map +1 -1
  89. package/dist/routes/pages/dreams.js +14 -18
  90. package/dist/routes/pages/dreams.js.map +1 -1
  91. package/dist/routes/pages/integrations.d.ts.map +1 -1
  92. package/dist/routes/pages/integrations.js +51 -14
  93. package/dist/routes/pages/integrations.js.map +1 -1
  94. package/dist/routes/pages/metrics.d.ts.map +1 -1
  95. package/dist/routes/pages/metrics.js +4 -1
  96. package/dist/routes/pages/metrics.js.map +1 -1
  97. package/dist/routes/pages/operations.js +1 -1
  98. package/dist/routes/pages/operations.js.map +1 -1
  99. package/dist/routes/pages/settings.d.ts.map +1 -1
  100. package/dist/routes/pages/settings.js +7 -2
  101. package/dist/routes/pages/settings.js.map +1 -1
  102. package/dist/routes/pages/thread-debug.d.ts.map +1 -1
  103. package/dist/routes/pages/thread-debug.js +5 -8
  104. package/dist/routes/pages/thread-debug.js.map +1 -1
  105. package/dist/routes/pages/vault.d.ts.map +1 -1
  106. package/dist/routes/pages/vault.js +1 -1
  107. package/dist/routes/pages/vault.js.map +1 -1
  108. package/dist/routes/pages/workspace.d.ts.map +1 -1
  109. package/dist/routes/pages/workspace.js +13 -10
  110. package/dist/routes/pages/workspace.js.map +1 -1
  111. package/dist/server/index.d.ts.map +1 -1
  112. package/dist/server/index.js +3 -0
  113. package/dist/server/index.js.map +1 -1
  114. package/dist/server/lib/app-creation-store.d.ts.map +1 -1
  115. package/dist/server/lib/app-creation-store.js +12 -52
  116. package/dist/server/lib/app-creation-store.js.map +1 -1
  117. package/dist/shared/app-roles.d.ts +9 -0
  118. package/dist/shared/app-roles.d.ts.map +1 -0
  119. package/dist/shared/app-roles.js +7 -0
  120. package/dist/shared/app-roles.js.map +1 -0
  121. package/package.json +4 -3
  122. package/src/components/admin-navigation.tsx +57 -11
  123. package/src/components/agents-panel.tsx +54 -29
  124. package/src/components/app-list-row.tsx +1 -2
  125. package/src/components/app-open-actions.tsx +5 -114
  126. package/src/components/app-row-settings.tsx +54 -0
  127. package/src/components/automation-run-history-dialog.tsx +8 -4
  128. package/src/components/connected-app-card.tsx +0 -1
  129. package/src/components/create-app-popover.spec.tsx +1 -1
  130. package/src/components/create-app-popover.tsx +2 -2
  131. package/src/components/dispatch-control-plane.spec.tsx +36 -2
  132. package/src/components/dispatch-control-plane.tsx +83 -27
  133. package/src/components/dispatch-shell.tsx +7 -1
  134. package/src/components/docs-link.tsx +52 -0
  135. package/src/components/layout/Layout.spec.tsx +10 -0
  136. package/src/components/layout/Layout.tsx +50 -23
  137. package/src/components/messaging-setup-panel.tsx +10 -3
  138. package/src/components/usage-alerts-panel.tsx +445 -0
  139. package/src/components/workspace-app-card.spec.tsx +130 -9
  140. package/src/components/workspace-app-card.tsx +128 -71
  141. package/src/components/workspace-app-search.tsx +96 -0
  142. package/src/components/workspace-resource-effective-stack.tsx +10 -5
  143. package/src/components/workspace-template-card.spec.tsx +52 -23
  144. package/src/components/workspace-template-card.tsx +7 -10
  145. package/src/lib/workspace-app-layout.spec.ts +47 -0
  146. package/src/lib/workspace-app-layout.ts +121 -0
  147. package/src/lib/workspace-apps.test.ts +45 -1
  148. package/src/lib/workspace-apps.ts +72 -0
  149. package/src/routes/pages/$appId.tsx +3 -6
  150. package/src/routes/pages/agents.tsx +6 -3
  151. package/src/routes/pages/apps.$appId.tsx +6 -6
  152. package/src/routes/pages/apps.tsx +116 -62
  153. package/src/routes/pages/audit.tsx +28 -8
  154. package/src/routes/pages/dreams.tsx +92 -142
  155. package/src/routes/pages/integrations.tsx +147 -68
  156. package/src/routes/pages/metrics.tsx +8 -0
  157. package/src/routes/pages/operations.tsx +2 -2
  158. package/src/routes/pages/settings.tsx +14 -9
  159. package/src/routes/pages/thread-debug.tsx +18 -36
  160. package/src/routes/pages/vault.tsx +7 -2
  161. package/src/routes/pages/workspace.tsx +92 -56
  162. package/src/server/index.ts +4 -0
  163. package/src/server/lib/app-creation-store.spec.ts +90 -43
  164. package/src/server/lib/app-creation-store.ts +15 -61
  165. package/src/shared/app-roles.ts +8 -0
@@ -3,12 +3,14 @@ import {
3
3
  useActionQuery,
4
4
  } from "@agent-native/core/client/hooks";
5
5
  import {
6
- IconCheck,
6
+ getDefaultMcpIntegrations,
7
+ McpIntegrationLogo,
8
+ } from "@agent-native/core/client/resources";
9
+ import {
7
10
  IconChevronRight,
8
- IconCircleDashed,
9
- IconKey,
10
11
  IconLink,
11
12
  IconPlugConnected,
13
+ IconSearch,
12
14
  } from "@tabler/icons-react";
13
15
  import { useQueryClient } from "@tanstack/react-query";
14
16
  import { useMemo, useState } from "react";
@@ -17,7 +19,6 @@ import { toast } from "sonner";
17
19
 
18
20
  import { ActionQueryError } from "../../components/action-query-error";
19
21
  import { DispatchShell } from "../../components/dispatch-shell";
20
- import { Badge } from "../../components/ui/badge";
21
22
  import { Button } from "../../components/ui/button";
22
23
  import {
23
24
  Collapsible,
@@ -34,11 +35,7 @@ import {
34
35
  } from "../../components/ui/dialog";
35
36
  import { Input } from "../../components/ui/input";
36
37
  import { Label } from "../../components/ui/label";
37
- import {
38
- Tooltip,
39
- TooltipContent,
40
- TooltipTrigger,
41
- } from "../../components/ui/tooltip";
38
+ import { Skeleton } from "../../components/ui/skeleton";
42
39
 
43
40
  export function meta() {
44
41
  return [{ title: "Connections — Dispatch" }];
@@ -97,6 +94,38 @@ function inferProviderFromKey(key: string, label: string): string {
97
94
  return "other";
98
95
  }
99
96
 
97
+ const MCP_INTEGRATIONS_BY_ID = new Map(
98
+ getDefaultMcpIntegrations().map((integration) => [
99
+ integration.id,
100
+ integration,
101
+ ]),
102
+ );
103
+
104
+ const CREDENTIAL_PROVIDER_LOGO_IDS: Record<string, string> = {
105
+ google: "google-workspace",
106
+ bigquery: "google-workspace",
107
+ jira: "atlassian",
108
+ };
109
+
110
+ function credentialLogo(service: Service) {
111
+ const providerId = inferProviderFromKey(service.key, service.label);
112
+ const integration = MCP_INTEGRATIONS_BY_ID.get(
113
+ CREDENTIAL_PROVIDER_LOGO_IDS[providerId] ?? providerId,
114
+ );
115
+ if (!integration?.logoUrl) {
116
+ return <IconPlugConnected className="size-4 text-muted-foreground" />;
117
+ }
118
+ return (
119
+ <McpIntegrationLogo
120
+ name={service.label}
121
+ logoUrl={integration.logoUrl}
122
+ integrationId={integration.id}
123
+ className="size-full rounded-md border-0 bg-transparent"
124
+ imageClassName="size-full p-1"
125
+ />
126
+ );
127
+ }
128
+
100
129
  function ConnectDialog({
101
130
  service,
102
131
  open,
@@ -110,6 +139,10 @@ function ConnectDialog({
110
139
  }) {
111
140
  const [value, setValue] = useState("");
112
141
  const qc = useQueryClient();
142
+ const configuredApps = service.apps.filter((app) => app.configured);
143
+ const configuredAppNames = configuredApps
144
+ .map((app) => app.appName)
145
+ .join(", ");
113
146
 
114
147
  const createSecret = useActionMutation("create-vault-secret", {});
115
148
  const createGrant = useActionMutation("create-vault-grant", {});
@@ -189,12 +222,11 @@ function ConnectDialog({
189
222
  <DialogHeader>
190
223
  <DialogTitle>Connect {service.label}</DialogTitle>
191
224
  <DialogDescription>
192
- Used by{" "}
193
- {service.apps.length === 1
194
- ? service.apps[0].appName
195
- : `${service.apps.length} apps`}
196
- . Saved to the workspace vault and synced to every app that needs
197
- it.
225
+ {configuredApps.length > 0
226
+ ? `Configured in ${configuredAppNames}. `
227
+ : "No app currently reports this key as configured. "}
228
+ Saving it creates a workspace vault entry using this exact key name.
229
+ Apps need the same key name to use it.
198
230
  </DialogDescription>
199
231
  </DialogHeader>
200
232
  <div className="space-y-3">
@@ -240,55 +272,54 @@ function ConnectorCard({
240
272
  accessMode: "all-apps" | "manual";
241
273
  }) {
242
274
  const [open, setOpen] = useState(false);
243
- const isConnected = service.apps.some((a) => a.configured);
244
- const appCount = service.apps.length;
275
+ const configuredApps = service.apps.filter((app) => app.configured);
276
+ const vaultApps = service.apps.filter((app) => app.vaultGranted);
277
+ const isAvailable = configuredApps.length > 0 || vaultApps.length > 0;
278
+ const configuredAppNames = configuredApps
279
+ .map((app) => app.appName)
280
+ .join(", ");
281
+ const vaultAppNames = vaultApps.map((app) => app.appName).join(", ");
245
282
 
246
283
  return (
247
284
  <>
248
- <button
249
- type="button"
250
- onClick={() => setOpen(true)}
251
- className="group flex flex-col items-start gap-2 rounded-2xl bg-card p-5 text-left transition-[background-color] hover:bg-card/80 cursor-pointer"
252
- >
253
- <div className="flex w-full items-start justify-between gap-2">
254
- <div className="flex h-9 w-9 items-center justify-center rounded-xl bg-muted">
255
- <IconKey size={16} className="text-muted-foreground" />
256
- </div>
257
- {isConnected ? (
258
- <Badge
259
- variant="secondary"
260
- className="bg-green-500/10 text-green-700 dark:text-green-400 gap-1"
261
- >
262
- <IconCheck size={12} />
263
- Connected
264
- </Badge>
265
- ) : (
266
- <Badge
267
- variant="secondary"
268
- className="bg-amber-500/10 text-amber-700 dark:text-amber-400 gap-1"
269
- >
270
- <IconCircleDashed size={12} />
271
- Connect
272
- </Badge>
273
- )}
285
+ <article className="flex min-w-0 items-center gap-3 border-b border-border/60 py-3.5 last:border-b-0">
286
+ <div className="flex size-8 shrink-0 items-center justify-center rounded-md bg-muted">
287
+ {credentialLogo(service)}
274
288
  </div>
275
- <div className="w-full min-w-0">
276
- <Tooltip>
277
- <TooltipTrigger asChild>
278
- <div className="text-sm font-semibold text-foreground truncate">
279
- {service.label}
280
- </div>
281
- </TooltipTrigger>
282
- <TooltipContent>{service.label}</TooltipContent>
283
- </Tooltip>
284
- <div className="font-mono text-xs text-muted-foreground/80 truncate">
285
- {service.key}
289
+ <div className="min-w-0 flex-1">
290
+ <div className="flex min-w-0 items-center gap-2">
291
+ <span className="truncate text-sm font-medium text-foreground">
292
+ {service.label}
293
+ </span>
294
+ <span
295
+ className={
296
+ isAvailable
297
+ ? "shrink-0 text-[11px] font-medium text-emerald-600 dark:text-emerald-400"
298
+ : "shrink-0 text-[11px] font-medium text-muted-foreground"
299
+ }
300
+ >
301
+ {vaultApps.length > 0 ? "Connected" : "App key"}
302
+ </span>
286
303
  </div>
304
+ <p className="mt-0.5 truncate font-mono text-[11px] leading-5 text-muted-foreground/80">
305
+ {service.key}
306
+ </p>
307
+ <p className="mt-0.5 truncate text-xs leading-5 text-muted-foreground">
308
+ Declared by {service.apps.length}{" "}
309
+ {service.apps.length === 1 ? "app" : "apps"}
310
+ {configuredApps.length > 0 ? ` · ${configuredAppNames}` : ""}
311
+ {vaultApps.length > 0 ? ` · Vault: ${vaultAppNames}` : ""}
312
+ </p>
287
313
  </div>
288
- <div className="text-xs text-muted-foreground">
289
- Used by {appCount} {appCount === 1 ? "app" : "apps"}
290
- </div>
291
- </button>
314
+ <button
315
+ type="button"
316
+ onClick={() => setOpen(true)}
317
+ className="inline-flex h-8 shrink-0 items-center justify-center gap-1 rounded-md border border-border bg-background px-2.5 text-xs font-medium text-foreground transition-colors hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
318
+ >
319
+ {isAvailable ? "Manage" : "Connect"}
320
+ <IconChevronRight className="size-3.5 rtl:-scale-x-100" />
321
+ </button>
322
+ </article>
292
323
  <ConnectDialog
293
324
  service={service}
294
325
  open={open}
@@ -326,11 +357,13 @@ function PerAppDetailRow({ app }: { app: CatalogApp }) {
326
357
  export default function ConnectionsRoute() {
327
358
  const catalogQuery = useActionQuery("list-integrations-catalog", {});
328
359
  const accessQuery = useActionQuery("get-vault-access-settings", {});
329
- const { data: catalog, isLoading } = catalogQuery;
360
+ const { data: catalog } = catalogQuery;
330
361
  const { data: accessSettings } = accessQuery;
362
+ const isLoading = catalogQuery.isLoading || accessQuery.isLoading;
331
363
  const apps = (catalog as CatalogApp[]) || [];
332
364
  const accessMode =
333
365
  (accessSettings as any)?.mode === "manual" ? "manual" : "all-apps";
366
+ const [query, setQuery] = useState("");
334
367
 
335
368
  const services = useMemo<Service[]>(() => {
336
369
  const map = new Map<string, Service>();
@@ -358,13 +391,26 @@ export default function ConnectionsRoute() {
358
391
  );
359
392
  }, [apps]);
360
393
 
361
- const available = services.filter((s) => !s.apps.some((a) => a.configured));
362
- const connected = services.filter((s) => s.apps.some((a) => a.configured));
394
+ const filteredServices = useMemo(() => {
395
+ const normalized = query.trim().toLowerCase();
396
+ if (!normalized) return services;
397
+ return services.filter(
398
+ (service) =>
399
+ service.label.toLowerCase().includes(normalized) ||
400
+ service.key.toLowerCase().includes(normalized),
401
+ );
402
+ }, [query, services]);
403
+ const available = filteredServices.filter(
404
+ (s) => !s.apps.some((a) => a.configured),
405
+ );
406
+ const connected = filteredServices.filter((s) =>
407
+ s.apps.some((a) => a.configured),
408
+ );
363
409
 
364
410
  return (
365
411
  <DispatchShell
366
412
  title="Connections"
367
- description="Connect services once. Apps that need them pick up the key automatically."
413
+ description="Connect workspace credentials declared by apps and manage how they are shared."
368
414
  >
369
415
  {catalogQuery.isError || accessQuery.isError ? (
370
416
  <ActionQueryError
@@ -376,9 +422,40 @@ export default function ConnectionsRoute() {
376
422
  />
377
423
  ) : null}
378
424
 
425
+ {!catalogQuery.isError && !isLoading ? (
426
+ <div className="mb-5 flex items-center gap-3">
427
+ <div className="relative min-w-0 flex-1">
428
+ <IconSearch className="pointer-events-none absolute start-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
429
+ <Input
430
+ value={query}
431
+ onChange={(event) => setQuery(event.target.value)}
432
+ placeholder="Search credentials"
433
+ aria-label="Search credentials"
434
+ className="h-9 ps-9"
435
+ />
436
+ </div>
437
+ <span className="shrink-0 text-xs text-muted-foreground">
438
+ {filteredServices.length} credential
439
+ {filteredServices.length === 1 ? "" : "s"}
440
+ </span>
441
+ </div>
442
+ ) : null}
443
+
379
444
  {!catalogQuery.isError && isLoading && services.length === 0 && (
380
- <div className="rounded-2xl border border-dashed px-6 py-12 text-center text-sm text-muted-foreground">
381
- Discovering apps and credentials…
445
+ <div className="overflow-hidden rounded-lg bg-card">
446
+ {Array.from({ length: 7 }).map((_, index) => (
447
+ <div
448
+ key={index}
449
+ className="flex items-center gap-3 border-b px-4 py-3 last:border-b-0"
450
+ >
451
+ <Skeleton className="size-8 rounded-md" />
452
+ <div className="min-w-0 flex-1">
453
+ <Skeleton className="h-4 w-40 max-w-full" />
454
+ <Skeleton className="mt-1.5 h-3 w-56 max-w-full" />
455
+ </div>
456
+ <Skeleton className="h-6 w-20 rounded-full" />
457
+ </div>
458
+ ))}
382
459
  </div>
383
460
  )}
384
461
 
@@ -392,13 +469,13 @@ export default function ConnectionsRoute() {
392
469
  <section>
393
470
  <div className="mb-3 flex items-baseline justify-between">
394
471
  <h2 className="text-sm font-medium text-foreground">
395
- Available to connect
472
+ Not configured in apps
396
473
  </h2>
397
474
  <span className="text-xs text-muted-foreground">
398
475
  {available.length}
399
476
  </span>
400
477
  </div>
401
- <div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
478
+ <div className="overflow-hidden rounded-lg bg-card">
402
479
  {available.map((service) => (
403
480
  <ConnectorCard
404
481
  key={service.key}
@@ -413,12 +490,14 @@ export default function ConnectionsRoute() {
413
490
  {connected.length > 0 && (
414
491
  <section>
415
492
  <div className="mb-3 mt-2 flex items-baseline justify-between">
416
- <h2 className="text-sm font-medium text-foreground">Connected</h2>
493
+ <h2 className="text-sm font-medium text-foreground">
494
+ Configured in apps
495
+ </h2>
417
496
  <span className="text-xs text-muted-foreground">
418
497
  {connected.length}
419
498
  </span>
420
499
  </div>
421
- <div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
500
+ <div className="overflow-hidden rounded-lg bg-card">
422
501
  {connected.map((service) => (
423
502
  <ConnectorCard
424
503
  key={service.key}
@@ -33,6 +33,7 @@ import {
33
33
  SelectValue,
34
34
  } from "../../components/ui/select";
35
35
  import { Skeleton } from "../../components/ui/skeleton";
36
+ import { UsageAlertsPanel } from "../../components/usage-alerts-panel";
36
37
  import { cn } from "../../lib/utils";
37
38
 
38
39
  export function meta() {
@@ -1038,6 +1039,13 @@ export default function MetricsRoute() {
1038
1039
  <ReviewUsageButton metrics={metrics} billing={billing} />
1039
1040
  </div>
1040
1041
 
1042
+ <UsageAlertsPanel
1043
+ scope={scope === "workspace" ? "workspace" : "user"}
1044
+ appOptions={metrics.byApp
1045
+ .filter((row) => row.key && row.key !== "unattributed")
1046
+ .map((row) => ({ id: row.key, label: displayApp(row.key) }))}
1047
+ />
1048
+
1041
1049
  <div className="grid gap-3 md:grid-cols-2 xl:grid-cols-5">
1042
1050
  <MetricCard
1043
1051
  label={billing.label}
@@ -105,11 +105,11 @@ export default function OperationsRoute() {
105
105
  >
106
106
  <TabsList className="w-fit">
107
107
  <TabsTrigger value="monitoring">
108
- <IconActivity size={15} />
108
+ <IconActivity size={15} className="mr-1.5" />
109
109
  {t("dispatch.pages.monitoring")}
110
110
  </TabsTrigger>
111
111
  <TabsTrigger value="database">
112
- <IconDatabase size={15} />
112
+ <IconDatabase size={15} className="mr-1.5" />
113
113
  {t("dispatch.pages.database")}
114
114
  </TabsTrigger>
115
115
  </TabsList>
@@ -26,6 +26,7 @@ import {
26
26
  } from "../../components/ui/card";
27
27
  import { Label } from "../../components/ui/label";
28
28
  import { Switch } from "../../components/ui/switch";
29
+ import { dispatchAccessDescriptor } from "../../shared/app-roles.js";
29
30
 
30
31
  export function meta() {
31
32
  return [{ title: "Settings - Dispatch" }];
@@ -33,7 +34,19 @@ export function meta() {
33
34
 
34
35
  export default function SettingsRoute() {
35
36
  const t = useT();
36
- 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
+ });
37
50
  const settingsTabs = [
38
51
  ...agentSettingsTabs.filter((tab) => tab.id !== "integrations"),
39
52
  {
@@ -172,14 +185,6 @@ export default function SettingsRoute() {
172
185
  </Card>
173
186
  </div>
174
187
  }
175
- team={
176
- <div className="mx-auto w-full max-w-3xl">
177
- <TeamPage
178
- showTitle={false}
179
- createOrgDescription="Set up a team to share dispatch destinations and approvals with your colleagues."
180
- />
181
- </div>
182
- }
183
188
  whatsNew={
184
189
  <div className="mx-auto w-full max-w-3xl">
185
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">