@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
@@ -4,11 +4,8 @@ import {
4
4
  } from "@agent-native/core/client/hooks";
5
5
  import {
6
6
  IconAlertTriangle,
7
- IconBrain,
8
7
  IconCalendarTime,
9
8
  IconCheck,
10
- IconCircleDashed,
11
- IconClock,
12
9
  IconDatabase,
13
10
  IconFileDiff,
14
11
  IconPlayerPlay,
@@ -548,32 +545,6 @@ function ProposalSkeleton() {
548
545
  );
549
546
  }
550
547
 
551
- function StatTile({
552
- label,
553
- value,
554
- icon: Icon,
555
- }: {
556
- label: string;
557
- value: string | number;
558
- icon: typeof IconBrain;
559
- }) {
560
- return (
561
- <div className="rounded-lg bg-card px-3 py-2.5">
562
- <div className="flex items-center justify-between gap-2">
563
- <div className="min-w-0 flex-1">
564
- <div className="truncate text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
565
- {label}
566
- </div>
567
- <div className="mt-1 text-xl font-semibold tabular-nums text-foreground">
568
- {value}
569
- </div>
570
- </div>
571
- <Icon size={18} className="shrink-0 text-muted-foreground" />
572
- </div>
573
- </div>
574
- );
575
- }
576
-
577
548
  function DreamSettingsSheet({
578
549
  open,
579
550
  onOpenChange,
@@ -1358,10 +1329,6 @@ export default function DreamsRoute() {
1358
1329
  const evidenceItems: DreamEvidence[] = detail?.evidence ?? [];
1359
1330
  const selectedSourceHealth: DreamSourceHealth[] =
1360
1331
  selectedDream?.sourceHealth ?? [];
1361
- const pendingProposalCount = proposals.filter(
1362
- (proposal) =>
1363
- String(proposal.status || "pending").toLowerCase() === "pending",
1364
- ).length;
1365
1332
  const appliedProposalCount = proposals.filter(
1366
1333
  (proposal) => String(proposal.status || "").toLowerCase() === "applied",
1367
1334
  ).length;
@@ -1428,32 +1395,10 @@ export default function DreamsRoute() {
1428
1395
  description="Review agent runs, propose memory improvements, and apply evidence-backed learning changes."
1429
1396
  >
1430
1397
  <div className="space-y-4">
1431
- <div className="flex flex-col gap-3">
1432
- <div className="grid grid-cols-2 gap-2 md:grid-cols-4">
1433
- <StatTile
1434
- label="Dream passes"
1435
- value={dreams.length}
1436
- icon={IconBrain}
1437
- />
1438
- <StatTile
1439
- label="Pending proposals"
1440
- value={pendingProposalCount}
1441
- icon={IconCircleDashed}
1442
- />
1443
- <StatTile
1444
- label="Candidate runs"
1445
- value={candidates.length}
1446
- icon={IconClock}
1447
- />
1448
- <StatTile
1449
- label="Inspected threads"
1450
- value={selectedDream ? dreamInspectedCount(selectedDream) : 0}
1451
- icon={IconCheck}
1452
- />
1453
- </div>
1454
- <div className="flex flex-wrap items-center gap-2">
1398
+ <div className="flex flex-wrap items-center justify-between gap-3 border-b pb-3">
1399
+ <div className="flex min-w-0 flex-wrap items-center gap-x-3 gap-y-1">
1455
1400
  {dreamSettings ? (
1456
- <Badge variant="outline" className="h-9 max-w-full truncate px-3">
1401
+ <Badge variant="outline" className="max-w-full truncate">
1457
1402
  {dreamSettings.enabled ? "Enabled" : "Paused"} ·{" "}
1458
1403
  {dreamSettings.allSources
1459
1404
  ? "All sources"
@@ -1461,6 +1406,11 @@ export default function DreamsRoute() {
1461
1406
  · {dreamSettings.schedule}
1462
1407
  </Badge>
1463
1408
  ) : null}
1409
+ <span className="text-xs text-muted-foreground">
1410
+ {dreams.length} passes · {candidates.length} candidate runs
1411
+ </span>
1412
+ </div>
1413
+ <div className="flex flex-wrap items-center gap-2">
1464
1414
  <DreamSettingsSheet
1465
1415
  open={settingsOpen}
1466
1416
  onOpenChange={handleSettingsOpenChange}
@@ -1519,8 +1469,8 @@ export default function DreamsRoute() {
1519
1469
  </div>
1520
1470
  </div>
1521
1471
 
1522
- <div className="grid gap-4 lg:grid-cols-2 xl:grid-cols-[280px_minmax(0,1fr)_380px]">
1523
- <section className="rounded-lg bg-card">
1472
+ <div className="grid gap-4 lg:grid-cols-[280px_minmax(0,1fr)]">
1473
+ <section className="min-w-0 rounded-lg bg-card">
1524
1474
  <div className="border-b px-4 py-3">
1525
1475
  <div className="text-sm font-semibold text-foreground">
1526
1476
  Recent passes
@@ -1770,92 +1720,92 @@ export default function DreamsRoute() {
1770
1720
  ) : null}
1771
1721
  </div>
1772
1722
  </section>
1723
+ </div>
1773
1724
 
1774
- <section className="rounded-lg bg-card">
1775
- <div className="border-b px-4 py-3">
1776
- <div>
1777
- <div className="text-sm font-semibold text-foreground">
1778
- Candidate runs
1779
- </div>
1780
- <div className="mt-1 text-xs text-muted-foreground">
1781
- Grounded signals ready for review.
1782
- </div>
1725
+ <section className="min-w-0 rounded-lg bg-card">
1726
+ <div className="border-b px-4 py-3">
1727
+ <div>
1728
+ <div className="text-sm font-semibold text-foreground">
1729
+ Candidate runs
1730
+ </div>
1731
+ <div className="mt-1 text-xs text-muted-foreground">
1732
+ Grounded signals ready for review.
1783
1733
  </div>
1784
1734
  </div>
1785
- <div className="max-h-[720px] overflow-auto p-3">
1786
- <QueryState
1787
- error={candidatesQuery.error}
1788
- label="Could not load candidates"
1789
- />
1790
- {candidatesQuery.isLoading ? <DreamListSkeleton /> : null}
1791
- {!candidatesQuery.isLoading &&
1792
- !candidatesQuery.error &&
1793
- candidateSourceHealth.length > 0 ? (
1794
- <div className="mb-3">
1795
- <SourceHealthPanel sources={candidateSourceHealth} />
1796
- </div>
1797
- ) : null}
1798
- {!candidatesQuery.isLoading && !candidatesQuery.error ? (
1799
- candidates.length > 0 ? (
1800
- <Table>
1801
- <TableHeader>
1802
- <TableRow>
1803
- <TableHead>Run</TableHead>
1804
- <TableHead>Signals</TableHead>
1805
- <TableHead className="w-20 text-right">Score</TableHead>
1806
- </TableRow>
1807
- </TableHeader>
1808
- <TableBody>
1809
- {candidates.map((candidate) => {
1810
- const id = candidateId(candidate);
1811
- const signals = candidateSignals(candidate);
1812
- return (
1813
- <TableRow key={id}>
1814
- <TableCell className="min-w-0 py-3">
1815
- <div className="max-w-[230px] truncate text-sm font-medium text-foreground">
1816
- {candidateLabel(candidate)}
1817
- </div>
1818
- <div className="mt-1 truncate font-mono text-[11px] text-muted-foreground">
1819
- {candidate.thread?.id ??
1820
- candidate.threadId ??
1821
- candidate.runId ??
1822
- id}
1823
- </div>
1824
- <div className="mt-1 text-[11px] text-muted-foreground">
1825
- {candidateOwner(candidate)} ·{" "}
1826
- {compactDate(candidateUpdatedAt(candidate))}
1827
- </div>
1828
- </TableCell>
1829
- <TableCell className="py-3">
1830
- <div className="mt-1 flex flex-wrap gap-1">
1831
- <Badge variant="outline">
1832
- {candidateStatus(candidate)}
1735
+ </div>
1736
+ <div className="max-h-[720px] overflow-auto p-3">
1737
+ <QueryState
1738
+ error={candidatesQuery.error}
1739
+ label="Could not load candidates"
1740
+ />
1741
+ {candidatesQuery.isLoading ? <DreamListSkeleton /> : null}
1742
+ {!candidatesQuery.isLoading &&
1743
+ !candidatesQuery.error &&
1744
+ candidateSourceHealth.length > 0 ? (
1745
+ <div className="mb-3">
1746
+ <SourceHealthPanel sources={candidateSourceHealth} />
1747
+ </div>
1748
+ ) : null}
1749
+ {!candidatesQuery.isLoading && !candidatesQuery.error ? (
1750
+ candidates.length > 0 ? (
1751
+ <Table>
1752
+ <TableHeader>
1753
+ <TableRow>
1754
+ <TableHead>Run</TableHead>
1755
+ <TableHead>Signals</TableHead>
1756
+ <TableHead className="w-20 text-right">Score</TableHead>
1757
+ </TableRow>
1758
+ </TableHeader>
1759
+ <TableBody>
1760
+ {candidates.map((candidate) => {
1761
+ const id = candidateId(candidate);
1762
+ const signals = candidateSignals(candidate);
1763
+ return (
1764
+ <TableRow key={id}>
1765
+ <TableCell className="min-w-0 py-3">
1766
+ <div className="max-w-[230px] truncate text-sm font-medium text-foreground">
1767
+ {candidateLabel(candidate)}
1768
+ </div>
1769
+ <div className="mt-1 truncate font-mono text-[11px] text-muted-foreground">
1770
+ {candidate.thread?.id ??
1771
+ candidate.threadId ??
1772
+ candidate.runId ??
1773
+ id}
1774
+ </div>
1775
+ <div className="mt-1 text-[11px] text-muted-foreground">
1776
+ {candidateOwner(candidate)} ·{" "}
1777
+ {compactDate(candidateUpdatedAt(candidate))}
1778
+ </div>
1779
+ </TableCell>
1780
+ <TableCell className="py-3">
1781
+ <div className="mt-1 flex flex-wrap gap-1">
1782
+ <Badge variant="outline">
1783
+ {candidateStatus(candidate)}
1784
+ </Badge>
1785
+ {signals.slice(0, 2).map((signal) => (
1786
+ <Badge key={signal} variant="secondary">
1787
+ {signal}
1833
1788
  </Badge>
1834
- {signals.slice(0, 2).map((signal) => (
1835
- <Badge key={signal} variant="secondary">
1836
- {signal}
1837
- </Badge>
1838
- ))}
1839
- </div>
1840
- </TableCell>
1841
- <TableCell className="py-3 text-right text-sm tabular-nums text-muted-foreground">
1842
- {candidate.score ?? "n/a"}
1843
- </TableCell>
1844
- </TableRow>
1845
- );
1846
- })}
1847
- </TableBody>
1848
- </Table>
1849
- ) : (
1850
- <EmptyPanel
1851
- title="No candidates"
1852
- description="No recent runs matched the dream candidate heuristics."
1853
- />
1854
- )
1855
- ) : null}
1856
- </div>
1857
- </section>
1858
- </div>
1789
+ ))}
1790
+ </div>
1791
+ </TableCell>
1792
+ <TableCell className="py-3 text-right text-sm tabular-nums text-muted-foreground">
1793
+ {candidate.score ?? "n/a"}
1794
+ </TableCell>
1795
+ </TableRow>
1796
+ );
1797
+ })}
1798
+ </TableBody>
1799
+ </Table>
1800
+ ) : (
1801
+ <EmptyPanel
1802
+ title="No candidates"
1803
+ description="No recent runs matched the dream candidate heuristics."
1804
+ />
1805
+ )
1806
+ ) : null}
1807
+ </div>
1808
+ </section>
1859
1809
  </div>
1860
1810
  </DispatchShell>
1861
1811
  );
@@ -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}