@agent-native/dispatch 0.41.1-nightly-20261104110058 → 0.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (195) hide show
  1. package/CHANGELOG.md +26 -55
  2. package/dist/actions/get-hosted-apps-settings.d.ts +5 -0
  3. package/dist/actions/get-hosted-apps-settings.d.ts.map +1 -0
  4. package/dist/actions/get-hosted-apps-settings.js +25 -0
  5. package/dist/actions/get-hosted-apps-settings.js.map +1 -0
  6. package/dist/actions/index.d.ts.map +1 -1
  7. package/dist/actions/index.js +4 -0
  8. package/dist/actions/index.js.map +1 -1
  9. package/dist/actions/list-connected-agents.d.ts +1 -0
  10. package/dist/actions/list-connected-agents.js +11 -10
  11. package/dist/actions/list-connected-agents.js.map +1 -1
  12. package/dist/actions/list_apps.js +3 -2
  13. package/dist/actions/list_apps.js.map +1 -1
  14. package/dist/actions/provider-api-register.d.ts +10 -10
  15. package/dist/actions/set-hosted-apps-settings.d.ts +7 -0
  16. package/dist/actions/set-hosted-apps-settings.d.ts.map +1 -0
  17. package/dist/actions/set-hosted-apps-settings.js +35 -0
  18. package/dist/actions/set-hosted-apps-settings.js.map +1 -0
  19. package/dist/components/app-card-tiers.d.ts +13 -0
  20. package/dist/components/app-card-tiers.d.ts.map +1 -0
  21. package/dist/components/app-card-tiers.js +22 -0
  22. package/dist/components/app-card-tiers.js.map +1 -0
  23. package/dist/components/app-list-row.d.ts +12 -4
  24. package/dist/components/app-list-row.d.ts.map +1 -1
  25. package/dist/components/app-list-row.js +19 -4
  26. package/dist/components/app-list-row.js.map +1 -1
  27. package/dist/components/app-open-actions.d.ts +4 -0
  28. package/dist/components/app-open-actions.d.ts.map +1 -1
  29. package/dist/components/app-open-actions.js +9 -0
  30. package/dist/components/app-open-actions.js.map +1 -1
  31. package/dist/components/app-pin-button.d.ts +9 -0
  32. package/dist/components/app-pin-button.d.ts.map +1 -0
  33. package/dist/components/app-pin-button.js +29 -0
  34. package/dist/components/app-pin-button.js.map +1 -0
  35. package/dist/components/app-tier-sections.d.ts +27 -0
  36. package/dist/components/app-tier-sections.d.ts.map +1 -0
  37. package/dist/components/app-tier-sections.js +44 -0
  38. package/dist/components/app-tier-sections.js.map +1 -0
  39. package/dist/components/chat-home-apps.d.ts.map +1 -1
  40. package/dist/components/chat-home-apps.js +19 -26
  41. package/dist/components/chat-home-apps.js.map +1 -1
  42. package/dist/components/connected-app-card.d.ts +3 -1
  43. package/dist/components/connected-app-card.d.ts.map +1 -1
  44. package/dist/components/connected-app-card.js +6 -3
  45. package/dist/components/connected-app-card.js.map +1 -1
  46. package/dist/components/create-app-popover.d.ts +4 -0
  47. package/dist/components/create-app-popover.d.ts.map +1 -1
  48. package/dist/components/create-app-popover.js +6 -0
  49. package/dist/components/create-app-popover.js.map +1 -1
  50. package/dist/components/dispatch-control-plane.d.ts.map +1 -1
  51. package/dist/components/dispatch-control-plane.js +25 -35
  52. package/dist/components/dispatch-control-plane.js.map +1 -1
  53. package/dist/components/hosted-apps-settings-group.d.ts +6 -0
  54. package/dist/components/hosted-apps-settings-group.d.ts.map +1 -0
  55. package/dist/components/hosted-apps-settings-group.js +76 -0
  56. package/dist/components/hosted-apps-settings-group.js.map +1 -0
  57. package/dist/components/layout/Layout.d.ts +6 -4
  58. package/dist/components/layout/Layout.d.ts.map +1 -1
  59. package/dist/components/layout/Layout.js +97 -80
  60. package/dist/components/layout/Layout.js.map +1 -1
  61. package/dist/components/workspace-app-card.d.ts.map +1 -1
  62. package/dist/components/workspace-app-card.js +9 -20
  63. package/dist/components/workspace-app-card.js.map +1 -1
  64. package/dist/components/workspace-app-layout-notice.d.ts +9 -0
  65. package/dist/components/workspace-app-layout-notice.d.ts.map +1 -0
  66. package/dist/components/workspace-app-layout-notice.js +27 -0
  67. package/dist/components/workspace-app-layout-notice.js.map +1 -0
  68. package/dist/components/workspace-app-search.d.ts.map +1 -1
  69. package/dist/components/workspace-app-search.js +11 -5
  70. package/dist/components/workspace-app-search.js.map +1 -1
  71. package/dist/components/workspace-template-card.d.ts.map +1 -1
  72. package/dist/components/workspace-template-card.js +7 -2
  73. package/dist/components/workspace-template-card.js.map +1 -1
  74. package/dist/db/index.d.ts.map +1 -1
  75. package/dist/db/index.js +1 -0
  76. package/dist/db/index.js.map +1 -1
  77. package/dist/lib/other-apps.d.ts +10 -1
  78. package/dist/lib/other-apps.d.ts.map +1 -1
  79. package/dist/lib/other-apps.js +7 -1
  80. package/dist/lib/other-apps.js.map +1 -1
  81. package/dist/lib/workspace-app-layout.d.ts +62 -6
  82. package/dist/lib/workspace-app-layout.d.ts.map +1 -1
  83. package/dist/lib/workspace-app-layout.js +196 -41
  84. package/dist/lib/workspace-app-layout.js.map +1 -1
  85. package/dist/lib/workspace-app-lists.d.ts +17 -0
  86. package/dist/lib/workspace-app-lists.d.ts.map +1 -0
  87. package/dist/lib/workspace-app-lists.js +21 -0
  88. package/dist/lib/workspace-app-lists.js.map +1 -0
  89. package/dist/lib/workspace-apps.d.ts +3 -1
  90. package/dist/lib/workspace-apps.d.ts.map +1 -1
  91. package/dist/lib/workspace-apps.js +5 -1
  92. package/dist/lib/workspace-apps.js.map +1 -1
  93. package/dist/routes/pages/apps.d.ts.map +1 -1
  94. package/dist/routes/pages/apps.js +23 -35
  95. package/dist/routes/pages/apps.js.map +1 -1
  96. package/dist/routes/pages/settings-page.d.ts.map +1 -1
  97. package/dist/routes/pages/settings-page.js +24 -3
  98. package/dist/routes/pages/settings-page.js.map +1 -1
  99. package/dist/server/lib/app-creation-store.d.ts.map +1 -1
  100. package/dist/server/lib/app-creation-store.js +13 -18
  101. package/dist/server/lib/app-creation-store.js.map +1 -1
  102. package/dist/server/lib/app-roles.d.ts +2 -0
  103. package/dist/server/lib/app-roles.d.ts.map +1 -1
  104. package/dist/server/lib/app-roles.js +26 -7
  105. package/dist/server/lib/app-roles.js.map +1 -1
  106. package/dist/server/lib/hosted-apps-settings-schema.d.ts +6 -0
  107. package/dist/server/lib/hosted-apps-settings-schema.d.ts.map +1 -0
  108. package/dist/server/lib/hosted-apps-settings-schema.js +6 -0
  109. package/dist/server/lib/hosted-apps-settings-schema.js.map +1 -0
  110. package/dist/server/lib/hosted-apps-settings.d.ts +27 -0
  111. package/dist/server/lib/hosted-apps-settings.d.ts.map +1 -0
  112. package/dist/server/lib/hosted-apps-settings.js +72 -0
  113. package/dist/server/lib/hosted-apps-settings.js.map +1 -0
  114. package/dist/server/plugins/agent-chat.js +6 -3
  115. package/dist/server/plugins/agent-chat.js.map +1 -1
  116. package/dist/server/plugins/integrations.js +3 -3
  117. package/dist/server/plugins/integrations.js.map +1 -1
  118. package/dist/shared/hosted-apps-settings.d.ts +3 -0
  119. package/dist/shared/hosted-apps-settings.d.ts.map +1 -0
  120. package/dist/shared/hosted-apps-settings.js +3 -0
  121. package/dist/shared/hosted-apps-settings.js.map +1 -0
  122. package/package.json +4 -4
  123. package/src/actions/get-hosted-apps-settings.ts +29 -0
  124. package/src/actions/hosted-apps-settings.spec.ts +100 -0
  125. package/src/actions/index.ts +4 -0
  126. package/src/actions/list-connected-agents.spec.ts +67 -0
  127. package/src/actions/list-connected-agents.ts +14 -15
  128. package/src/actions/list_apps.spec.ts +16 -0
  129. package/src/actions/list_apps.ts +5 -2
  130. package/src/actions/set-hosted-apps-settings.ts +42 -0
  131. package/src/components/app-card-tiers.tsx +85 -0
  132. package/src/components/app-list-row.spec.tsx +54 -0
  133. package/src/components/app-list-row.tsx +50 -8
  134. package/src/components/app-open-actions.tsx +16 -0
  135. package/src/components/app-pin-button.spec.tsx +68 -0
  136. package/src/components/app-pin-button.tsx +66 -0
  137. package/src/components/app-tier-sections.spec.tsx +155 -0
  138. package/src/components/app-tier-sections.tsx +158 -0
  139. package/src/components/chat-home-apps.spec.tsx +135 -29
  140. package/src/components/chat-home-apps.tsx +115 -111
  141. package/src/components/connected-app-card.spec.tsx +18 -2
  142. package/src/components/connected-app-card.tsx +18 -1
  143. package/src/components/create-app-popover.tsx +22 -0
  144. package/src/components/dispatch-control-plane.spec.tsx +89 -20
  145. package/src/components/dispatch-control-plane.tsx +91 -125
  146. package/src/components/hosted-apps-settings-group.spec.tsx +218 -0
  147. package/src/components/hosted-apps-settings-group.tsx +189 -0
  148. package/src/components/layout/Layout.spec.tsx +356 -49
  149. package/src/components/layout/Layout.tsx +189 -142
  150. package/src/components/workspace-app-card.spec.tsx +33 -29
  151. package/src/components/workspace-app-card.tsx +13 -31
  152. package/src/components/workspace-app-layout-notice.spec.tsx +106 -0
  153. package/src/components/workspace-app-layout-notice.tsx +65 -0
  154. package/src/components/workspace-app-search.spec.tsx +53 -0
  155. package/src/components/workspace-app-search.tsx +39 -32
  156. package/src/components/workspace-template-card.spec.tsx +10 -0
  157. package/src/components/workspace-template-card.tsx +7 -2
  158. package/src/db/index.spec.ts +9 -0
  159. package/src/db/index.ts +1 -0
  160. package/src/lib/other-apps.spec.ts +25 -0
  161. package/src/lib/other-apps.ts +9 -0
  162. package/src/lib/workspace-app-layout.spec.ts +61 -3
  163. package/src/lib/workspace-app-layout.store.spec.tsx +261 -0
  164. package/src/lib/workspace-app-layout.ts +265 -48
  165. package/src/lib/workspace-app-lists.ts +32 -0
  166. package/src/lib/workspace-apps.ts +10 -2
  167. package/src/routes/pages/apps.spec.tsx +133 -5
  168. package/src/routes/pages/apps.tsx +79 -140
  169. package/src/routes/pages/chat.spec.tsx +66 -49
  170. package/src/routes/pages/settings-page.tsx +31 -3
  171. package/src/routes/pages/settings.spec.tsx +37 -0
  172. package/src/server/lib/app-creation-store.spec.ts +14 -0
  173. package/src/server/lib/app-creation-store.ts +16 -29
  174. package/src/server/lib/app-creation-store.write-observation.spec.ts +216 -0
  175. package/src/server/lib/app-roles.spec.ts +48 -1
  176. package/src/server/lib/app-roles.ts +44 -12
  177. package/src/server/lib/hosted-apps-settings-schema.ts +6 -0
  178. package/src/server/lib/hosted-apps-settings.spec.ts +154 -0
  179. package/src/server/lib/hosted-apps-settings.ts +108 -0
  180. package/src/server/plugins/agent-chat.spec.ts +16 -0
  181. package/src/server/plugins/agent-chat.ts +7 -2
  182. package/src/server/plugins/integrations.ts +3 -3
  183. package/src/shared/hosted-apps-settings.ts +3 -0
  184. package/src/styles/dispatch.css +40 -11
  185. package/dist/components/available-apps-section.d.ts +0 -9
  186. package/dist/components/available-apps-section.d.ts.map +0 -1
  187. package/dist/components/available-apps-section.js +0 -97
  188. package/dist/components/available-apps-section.js.map +0 -1
  189. package/dist/components/other-apps-section.d.ts +0 -35
  190. package/dist/components/other-apps-section.d.ts.map +0 -1
  191. package/dist/components/other-apps-section.js +0 -79
  192. package/dist/components/other-apps-section.js.map +0 -1
  193. package/src/components/available-apps-section.tsx +0 -239
  194. package/src/components/other-apps-section.spec.ts +0 -50
  195. package/src/components/other-apps-section.tsx +0 -254
@@ -5,7 +5,7 @@ import { MemoryRouter } from "react-router";
5
5
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
6
6
 
7
7
  const queryState = vi.hoisted(() => ({
8
- workspaceApps: [] as Array<Record<string, unknown>>,
8
+ workspaceApps: [] as Array<Record<string, unknown>> | undefined,
9
9
  workspaceAppsError: null as Error | null,
10
10
  workspaceAppsLoading: false,
11
11
  connectedApps: [] as Array<Record<string, unknown>>,
@@ -16,6 +16,8 @@ const queryState = vi.hoisted(() => ({
16
16
  installCommunityApp: vi.fn(),
17
17
  installCommunityAppResult: null as Record<string, unknown> | null,
18
18
  sendToAgentChat: vi.fn(),
19
+ pinnedIds: [] as string[],
20
+ canManageOrg: false,
19
21
  }));
20
22
 
21
23
  vi.mock("@agent-native/core/client/hooks", () => ({
@@ -93,8 +95,19 @@ vi.mock("../../components/action-query-error", () => ({
93
95
  ),
94
96
  }));
95
97
 
98
+ vi.mock("@agent-native/core/client/org", async (importOriginal) => ({
99
+ ...(await importOriginal<typeof import("@agent-native/core/client/org")>()),
100
+ useOrgRole: () => ({
101
+ org: { orgId: "org-1" },
102
+ canManageOrg: queryState.canManageOrg,
103
+ isLoading: false,
104
+ role: queryState.canManageOrg ? "admin" : "member",
105
+ }),
106
+ }));
107
+
96
108
  vi.mock("../../components/create-app-popover", () => ({
97
- CreateAppPopover: () => null,
109
+ CreateAppPopover: () => <span data-create-app />,
110
+ NewAppButton: () => <span data-create-app />,
98
111
  }));
99
112
 
100
113
  vi.mock("../../components/dispatch-shell", () => ({
@@ -108,16 +121,18 @@ vi.mock("../../components/workspace-app-search", () => ({
108
121
  WorkspaceAppSearchEmpty: () => <div>No search results</div>,
109
122
  }));
110
123
 
111
- vi.mock("../../lib/workspace-app-layout", () => ({
124
+ vi.mock("../../lib/workspace-app-layout", async (importOriginal) => ({
125
+ ...(await importOriginal<typeof import("../../lib/workspace-app-layout")>()),
112
126
  orderWorkspaceApps: (apps: unknown[]) => apps,
113
127
  useWorkspaceAppLayout: () => ({
114
- layout: { pinnedIds: [], orderedIds: [] },
128
+ layout: { pinnedIds: queryState.pinnedIds, orderedIds: [] },
115
129
  persistenceError: null,
116
130
  togglePinned: vi.fn(),
117
131
  }),
118
132
  workspaceAppMatchesQuery: () => true,
119
133
  }));
120
134
 
135
+ import { TooltipProvider } from "../../components/ui/tooltip";
121
136
  import AppsRouteEntry from "./apps";
122
137
 
123
138
  describe("Dispatch apps route", () => {
@@ -137,6 +152,8 @@ describe("Dispatch apps route", () => {
137
152
  queryState.installCommunityApp.mockReset();
138
153
  queryState.installCommunityAppResult = null;
139
154
  queryState.sendToAgentChat.mockReset();
155
+ queryState.pinnedIds = [];
156
+ queryState.canManageOrg = false;
140
157
  container = document.createElement("div");
141
158
  document.body.appendChild(container);
142
159
  root = createRoot(container);
@@ -152,7 +169,9 @@ describe("Dispatch apps route", () => {
152
169
  await act(async () => {
153
170
  root.render(
154
171
  <MemoryRouter initialEntries={["/apps"]}>
155
- <AppsRouteEntry />
172
+ <TooltipProvider>
173
+ <AppsRouteEntry />
174
+ </TooltipProvider>
156
175
  </MemoryRouter>,
157
176
  );
158
177
  });
@@ -169,12 +188,121 @@ describe("Dispatch apps route", () => {
169
188
  expect(container.textContent).not.toContain("No workspace apps");
170
189
  });
171
190
 
191
+ it.each([
192
+ [true, "link"],
193
+ [false, "disabled"],
194
+ ] as const)(
195
+ "puts pinned hosted apps first and hosted apps last (admin %s)",
196
+ async (canManageOrg, manage) => {
197
+ queryState.canManageOrg = canManageOrg;
198
+ queryState.pinnedIds = ["clips"];
199
+ queryState.workspaceApps = [
200
+ { id: "notes", name: "Notes", path: "/notes", status: "ready" },
201
+ ];
202
+ queryState.connectedApps = [
203
+ {
204
+ id: "mail",
205
+ name: "Mail",
206
+ url: "https://mail.agent-native.com",
207
+ source: "builtin",
208
+ },
209
+ {
210
+ id: "clips",
211
+ name: "Clips",
212
+ url: "https://clips.agent-native.com",
213
+ source: "builtin",
214
+ },
215
+ ];
216
+
217
+ await renderAppsRoute();
218
+
219
+ const sections = [...container.querySelectorAll("section[aria-label]")];
220
+ expect(
221
+ sections.map((section) => section.getAttribute("aria-label")),
222
+ ).toEqual([
223
+ "dispatch.pages.pinnedApps",
224
+ "dispatch.pages.workspaceApps",
225
+ "dispatch.pages.hostedApps",
226
+ ]);
227
+ expect(sections[0]?.textContent).toContain("Clips");
228
+ expect(sections[1]?.textContent).toContain("Notes");
229
+ expect(sections[2]?.textContent).toContain("Mail");
230
+ expect(sections[2]?.textContent).not.toContain("Clips");
231
+ const mailRow = sections[2]!;
232
+ expect(
233
+ mailRow.querySelector('a[href^="https://mail.agent-native.com"] svg'),
234
+ ).not.toBeNull();
235
+ expect(
236
+ mailRow
237
+ .querySelector('button[aria-label="dispatch.pages.pinApp"]')
238
+ ?.getAttribute("aria-pressed"),
239
+ ).toBe("false");
240
+ const manageLink = mailRow.querySelector('a[href="/settings/apps"]');
241
+ const manageButton = [...mailRow.querySelectorAll("button")].find(
242
+ (button) => button.textContent === "dispatch.pages.manageHostedApps",
243
+ );
244
+ if (manage === "link") {
245
+ expect(manageLink?.textContent).toBe("dispatch.pages.manageHostedApps");
246
+ } else {
247
+ expect(manageLink).toBeNull();
248
+ expect(manageButton?.disabled).toBe(true);
249
+ }
250
+ },
251
+ );
252
+
253
+ it.each([
254
+ ["loading", null],
255
+ ["failed", new Error("Workspace app list failed")],
256
+ ])(
257
+ "lists no apps until the workspace list arrives (%s)",
258
+ async (_state, error) => {
259
+ queryState.workspaceApps = undefined;
260
+ queryState.workspaceAppsError = error;
261
+ queryState.connectedApps = [
262
+ {
263
+ id: "mail",
264
+ name: "Mail",
265
+ url: "https://mail.agent-native.com",
266
+ source: "builtin",
267
+ },
268
+ ];
269
+
270
+ await renderAppsRoute();
271
+
272
+ expect(container.textContent).not.toContain("No workspace apps");
273
+ expect(container.textContent).not.toContain("Mail");
274
+ },
275
+ );
276
+
172
277
  it("shows the empty state after successful app discovery", async () => {
173
278
  await renderAppsRoute();
174
279
 
175
280
  expect(container.textContent).toContain("No workspace apps");
176
281
  });
177
282
 
283
+ it("keeps Add app when app discovery fails", async () => {
284
+ queryState.workspaceApps = [];
285
+ queryState.connectedAppsError = new Error("discovery down");
286
+ await renderAppsRoute();
287
+
288
+ expect(container.querySelectorAll("[data-create-app]")).toHaveLength(1);
289
+ });
290
+
291
+ it("offers one Add app when only hosted apps are listed", async () => {
292
+ queryState.connectedApps = [
293
+ {
294
+ id: "mail",
295
+ name: "Mail",
296
+ url: "https://mail.agent-native.com",
297
+ source: "builtin",
298
+ },
299
+ ];
300
+ await renderAppsRoute();
301
+
302
+ expect(container.textContent).toContain("No workspace apps");
303
+ expect(container.querySelectorAll("[data-create-app]")).toHaveLength(1);
304
+ });
305
+
178
306
  it("identifies each app discovery query when both fail", async () => {
179
307
  queryState.workspaceAppsError = new Error("Workspace app list failed");
180
308
  queryState.connectedAppsError = new Error("Connected agent list failed");
@@ -11,14 +11,14 @@ import { useState } from "react";
11
11
  import { Outlet, useParams } from "react-router";
12
12
 
13
13
  import { ActionQueryError } from "../../components/action-query-error";
14
- import {
15
- APP_LIST_GRID_CLASS,
16
- APP_LIST_GRID_ROW_CLASS,
17
- AppList,
18
- } from "../../components/app-list-row";
14
+ import { AppCardTiers } from "../../components/app-card-tiers";
15
+ import { AppList, AppListRow } from "../../components/app-list-row";
16
+ import { AppListActions } from "../../components/app-tier-sections";
19
17
  import { CommunityAppsSection } from "../../components/community-apps-section";
20
- import { ConnectedAppCard } from "../../components/connected-app-card";
21
- import { CreateAppPopover } from "../../components/create-app-popover";
18
+ import {
19
+ CreateAppPopover,
20
+ NewAppButton,
21
+ } from "../../components/create-app-popover";
22
22
  import { DispatchShell } from "../../components/dispatch-shell";
23
23
  import { Button } from "../../components/ui/button";
24
24
  import {
@@ -32,20 +32,15 @@ import {
32
32
  WorkspaceAppSearch,
33
33
  WorkspaceAppSearchEmpty,
34
34
  } from "../../components/workspace-app-search";
35
- import {
36
- filterBuiltInApps,
37
- type ConnectedAppSummary,
38
- } from "../../lib/other-apps";
39
35
  import { cn } from "../../lib/utils";
40
36
  import {
37
+ hasPinnedWorkspaceApp,
41
38
  orderWorkspaceApps,
42
39
  useWorkspaceAppLayout,
43
40
  workspaceAppMatchesQuery,
44
41
  } from "../../lib/workspace-app-layout";
45
- import {
46
- isWorkspaceAppVisibleInDefaultLaunchers,
47
- type WorkspaceAppSummary,
48
- } from "../../lib/workspace-apps";
42
+ import { useWorkspaceAppLists } from "../../lib/workspace-app-lists";
43
+ import { isWorkspaceAppVisibleInDefaultLaunchers } from "../../lib/workspace-apps";
49
44
 
50
45
  export function meta() {
51
46
  return [{ title: "Apps — Dispatch" }];
@@ -67,16 +62,14 @@ function AppsRoute() {
67
62
  const [showPending, setShowPending] = useState(false);
68
63
  const [showHidden, setShowHidden] = useState(false);
69
64
  const [searchQuery, setSearchQuery] = useState("");
70
- const { layout, persistenceError, togglePinned } = useWorkspaceAppLayout();
71
- const appsQuery = useActionQuery("list-workspace-apps", {
72
- includeAgentCards: false,
73
- includeArchived: true,
74
- });
75
- const connectedAppsQuery = useActionQuery<ConnectedAppSummary[]>(
76
- "list-connected-agents",
77
- {},
78
- );
79
- const { data: apps = [], isLoading: appsLoading } = appsQuery;
65
+ const { layout } = useWorkspaceAppLayout();
66
+ const {
67
+ status: appsStatus,
68
+ workspaceApps: apps,
69
+ hostedApps: defaultApps,
70
+ appsQuery,
71
+ connectedAppsQuery,
72
+ } = useWorkspaceAppLists();
80
73
  const { data: workspace } = useActionQuery(
81
74
  "get-workspace-info",
82
75
  {},
@@ -84,18 +77,14 @@ function AppsRoute() {
84
77
  );
85
78
  const ws = workspace as WorkspaceInfo | undefined;
86
79
  const workspaceLabel = ws?.displayName ?? ws?.name ?? null;
87
- const allApps = (apps as WorkspaceAppSummary[]).filter(
88
- isWorkspaceAppVisibleInDefaultLaunchers,
89
- );
80
+ const allApps = apps.filter(isWorkspaceAppVisibleInDefaultLaunchers);
90
81
  const visibleApps = allApps.filter((app) => !app.archived);
91
82
  const activeApps = visibleApps.filter((app) => app.status !== "pending");
92
83
  const pendingApps = visibleApps.filter((app) => app.status === "pending");
93
84
  const archivedApps = allApps.filter((app) => app.archived);
94
- const defaultApps = filterBuiltInApps(connectedAppsQuery.data ?? [], allApps);
95
- const orderedActiveApps = orderWorkspaceApps(activeApps, layout);
96
85
  const orderedPendingApps = orderWorkspaceApps(pendingApps, layout);
97
86
  const orderedArchivedApps = orderWorkspaceApps(archivedApps, layout);
98
- const filteredActiveApps = orderedActiveApps.filter((app) =>
87
+ const filteredActiveApps = activeApps.filter((app) =>
99
88
  workspaceAppMatchesQuery(app, searchQuery),
100
89
  );
101
90
  const filteredPendingApps = orderedPendingApps.filter((app) =>
@@ -112,11 +101,14 @@ function AppsRoute() {
112
101
  filteredPendingApps.length > 0 ||
113
102
  filteredArchivedApps.length > 0 ||
114
103
  filteredDefaultApps.length > 0;
115
- const showAppSkeletons =
116
- (appsLoading || connectedAppsQuery.isLoading) &&
117
- allApps.length === 0 &&
118
- defaultApps.length === 0;
104
+ const showAppSkeletons = appsStatus === "loading";
119
105
  const appDiscoveryFailed = appsQuery.isError || connectedAppsQuery.isError;
106
+ const showsEmptyAppsState =
107
+ !showAppSkeletons &&
108
+ !appDiscoveryFailed &&
109
+ !searchQuery.trim() &&
110
+ activeApps.length === 0 &&
111
+ pendingApps.length === 0;
120
112
 
121
113
  return (
122
114
  <DispatchShell
@@ -129,58 +121,8 @@ function AppsRoute() {
129
121
  : t("dispatch.pages.appsDescription")
130
122
  }
131
123
  >
132
- <div className="space-y-8">
124
+ <div className="mx-auto w-full max-w-[1000px] space-y-8">
133
125
  <section className="space-y-3">
134
- <div className="flex flex-wrap items-center justify-between gap-3">
135
- <div className="flex min-w-0 items-start gap-2">
136
- <IconApps
137
- size={16}
138
- className="mt-0.5 shrink-0 text-muted-foreground"
139
- />
140
- <div className="min-w-0">
141
- <h2 className="truncate text-sm font-semibold text-foreground">
142
- {t("dispatch.pages.yourApps", { defaultValue: "Your apps" })}
143
- </h2>
144
- </div>
145
- </div>
146
- <div className="flex min-w-0 flex-wrap items-center justify-end gap-2">
147
- {!showAppSkeletons ? (
148
- <WorkspaceAppSearch
149
- className="w-full sm:w-[250px]"
150
- query={searchQuery}
151
- onQueryChange={setSearchQuery}
152
- />
153
- ) : null}
154
- {activeApps.length > 0 ||
155
- pendingApps.length > 0 ||
156
- defaultApps.length > 0 ? (
157
- <CreateAppPopover
158
- align="end"
159
- trigger={
160
- <Button size="sm" variant="outline">
161
- <IconPlus size={15} className="mr-1.5" />
162
- {t("dispatch.pages.addApp", {
163
- defaultValue: "Add app",
164
- })}
165
- </Button>
166
- }
167
- />
168
- ) : null}
169
- </div>
170
- </div>
171
-
172
- {!showAppSkeletons && allApps.length > 0 && persistenceError ? (
173
- <p className="text-xs text-muted-foreground" role="status">
174
- {persistenceError === "both"
175
- ? t("dispatch.pages.appPinSaveFailed", {
176
- defaultValue: "App pins could not be saved.",
177
- })
178
- : t("dispatch.pages.appPinSavedLocally", {
179
- defaultValue: "App pins are saved on this device only.",
180
- })}
181
- </p>
182
- ) : null}
183
-
184
126
  {appsQuery.isError ? (
185
127
  <ActionQueryError
186
128
  error={appsQuery.error}
@@ -195,39 +137,50 @@ function AppsRoute() {
195
137
  queryName="list-connected-agents"
196
138
  />
197
139
  ) : null}
198
- {showAppSkeletons ? (
199
- <AppsSkeletonGrid />
200
- ) : filteredActiveApps.length > 0 ||
201
- filteredDefaultApps.length > 0 ? (
202
- <AppList className={APP_LIST_GRID_CLASS}>
203
- {filteredActiveApps.map((app) => (
204
- <WorkspaceAppCard
205
- key={app.id}
206
- app={app}
207
- className={APP_LIST_GRID_ROW_CLASS}
208
- isPinned={layout.pinnedIds.includes(app.id.toLowerCase())}
209
- onTogglePinned={() => togglePinned(app.id)}
210
- />
211
- ))}
212
- {filteredDefaultApps.map((app) => (
213
- <ConnectedAppCard
214
- key={app.id}
215
- app={app}
216
- className={APP_LIST_GRID_ROW_CLASS}
217
- />
218
- ))}
219
- </AppList>
220
- ) : appDiscoveryFailed ? null : !hasSearchResults &&
221
- searchQuery.trim() ? (
222
- <WorkspaceAppSearchEmpty
223
- query={searchQuery}
224
- onClear={() => setSearchQuery("")}
225
- />
226
- ) : searchQuery.trim() ? null : pendingApps.length > 0 ? (
227
- <EmptyActiveAppsState />
228
- ) : (
229
- <EmptyAppsState />
230
- )}
140
+ <AppCardTiers
141
+ workspaceApps={showAppSkeletons ? [] : filteredActiveApps}
142
+ hostedApps={showAppSkeletons ? [] : filteredDefaultApps}
143
+ appsTitle={t("dispatch.pages.workspaceApps")}
144
+ hasPinnedApps={
145
+ !showAppSkeletons &&
146
+ hasPinnedWorkspaceApp([...activeApps, ...defaultApps], layout)
147
+ }
148
+ actions={
149
+ <AppListActions
150
+ search={
151
+ !showAppSkeletons ? (
152
+ <WorkspaceAppSearch
153
+ className="w-full sm:w-[250px]"
154
+ query={searchQuery}
155
+ onQueryChange={setSearchQuery}
156
+ />
157
+ ) : undefined
158
+ }
159
+ // The empty state carries its own Add app.
160
+ newApp={
161
+ !showAppSkeletons && !showsEmptyAppsState ? (
162
+ <NewAppButton />
163
+ ) : undefined
164
+ }
165
+ />
166
+ }
167
+ appsFallback={
168
+ showAppSkeletons ? (
169
+ <AppsSkeletonGrid />
170
+ ) : appDiscoveryFailed ? null : searchQuery.trim() ? (
171
+ hasSearchResults ? null : (
172
+ <WorkspaceAppSearchEmpty
173
+ query={searchQuery}
174
+ onClear={() => setSearchQuery("")}
175
+ />
176
+ )
177
+ ) : showsEmptyAppsState ? (
178
+ <EmptyAppsState />
179
+ ) : activeApps.length > 0 ? null : (
180
+ <EmptyActiveAppsState />
181
+ )
182
+ }
183
+ />
231
184
  </section>
232
185
 
233
186
  {pendingApps.length > 0 &&
@@ -276,13 +229,9 @@ function AppsRoute() {
276
229
  </CollapsibleTrigger>
277
230
  </div>
278
231
  <CollapsibleContent>
279
- <AppList className={APP_LIST_GRID_CLASS}>
232
+ <AppList layout="grid">
280
233
  {filteredPendingApps.map((app) => (
281
- <WorkspaceAppCard
282
- key={app.id}
283
- app={app}
284
- className={APP_LIST_GRID_ROW_CLASS}
285
- />
234
+ <WorkspaceAppCard key={app.id} app={app} />
286
235
  ))}
287
236
  </AppList>
288
237
  </CollapsibleContent>
@@ -336,13 +285,9 @@ function AppsRoute() {
336
285
  </CollapsibleTrigger>
337
286
  </div>
338
287
  <CollapsibleContent>
339
- <AppList className={APP_LIST_GRID_CLASS}>
288
+ <AppList layout="grid">
340
289
  {filteredArchivedApps.map((app) => (
341
- <WorkspaceAppCard
342
- key={app.id}
343
- app={app}
344
- className={APP_LIST_GRID_ROW_CLASS}
345
- />
290
+ <WorkspaceAppCard key={app.id} app={app} />
346
291
  ))}
347
292
  </AppList>
348
293
  </CollapsibleContent>
@@ -358,22 +303,16 @@ function AppsRoute() {
358
303
 
359
304
  function AppsSkeletonGrid() {
360
305
  return (
361
- <AppList className={APP_LIST_GRID_CLASS}>
306
+ <AppList layout="grid">
362
307
  {Array.from({ length: 3 }).map((_, index) => (
363
- <div
364
- key={index}
365
- className={cn(
366
- "flex min-w-0 items-center gap-3 border-b px-4 py-3.5 last:border-b-0",
367
- APP_LIST_GRID_ROW_CLASS,
368
- )}
369
- >
308
+ <AppListRow key={index}>
370
309
  <Skeleton className="size-8 shrink-0 rounded-md" />
371
310
  <div className="min-w-0 flex-1 space-y-2">
372
311
  <Skeleton className="h-4 w-32" />
373
312
  <Skeleton className="h-3 w-2/3" />
374
313
  </div>
375
314
  <Skeleton className="h-8 w-24 shrink-0 rounded-md" />
376
- </div>
315
+ </AppListRow>
377
316
  ))}
378
317
  </AppList>
379
318
  );