@agent-native/dispatch 0.40.30-nightly-20261021093329 → 0.40.31

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 (150) hide show
  1. package/CHANGELOG.md +15 -60
  2. package/dist/actions/connect-external-agent.d.ts.map +1 -1
  3. package/dist/actions/connect-external-agent.js +12 -13
  4. package/dist/actions/connect-external-agent.js.map +1 -1
  5. package/dist/actions/create-workspace-resource.d.ts +13 -13
  6. package/dist/actions/delete-destination.d.ts +12 -12
  7. package/dist/actions/delete-workspace-resource.d.ts +13 -13
  8. package/dist/actions/import-agent.d.ts +13 -13
  9. package/dist/actions/index.d.ts.map +1 -1
  10. package/dist/actions/index.js +8 -0
  11. package/dist/actions/index.js.map +1 -1
  12. package/dist/actions/inspect-connect-agent-card.d.ts +5 -0
  13. package/dist/actions/inspect-connect-agent-card.d.ts.map +1 -0
  14. package/dist/actions/inspect-connect-agent-card.js +15 -0
  15. package/dist/actions/inspect-connect-agent-card.js.map +1 -0
  16. package/dist/actions/install-curated-community-app.d.ts +36 -0
  17. package/dist/actions/install-curated-community-app.d.ts.map +1 -0
  18. package/dist/actions/install-curated-community-app.js +26 -0
  19. package/dist/actions/install-curated-community-app.js.map +1 -0
  20. package/dist/actions/list-connect-marketplace-apps.d.ts +3 -0
  21. package/dist/actions/list-connect-marketplace-apps.d.ts.map +1 -0
  22. package/dist/actions/list-connect-marketplace-apps.js +15 -0
  23. package/dist/actions/list-connect-marketplace-apps.js.map +1 -0
  24. package/dist/actions/list-connected-agents.d.ts +2 -0
  25. package/dist/actions/list-connected-agents.js +7 -3
  26. package/dist/actions/list-connected-agents.js.map +1 -1
  27. package/dist/actions/list-curated-community-apps.d.ts +3 -0
  28. package/dist/actions/list-curated-community-apps.d.ts.map +1 -0
  29. package/dist/actions/list-curated-community-apps.js +10 -0
  30. package/dist/actions/list-curated-community-apps.js.map +1 -0
  31. package/dist/actions/list-workspace-connections.d.ts.map +1 -1
  32. package/dist/actions/list-workspace-connections.js +13 -18
  33. package/dist/actions/list-workspace-connections.js.map +1 -1
  34. package/dist/actions/provider-api-register.d.ts +14 -14
  35. package/dist/actions/remix-workspace-template.d.ts +6 -0
  36. package/dist/actions/update-workspace-app-metadata.d.ts +1 -0
  37. package/dist/actions/update-workspace-app-metadata.js +26 -5
  38. package/dist/actions/update-workspace-app-metadata.js.map +1 -1
  39. package/dist/components/action-query-error.d.ts +2 -1
  40. package/dist/components/action-query-error.d.ts.map +1 -1
  41. package/dist/components/action-query-error.js +25 -2
  42. package/dist/components/action-query-error.js.map +1 -1
  43. package/dist/components/chat-home-apps.d.ts.map +1 -1
  44. package/dist/components/chat-home-apps.js +2 -2
  45. package/dist/components/chat-home-apps.js.map +1 -1
  46. package/dist/components/community-apps-section.d.ts +2 -0
  47. package/dist/components/community-apps-section.d.ts.map +1 -0
  48. package/dist/components/community-apps-section.js +90 -0
  49. package/dist/components/community-apps-section.js.map +1 -0
  50. package/dist/components/dispatch-control-plane.d.ts.map +1 -1
  51. package/dist/components/dispatch-control-plane.js +1 -1
  52. package/dist/components/dispatch-control-plane.js.map +1 -1
  53. package/dist/components/layout/Layout.d.ts +1 -0
  54. package/dist/components/layout/Layout.d.ts.map +1 -1
  55. package/dist/components/layout/Layout.js +7 -0
  56. package/dist/components/layout/Layout.js.map +1 -1
  57. package/dist/components/workspace-app-card.d.ts.map +1 -1
  58. package/dist/components/workspace-app-card.js +63 -9
  59. package/dist/components/workspace-app-card.js.map +1 -1
  60. package/dist/lib/connect-apps.d.ts +1 -2
  61. package/dist/lib/connect-apps.d.ts.map +1 -1
  62. package/dist/lib/connect-apps.js +41 -65
  63. package/dist/lib/connect-apps.js.map +1 -1
  64. package/dist/lib/workspace-apps.d.ts +1 -0
  65. package/dist/lib/workspace-apps.d.ts.map +1 -1
  66. package/dist/lib/workspace-apps.js.map +1 -1
  67. package/dist/routes/pages/$appId.d.ts.map +1 -1
  68. package/dist/routes/pages/$appId.js +1 -1
  69. package/dist/routes/pages/$appId.js.map +1 -1
  70. package/dist/routes/pages/apps.d.ts.map +1 -1
  71. package/dist/routes/pages/apps.js +3 -2
  72. package/dist/routes/pages/apps.js.map +1 -1
  73. package/dist/routes/pages/connect.d.ts.map +1 -1
  74. package/dist/routes/pages/connect.js +5 -7
  75. package/dist/routes/pages/connect.js.map +1 -1
  76. package/dist/server/index.d.ts +2 -0
  77. package/dist/server/index.d.ts.map +1 -1
  78. package/dist/server/index.js +2 -0
  79. package/dist/server/index.js.map +1 -1
  80. package/dist/server/lib/app-creation-store.d.ts +11 -1
  81. package/dist/server/lib/app-creation-store.d.ts.map +1 -1
  82. package/dist/server/lib/app-creation-store.js +379 -92
  83. package/dist/server/lib/app-creation-store.js.map +1 -1
  84. package/dist/server/lib/community-apps.d.ts +45 -0
  85. package/dist/server/lib/community-apps.d.ts.map +1 -0
  86. package/dist/server/lib/community-apps.js +267 -0
  87. package/dist/server/lib/community-apps.js.map +1 -0
  88. package/dist/server/lib/community-source.d.ts +19 -0
  89. package/dist/server/lib/community-source.d.ts.map +1 -0
  90. package/dist/server/lib/community-source.js +118 -0
  91. package/dist/server/lib/community-source.js.map +1 -0
  92. package/dist/server/lib/connect-apps.d.ts +4 -0
  93. package/dist/server/lib/connect-apps.d.ts.map +1 -0
  94. package/dist/server/lib/connect-apps.js +99 -0
  95. package/dist/server/lib/connect-apps.js.map +1 -0
  96. package/dist/server/lib/org-apps-directory-cache.d.ts +20 -0
  97. package/dist/server/lib/org-apps-directory-cache.d.ts.map +1 -0
  98. package/dist/server/lib/org-apps-directory-cache.js +44 -0
  99. package/dist/server/lib/org-apps-directory-cache.js.map +1 -0
  100. package/dist/server/plugins/integrations.js +3 -3
  101. package/dist/server/plugins/integrations.js.map +1 -1
  102. package/dist/shared/labs.d.ts +2 -0
  103. package/dist/shared/labs.d.ts.map +1 -0
  104. package/dist/shared/labs.js +8 -0
  105. package/dist/shared/labs.js.map +1 -0
  106. package/package.json +5 -4
  107. package/src/actions/connect-external-agent.spec.ts +101 -3
  108. package/src/actions/connect-external-agent.ts +12 -11
  109. package/src/actions/index.spec.ts +13 -0
  110. package/src/actions/index.ts +8 -0
  111. package/src/actions/inspect-connect-agent-card.ts +17 -0
  112. package/src/actions/install-curated-community-app.ts +32 -0
  113. package/src/actions/list-connect-marketplace-apps.ts +17 -0
  114. package/src/actions/list-connected-agents.spec.ts +17 -1
  115. package/src/actions/list-connected-agents.ts +7 -2
  116. package/src/actions/list-curated-community-apps.ts +12 -0
  117. package/src/actions/list-workspace-connections.spec.ts +14 -3
  118. package/src/actions/list-workspace-connections.ts +13 -17
  119. package/src/actions/update-workspace-app-metadata.spec.ts +25 -0
  120. package/src/actions/update-workspace-app-metadata.ts +48 -24
  121. package/src/components/action-query-error.spec.tsx +20 -0
  122. package/src/components/action-query-error.tsx +40 -0
  123. package/src/components/chat-home-apps.spec.tsx +32 -1
  124. package/src/components/chat-home-apps.tsx +6 -1
  125. package/src/components/community-apps-section.tsx +224 -0
  126. package/src/components/dispatch-control-plane.tsx +1 -0
  127. package/src/components/layout/Layout.tsx +9 -0
  128. package/src/components/workspace-app-card.spec.tsx +226 -1
  129. package/src/components/workspace-app-card.tsx +115 -12
  130. package/src/lib/connect-apps.spec.ts +118 -66
  131. package/src/lib/connect-apps.ts +47 -82
  132. package/src/lib/workspace-apps.ts +1 -0
  133. package/src/routes/pages/$appId.tsx +1 -0
  134. package/src/routes/pages/apps.spec.tsx +227 -3
  135. package/src/routes/pages/apps.tsx +5 -0
  136. package/src/routes/pages/connect.spec.tsx +53 -6
  137. package/src/routes/pages/connect.tsx +14 -11
  138. package/src/server/index.ts +9 -0
  139. package/src/server/lib/app-creation-store.spec.ts +820 -16
  140. package/src/server/lib/app-creation-store.ts +504 -101
  141. package/src/server/lib/community-apps.spec.ts +367 -0
  142. package/src/server/lib/community-apps.ts +372 -0
  143. package/src/server/lib/community-source.spec.ts +151 -0
  144. package/src/server/lib/community-source.ts +186 -0
  145. package/src/server/lib/connect-apps.ts +149 -0
  146. package/src/server/lib/org-apps-directory-cache.spec.ts +71 -0
  147. package/src/server/lib/org-apps-directory-cache.ts +66 -0
  148. package/src/server/plugins/integrations.spec.ts +60 -0
  149. package/src/server/plugins/integrations.ts +3 -3
  150. package/src/shared/labs.ts +9 -0
@@ -14,28 +14,58 @@ export interface MarketplaceApp {
14
14
  source?: "first-party" | "community";
15
15
  }
16
16
 
17
+ function isPrivateIpv4(hostname: string): boolean {
18
+ const octets = hostname.split(".").map(Number);
19
+ if (
20
+ octets.length !== 4 ||
21
+ octets.some((octet) => !Number.isInteger(octet) || octet < 0 || octet > 255)
22
+ ) {
23
+ return false;
24
+ }
25
+ const [first, second] = octets;
26
+ return (
27
+ first === 0 ||
28
+ first === 10 ||
29
+ first === 127 ||
30
+ (first === 100 && second >= 64 && second <= 127) ||
31
+ (first === 169 && second === 254) ||
32
+ (first === 172 && second >= 16 && second <= 31) ||
33
+ (first === 192 && second === 168)
34
+ );
35
+ }
36
+
37
+ function mappedIpv4(hostname: string): string | null {
38
+ const match = /^::ffff:(.*)$/i.exec(hostname);
39
+ if (!match) return null;
40
+ if (match[1].includes(".")) return match[1];
41
+ const words = match[1].split(":");
42
+ if (
43
+ words.length !== 2 ||
44
+ words.some((word) => !/^[\da-f]{1,4}$/i.test(word))
45
+ ) {
46
+ return null;
47
+ }
48
+ const high = Number.parseInt(words[0], 16);
49
+ const low = Number.parseInt(words[1], 16);
50
+ return `${high >> 8}.${high & 255}.${low >> 8}.${low & 255}`;
51
+ }
52
+
17
53
  export function normalizeConnectUrl(value: string): URL | null {
18
54
  try {
19
55
  const url = new URL(value.trim());
20
56
  const hostname = url.hostname.toLowerCase();
21
57
  const ipv6 = hostname.replace(/^\[|\]$/g, "");
22
- const privateIpv4 =
23
- /^(10\.|127\.|169\.254\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.)/.test(
24
- hostname,
25
- ) ||
26
- hostname === "0.0.0.0" ||
27
- /^100\.(?:6[4-9]|[7-9]\d|1[01]\d|12[0-7])\./.test(hostname);
58
+ const privateIpv4 = isPrivateIpv4(hostname);
59
+ const mappedAddress = mappedIpv4(ipv6);
28
60
  const privateIpv6 =
29
- ipv6 === "::" ||
30
- ipv6 === "::1" ||
31
- /^(?:fc|fd)/.test(ipv6) ||
32
- /^fe[89ab]/.test(ipv6) ||
33
- /^::ffff:(?:10\.|127\.|169\.254\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.)/.test(
34
- ipv6,
35
- );
61
+ ipv6.includes(":") &&
62
+ (ipv6 === "::" ||
63
+ ipv6 === "::1" ||
64
+ /^(?:fc|fd)/.test(ipv6) ||
65
+ /^fe[89ab]/.test(ipv6) ||
66
+ Boolean(mappedAddress && isPrivateIpv4(mappedAddress)));
36
67
  if (
37
- (url.protocol !== "https:" &&
38
- !(url.protocol === "http:" && hostname === "localhost")) ||
68
+ url.protocol !== "https:" ||
39
69
  (hostname !== "localhost" && (privateIpv4 || privateIpv6)) ||
40
70
  url.username ||
41
71
  url.password
@@ -103,73 +133,8 @@ export function buildIdentityConnectUrl(appUrl: string): string {
103
133
  return url.toString();
104
134
  }
105
135
 
106
- export async function fetchMarketplaceApps(
107
- feedUrl = "https://www.agent-native.com/apps.json",
108
- ): Promise<MarketplaceApp[]> {
109
- const response = await fetch(feedUrl, {
110
- headers: { accept: "application/json" },
111
- signal: AbortSignal.timeout(5_000),
112
- });
113
- if (!response.ok)
114
- throw new Error(`Marketplace feed returned ${response.status}`);
115
- const length = Number(response.headers.get("content-length") ?? 0);
116
- if (length > 1_000_000) throw new Error("Marketplace feed is too large");
117
- const body = await response.text();
118
- if (body.length > 1_000_000) throw new Error("Marketplace feed is too large");
119
- const payload = JSON.parse(body) as { apps?: unknown };
120
- if (!Array.isArray(payload.apps))
121
- throw new Error("Marketplace feed is invalid");
122
- const candidates = payload.apps.filter((entry): entry is MarketplaceApp => {
123
- if (!entry || typeof entry !== "object") return false;
124
- const app = entry as Record<string, unknown>;
125
- const url =
126
- typeof app.url === "string" ? normalizeConnectUrl(app.url) : null;
127
- return (
128
- typeof app.id === "string" &&
129
- typeof app.name === "string" &&
130
- typeof app.description === "string" &&
131
- typeof app.url === "string" &&
132
- Boolean(url) &&
133
- Array.isArray(app.capabilities) &&
134
- app.capabilities.every((capability) => typeof capability === "string")
135
- );
136
- });
137
- const verified = await Promise.allSettled(
138
- candidates.map(async (app) => {
139
- const card = await fetchConnectAgentCard(app.url);
140
- return card.connect ? { ...app, capabilities: ["connect"] } : null;
141
- }),
142
- );
143
- return verified.flatMap((result) =>
144
- result.status === "fulfilled" && result.value ? [result.value] : [],
145
- );
146
- }
147
-
148
- export async function fetchConnectAgentCard(
149
- appUrl: string,
150
- ): Promise<ConnectAgentCard> {
151
- const target = normalizeConnectUrl(appUrl);
152
- if (!target) throw new Error("Enter a secure app URL.");
153
- const response = await fetch(
154
- appPathUrl(target.toString(), "/.well-known/agent-card.json").toString(),
155
- {
156
- headers: { accept: "application/json" },
157
- signal: AbortSignal.timeout(5_000),
158
- },
159
- );
160
- if (!response.ok) throw new Error(`Agent card returned ${response.status}`);
161
- const length = Number(response.headers.get("content-length") ?? 0);
162
- if (length > 256_000) throw new Error("Agent card is too large");
163
- const body = await response.text();
164
- if (body.length > 256_000) throw new Error("Agent card is too large");
165
- const card = parseConnectAgentCard(
166
- JSON.parse(body),
167
- target.origin,
168
- target.pathname,
169
- );
170
- if (!card)
171
- throw new Error("This app has an invalid or incompatible agent card.");
172
- return card;
136
+ export function buildAgentCardUrl(appUrl: string): string {
137
+ return appPathUrl(appUrl, "/.well-known/agent-card.json").toString();
173
138
  }
174
139
 
175
140
  function appPathUrl(appUrl: string, suffix: string): URL {
@@ -18,6 +18,7 @@ export interface WorkspaceAppSummary {
18
18
  id: string;
19
19
  name: string;
20
20
  description?: string;
21
+ whenToUse?: string[];
21
22
  defaultDescriptionKey?: string;
22
23
  path: string;
23
24
  homePath?: string;
@@ -98,6 +98,7 @@ export default function WorkspaceAppCatchAllRoute() {
98
98
  <ActionQueryError
99
99
  error={appsQuery.error}
100
100
  onRetry={() => void appsQuery.refetch()}
101
+ queryName="list-workspace-apps"
101
102
  />
102
103
  </DispatchShell>
103
104
  );
@@ -11,12 +11,18 @@ const queryState = vi.hoisted(() => ({
11
11
  connectedApps: [] as Array<Record<string, unknown>>,
12
12
  connectedAppsError: null as Error | null,
13
13
  connectedAppsLoading: false,
14
+ communityAppsEnabled: false,
15
+ communityApps: [] as Array<Record<string, unknown>>,
16
+ installCommunityApp: vi.fn(),
17
+ installCommunityAppResult: null as Record<string, unknown> | null,
18
+ sendToAgentChat: vi.fn(),
14
19
  }));
15
20
 
16
21
  vi.mock("@agent-native/core/client/hooks", () => ({
17
22
  useActionQuery: (action: string) => {
18
23
  const workspaceApps = action === "list-workspace-apps";
19
24
  const connectedApps = action === "list-connected-agents";
25
+ const communityApps = action === "list-curated-community-apps";
20
26
  const error = workspaceApps
21
27
  ? queryState.workspaceAppsError
22
28
  : connectedApps
@@ -27,7 +33,9 @@ vi.mock("@agent-native/core/client/hooks", () => ({
27
33
  ? queryState.workspaceApps
28
34
  : connectedApps
29
35
  ? queryState.connectedApps
30
- : { name: null, displayName: null, appCount: 0 },
36
+ : communityApps
37
+ ? queryState.communityApps
38
+ : { name: null, displayName: null, appCount: 0 },
31
39
  error,
32
40
  isError: Boolean(error),
33
41
  isLoading: workspaceApps
@@ -38,6 +46,30 @@ vi.mock("@agent-native/core/client/hooks", () => ({
38
46
  refetch: vi.fn(),
39
47
  };
40
48
  },
49
+ useActionMutation: (
50
+ _action: string,
51
+ options?: { onSuccess?: (result: Record<string, unknown>) => void },
52
+ ) => ({
53
+ mutate: (input: unknown) => {
54
+ queryState.installCommunityApp(input);
55
+ if (queryState.installCommunityAppResult) {
56
+ options?.onSuccess?.(queryState.installCommunityAppResult);
57
+ }
58
+ },
59
+ isPending: false,
60
+ }),
61
+ }));
62
+
63
+ vi.mock("@agent-native/core/client/agent-chat", () => ({
64
+ sendToAgentChat: queryState.sendToAgentChat,
65
+ }));
66
+
67
+ vi.mock("@agent-native/core/client/labs", () => ({
68
+ useLab: () => queryState.communityAppsEnabled,
69
+ }));
70
+
71
+ vi.mock("../../shared/labs.js", () => ({
72
+ COMMUNITY_APPS_LAB: { key: "dispatch.community-apps", defaultEnabled: false },
41
73
  }));
42
74
 
43
75
  vi.mock("@agent-native/core/client/i18n", () => ({
@@ -48,8 +80,16 @@ vi.mock("@agent-native/core/client/i18n", () => ({
48
80
  }));
49
81
 
50
82
  vi.mock("../../components/action-query-error", () => ({
51
- ActionQueryError: ({ error }: { error: Error }) => (
52
- <div role="alert">{error.message}</div>
83
+ ActionQueryError: ({
84
+ error,
85
+ queryName,
86
+ }: {
87
+ error: Error;
88
+ queryName?: string;
89
+ }) => (
90
+ <div role="alert">
91
+ {queryName}: {error.message}
92
+ </div>
53
93
  ),
54
94
  }));
55
95
 
@@ -92,6 +132,11 @@ describe("Dispatch apps route", () => {
92
132
  queryState.connectedApps = [];
93
133
  queryState.connectedAppsError = null;
94
134
  queryState.connectedAppsLoading = false;
135
+ queryState.communityAppsEnabled = false;
136
+ queryState.communityApps = [];
137
+ queryState.installCommunityApp.mockReset();
138
+ queryState.installCommunityAppResult = null;
139
+ queryState.sendToAgentChat.mockReset();
95
140
  container = document.createElement("div");
96
141
  document.body.appendChild(container);
97
142
  root = createRoot(container);
@@ -129,4 +174,183 @@ describe("Dispatch apps route", () => {
129
174
 
130
175
  expect(container.textContent).toContain("No workspace apps");
131
176
  });
177
+
178
+ it("identifies each app discovery query when both fail", async () => {
179
+ queryState.workspaceAppsError = new Error("Workspace app list failed");
180
+ queryState.connectedAppsError = new Error("Connected agent list failed");
181
+
182
+ await renderAppsRoute();
183
+
184
+ const errors = Array.from(
185
+ container.querySelectorAll('[role="alert"]'),
186
+ (alert) => alert.textContent,
187
+ );
188
+ expect(errors).toContain("list-workspace-apps: Workspace app list failed");
189
+ expect(errors).toContain(
190
+ "list-connected-agents: Connected agent list failed",
191
+ );
192
+ });
193
+
194
+ it("hides community apps while the Lab is off", async () => {
195
+ await renderAppsRoute();
196
+
197
+ expect(container.textContent).not.toContain("dispatch.pages.communityApps");
198
+ });
199
+
200
+ it("lists the reviewed catalog and starts the selected install", async () => {
201
+ queryState.communityAppsEnabled = true;
202
+ queryState.communityApps = [
203
+ {
204
+ id: "community-qr",
205
+ name: "QR",
206
+ description: "Generate QR images.",
207
+ appId: "community-qr",
208
+ repo: "BuilderIO/agent-native",
209
+ ref: "main",
210
+ license: "MIT",
211
+ provenance: "Reviewed source",
212
+ templatePath: "templates/community/qr",
213
+ installed: false,
214
+ pending: false,
215
+ installable: true,
216
+ },
217
+ ];
218
+
219
+ await renderAppsRoute();
220
+
221
+ expect(container.textContent).toContain("QR");
222
+ expect(
223
+ container.querySelector<HTMLAnchorElement>("a[href*='github.com']")?.href,
224
+ ).toContain("/tree/main/templates/community/qr");
225
+ const installButton = Array.from(
226
+ container.querySelectorAll<HTMLButtonElement>("button"),
227
+ ).find((button) =>
228
+ button.textContent?.includes("dispatch.pages.communityAppsInstall"),
229
+ );
230
+ expect(installButton).toBeDefined();
231
+ await act(async () => installButton?.click());
232
+ expect(queryState.installCommunityApp).toHaveBeenCalledWith({
233
+ sourceId: "community-qr",
234
+ });
235
+ });
236
+
237
+ it("sends verified source files to the local code agent", async () => {
238
+ queryState.communityAppsEnabled = true;
239
+ queryState.communityApps = [
240
+ {
241
+ id: "community-qr",
242
+ name: "QR",
243
+ description: "Generate QR images.",
244
+ appId: "community-qr",
245
+ repo: "BuilderIO/agent-native",
246
+ ref: "main",
247
+ license: "MIT",
248
+ provenance: "Reviewed source",
249
+ templatePath: "templates/community/qr",
250
+ installed: false,
251
+ pending: false,
252
+ installable: true,
253
+ },
254
+ ];
255
+ queryState.installCommunityAppResult = {
256
+ mode: "local-agent",
257
+ prompt: "Create this app from the pinned files.",
258
+ attachments: [
259
+ {
260
+ type: "file",
261
+ name: "community-qr/README.md",
262
+ contentType: "text/plain",
263
+ text: "Verified source",
264
+ },
265
+ ],
266
+ };
267
+
268
+ await renderAppsRoute();
269
+ const installButton = Array.from(
270
+ container.querySelectorAll<HTMLButtonElement>("button"),
271
+ ).find((button) =>
272
+ button.textContent?.includes("dispatch.pages.communityAppsInstall"),
273
+ );
274
+ await act(async () => installButton?.click());
275
+
276
+ expect(queryState.sendToAgentChat).toHaveBeenCalledWith({
277
+ message: "Create this app from the pinned files.",
278
+ attachments: [
279
+ {
280
+ type: "file",
281
+ name: "community-qr/README.md",
282
+ contentType: "text/plain",
283
+ text: "Verified source",
284
+ },
285
+ ],
286
+ submit: true,
287
+ type: "code",
288
+ newTab: true,
289
+ reuseEmptyTab: true,
290
+ });
291
+ });
292
+
293
+ it("explains when a community app ID is already in use", async () => {
294
+ queryState.communityAppsEnabled = true;
295
+ queryState.communityApps = [
296
+ {
297
+ id: "community-qr",
298
+ name: "QR",
299
+ description: "Generate QR images.",
300
+ appId: "qr",
301
+ repo: "BuilderIO/agent-native",
302
+ ref: "main",
303
+ license: "MIT",
304
+ provenance: "Reviewed source",
305
+ templatePath: "templates/community/qr",
306
+ installed: false,
307
+ pending: false,
308
+ installable: false,
309
+ installUnavailableReason: "app-id-conflict",
310
+ },
311
+ ];
312
+
313
+ await renderAppsRoute();
314
+
315
+ expect(container.textContent).toContain(
316
+ "dispatch.pages.communityAppsIdConflict",
317
+ );
318
+ expect(
319
+ Array.from(container.querySelectorAll("button")).some((button) =>
320
+ button.textContent?.includes("dispatch.pages.communityAppsInstall"),
321
+ ),
322
+ ).toBe(false);
323
+ });
324
+
325
+ it("explains when Builder-mode community installs cannot be verified", async () => {
326
+ queryState.communityAppsEnabled = true;
327
+ queryState.communityApps = [
328
+ {
329
+ id: "community-qr",
330
+ name: "QR",
331
+ description: "Generate QR images.",
332
+ appId: "community-qr",
333
+ repo: "BuilderIO/agent-native",
334
+ ref: "main",
335
+ license: "MIT",
336
+ provenance: "Reviewed source",
337
+ templatePath: "templates/community/qr",
338
+ installed: false,
339
+ pending: false,
340
+ installable: false,
341
+ installUnavailableReason: "builder-install-unverified",
342
+ },
343
+ ];
344
+
345
+ await renderAppsRoute();
346
+
347
+ expect(container.textContent).toContain(
348
+ "dispatch.pages.communityAppsBuilderInstallUnavailable",
349
+ );
350
+ expect(
351
+ Array.from(container.querySelectorAll("button")).some((button) =>
352
+ button.textContent?.includes("dispatch.pages.communityAppsInstall"),
353
+ ),
354
+ ).toBe(false);
355
+ });
132
356
  });
@@ -16,6 +16,7 @@ import {
16
16
  APP_LIST_GRID_ROW_CLASS,
17
17
  AppList,
18
18
  } from "../../components/app-list-row";
19
+ import { CommunityAppsSection } from "../../components/community-apps-section";
19
20
  import { ConnectedAppCard } from "../../components/connected-app-card";
20
21
  import { CreateAppPopover } from "../../components/create-app-popover";
21
22
  import { DispatchShell } from "../../components/dispatch-shell";
@@ -184,12 +185,14 @@ function AppsRoute() {
184
185
  <ActionQueryError
185
186
  error={appsQuery.error}
186
187
  onRetry={() => void appsQuery.refetch()}
188
+ queryName="list-workspace-apps"
187
189
  />
188
190
  ) : null}
189
191
  {connectedAppsQuery.isError ? (
190
192
  <ActionQueryError
191
193
  error={connectedAppsQuery.error}
192
194
  onRetry={() => void connectedAppsQuery.refetch()}
195
+ queryName="list-connected-agents"
193
196
  />
194
197
  ) : null}
195
198
  {showAppSkeletons ? (
@@ -346,6 +349,8 @@ function AppsRoute() {
346
349
  </section>
347
350
  </Collapsible>
348
351
  ) : null}
352
+
353
+ <CommunityAppsSection />
349
354
  </div>
350
355
  </DispatchShell>
351
356
  );
@@ -8,7 +8,8 @@ import ConnectRoute from "./connect";
8
8
  const connectState = vi.hoisted(() => ({
9
9
  enabled: false,
10
10
  status: "ready" as "loading" | "ready",
11
- useQuery: vi.fn(() => ({ data: [], isLoading: false, isError: false })),
11
+ useActionQuery: vi.fn(() => ({ data: [], isLoading: false, isError: false })),
12
+ useActionMutation: vi.fn(() => ({ mutateAsync: vi.fn() })),
12
13
  }));
13
14
 
14
15
  vi.mock("@agent-native/core/client/feature-flags", () => ({
@@ -23,8 +24,9 @@ vi.mock("../../components/dispatch-shell", () => ({
23
24
  vi.mock("@agent-native/core/client/i18n", () => ({
24
25
  useT: () => (key: string) => key,
25
26
  }));
26
- vi.mock("@tanstack/react-query", () => ({
27
- useQuery: connectState.useQuery,
27
+ vi.mock("@agent-native/core/client/hooks", () => ({
28
+ useActionQuery: connectState.useActionQuery,
29
+ useActionMutation: connectState.useActionMutation,
28
30
  }));
29
31
 
30
32
  describe("ConnectRoute", () => {
@@ -34,7 +36,8 @@ describe("ConnectRoute", () => {
34
36
  beforeEach(() => {
35
37
  connectState.enabled = false;
36
38
  connectState.status = "ready";
37
- connectState.useQuery.mockClear();
39
+ connectState.useActionQuery.mockClear();
40
+ connectState.useActionMutation.mockClear();
38
41
  container = document.createElement("div");
39
42
  document.body.appendChild(container);
40
43
  root = createRoot(container);
@@ -48,13 +51,57 @@ describe("ConnectRoute", () => {
48
51
  it("renders nothing and does not fetch when the labs flag is off", async () => {
49
52
  await act(async () => root.render(<ConnectRoute />));
50
53
  expect(container.innerHTML).toBe("");
51
- expect(connectState.useQuery).not.toHaveBeenCalled();
54
+ expect(connectState.useActionQuery).not.toHaveBeenCalled();
55
+ expect(connectState.useActionMutation).not.toHaveBeenCalled();
52
56
  });
53
57
 
54
58
  it("shows the page skeleton while the labs flag is loading", async () => {
55
59
  connectState.status = "loading";
56
60
  await act(async () => root.render(<ConnectRoute />));
57
61
  expect(container.querySelector('[aria-busy="true"]')).not.toBeNull();
58
- expect(connectState.useQuery).not.toHaveBeenCalled();
62
+ expect(connectState.useActionQuery).not.toHaveBeenCalled();
63
+ expect(connectState.useActionMutation).not.toHaveBeenCalled();
64
+ });
65
+
66
+ it("inspects a URL through the server action and renders its card", async () => {
67
+ connectState.enabled = true;
68
+ const mutateAsync = vi.fn().mockResolvedValue({
69
+ name: "Slides",
70
+ description: "A slide workspace",
71
+ url: "https://slides.example.com/_agent-native/a2a",
72
+ connect: true,
73
+ });
74
+ connectState.useActionMutation.mockReturnValue({ mutateAsync } as never);
75
+
76
+ await act(async () => root.render(<ConnectRoute />));
77
+ expect(connectState.useActionQuery).toHaveBeenCalledWith(
78
+ "list-connect-marketplace-apps",
79
+ {},
80
+ { staleTime: 5 * 60 * 1000, retry: 1 },
81
+ );
82
+
83
+ const input = container.querySelector("input");
84
+ expect(input).not.toBeNull();
85
+ await act(async () => {
86
+ const setter = Object.getOwnPropertyDescriptor(
87
+ HTMLInputElement.prototype,
88
+ "value",
89
+ )?.set;
90
+ setter?.call(input, "https://slides.example.com");
91
+ input?.dispatchEvent(new Event("input", { bubbles: true }));
92
+ });
93
+
94
+ const inspectButton = [...container.querySelectorAll("button")].find(
95
+ (button) => button.textContent?.includes("dispatch.pages.inspectApp"),
96
+ );
97
+ expect(inspectButton).toBeDefined();
98
+ await act(async () => inspectButton?.click());
99
+
100
+ expect(mutateAsync).toHaveBeenCalledWith({
101
+ url: "https://slides.example.com/",
102
+ });
103
+ expect(container.querySelector("[data-connect-card] h3")?.textContent).toBe(
104
+ "Slides",
105
+ );
59
106
  });
60
107
  });
@@ -1,7 +1,10 @@
1
1
  import { useFeatureFlagState } from "@agent-native/core/client/feature-flags";
2
+ import {
3
+ useActionMutation,
4
+ useActionQuery,
5
+ } from "@agent-native/core/client/hooks";
2
6
  import { useT } from "@agent-native/core/client/i18n";
3
7
  import { IconExternalLink, IconPlugConnected } from "@tabler/icons-react";
4
- import { useQuery } from "@tanstack/react-query";
5
8
  import { useState } from "react";
6
9
 
7
10
  import { AppIcon } from "../../components/app-icon";
@@ -11,8 +14,6 @@ import { Input } from "../../components/ui/input";
11
14
  import { Skeleton } from "../../components/ui/skeleton";
12
15
  import {
13
16
  buildIdentityConnectUrl,
14
- fetchConnectAgentCard,
15
- fetchMarketplaceApps,
16
17
  normalizeConnectUrl,
17
18
  type ConnectAgentCard,
18
19
  type MarketplaceApp,
@@ -143,13 +144,15 @@ function ConnectAppsContent() {
143
144
  const [card, setCard] = useState<ConnectAgentCard | null>(null);
144
145
  const [cardError, setCardError] = useState(false);
145
146
  const [cardLoading, setCardLoading] = useState(false);
146
- const feedQuery = useQuery({
147
- queryKey: ["connect-apps-marketplace"],
148
- queryFn: () => fetchMarketplaceApps(),
149
- enabled: true,
150
- staleTime: 5 * 60 * 1000,
151
- retry: 1,
152
- });
147
+ const feedQuery = useActionQuery(
148
+ "list-connect-marketplace-apps",
149
+ {},
150
+ {
151
+ staleTime: 5 * 60 * 1000,
152
+ retry: 1,
153
+ },
154
+ );
155
+ const inspectCard = useActionMutation("inspect-connect-agent-card");
153
156
 
154
157
  const target = normalizeConnectUrl(url);
155
158
 
@@ -159,7 +162,7 @@ function ConnectAppsContent() {
159
162
  setCardError(false);
160
163
  setCard(null);
161
164
  try {
162
- setCard(await fetchConnectAgentCard(target.toString()));
165
+ setCard(await inspectCard.mutateAsync({ url: target.toString() }));
163
166
  } catch {
164
167
  setCardError(true);
165
168
  } finally {
@@ -7,6 +7,15 @@ import {
7
7
  import { dispatchActions } from "../actions/index.js";
8
8
  import type { DispatchConfig } from "../config.js";
9
9
  import { dispatchAccessDescriptor } from "../shared/app-roles.js";
10
+ export {
11
+ listOrganizationVisibleWorkspaceApps,
12
+ listWorkspaceApps,
13
+ } from "./lib/app-creation-store.js";
14
+ export {
15
+ invalidateOrgAppsDirectoryCache,
16
+ orgAppsDirectoryAccessCache,
17
+ orgAppsDirectoryCache,
18
+ } from "./lib/org-apps-directory-cache.js";
10
19
 
11
20
  defineAppRoles(dispatchAccessDescriptor, { allowOrgAdmins: true });
12
21