@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
@@ -3,35 +3,33 @@ import {
3
3
  navigateWithAgentChatViewTransition,
4
4
  useChatModels,
5
5
  } from "@agent-native/core/client/agent-chat";
6
- import { useActionQuery } from "@agent-native/core/client/hooks";
7
6
  import { useT } from "@agent-native/core/client/i18n";
8
7
  import { useOrgRole } from "@agent-native/core/client/org";
9
8
  import {
10
9
  PromptBar,
11
10
  PromptComposer,
12
11
  } from "@agent-native/toolkit/app/chat/composer/index";
13
- import { IconChevronDown, IconClockHour4, IconPlus } from "@tabler/icons-react";
14
- import type { ReactNode } from "react";
12
+ import { IconChevronDown, IconClockHour4 } from "@tabler/icons-react";
15
13
  import { useState } from "react";
16
- import { Link, useNavigate } from "react-router";
14
+ import { useNavigate } from "react-router";
17
15
 
18
- import { cn } from "../lib/utils";
16
+ import type { ConnectedAppSummary } from "../lib/other-apps";
19
17
  import {
18
+ hasPinnedWorkspaceApp,
20
19
  orderWorkspaceApps,
21
20
  useWorkspaceAppLayout,
22
21
  workspaceAppMatchesQuery,
23
22
  } from "../lib/workspace-app-layout";
23
+ import { useWorkspaceAppLists } from "../lib/workspace-app-lists";
24
24
  import {
25
25
  isWorkspaceAppVisibleInDefaultLaunchers,
26
26
  type WorkspaceAppSummary,
27
27
  } from "../lib/workspace-apps";
28
28
  import { ActionQueryError } from "./action-query-error";
29
- import {
30
- APP_LIST_GRID_CLASS,
31
- AppList,
32
- APP_LIST_GRID_ROW_CLASS,
33
- } from "./app-list-row";
34
- import { CreateAppPopover } from "./create-app-popover";
29
+ import { AppCardTiers } from "./app-card-tiers";
30
+ import { AppList, AppListRow } from "./app-list-row";
31
+ import { AppListActions, NoWorkspaceAppsText } from "./app-tier-sections";
32
+ import { NewAppButton } from "./create-app-popover";
35
33
  import { useSetPageTitle } from "./layout/HeaderActions";
36
34
  import { Button } from "./ui/button";
37
35
  import {
@@ -46,23 +44,6 @@ import {
46
44
  WorkspaceAppSearchEmpty,
47
45
  } from "./workspace-app-search";
48
46
 
49
- function SectionHeader({
50
- title,
51
- action,
52
- }: {
53
- title: string;
54
- action?: ReactNode;
55
- }) {
56
- return (
57
- <div className="flex min-w-0 items-center justify-between gap-3">
58
- <h2 className="truncate text-sm font-semibold text-foreground">
59
- {title}
60
- </h2>
61
- {action ? <div className="shrink-0">{action}</div> : null}
62
- </div>
63
- );
64
- }
65
-
66
47
  function CommandPanel() {
67
48
  const t = useT();
68
49
  const { org } = useOrgRole();
@@ -159,106 +140,79 @@ function CommandPanel() {
159
140
 
160
141
  function AppsPanel({
161
142
  apps,
143
+ hostedApps,
162
144
  isLoading,
163
145
  }: {
164
146
  apps: WorkspaceAppSummary[];
147
+ hostedApps: ConnectedAppSummary[];
165
148
  isLoading: boolean;
166
149
  }) {
167
150
  const t = useT();
168
151
  const [showPending, setShowPending] = useState(false);
169
152
  const [searchQuery, setSearchQuery] = useState("");
170
- const { layout, persistenceError, togglePinned } = useWorkspaceAppLayout();
153
+ const { layout } = useWorkspaceAppLayout();
171
154
  const visibleApps = apps.filter(
172
155
  (app) => isWorkspaceAppVisibleInDefaultLaunchers(app) && !app.archived,
173
156
  );
174
157
  const activeApps = visibleApps.filter((app) => app.status !== "pending");
175
158
  const pendingApps = visibleApps.filter((app) => app.status === "pending");
176
- const orderedActiveApps = orderWorkspaceApps(activeApps, layout);
177
- const orderedPendingApps = orderWorkspaceApps(pendingApps, layout);
178
- const filteredActiveApps = orderedActiveApps.filter((app) =>
159
+ const filteredActiveApps = activeApps.filter((app) =>
179
160
  workspaceAppMatchesQuery(app, searchQuery),
180
161
  );
181
- const filteredPendingApps = orderedPendingApps.filter((app) =>
162
+ const filteredPendingApps = orderWorkspaceApps(pendingApps, layout).filter(
163
+ (app) => workspaceAppMatchesQuery(app, searchQuery),
164
+ );
165
+ const filteredHostedApps = hostedApps.filter((app) =>
182
166
  workspaceAppMatchesQuery(app, searchQuery),
183
167
  );
184
168
  const hasSearchResults =
185
- filteredActiveApps.length > 0 || filteredPendingApps.length > 0;
186
- const showSkeletons =
187
- isLoading && activeApps.length === 0 && pendingApps.length === 0;
169
+ filteredActiveApps.length > 0 ||
170
+ filteredPendingApps.length > 0 ||
171
+ filteredHostedApps.length > 0;
188
172
 
189
173
  return (
190
174
  <section className="mx-auto flex w-full max-w-[1000px] flex-col gap-3">
191
- <SectionHeader
192
- title="Apps"
193
- action={
194
- <div className="flex min-w-0 flex-wrap items-center justify-end gap-1.5">
195
- <Button variant="ghost" size="sm" asChild>
196
- <Link to="/apps" className="text-muted-foreground">
197
- View all
198
- </Link>
199
- </Button>
200
- {!showSkeletons &&
201
- (visibleApps.length > 0 || Boolean(searchQuery.trim())) ? (
202
- <WorkspaceAppSearch
203
- className="w-[220px]"
175
+ <AppCardTiers
176
+ workspaceApps={isLoading ? [] : filteredActiveApps}
177
+ hostedApps={isLoading ? [] : filteredHostedApps}
178
+ appsTitle={t("dispatch.pages.workspaceApps")}
179
+ hasPinnedApps={
180
+ !isLoading &&
181
+ hasPinnedWorkspaceApp([...activeApps, ...hostedApps], layout)
182
+ }
183
+ actions={
184
+ <AppListActions
185
+ viewAllHref="/apps"
186
+ search={
187
+ !isLoading &&
188
+ (visibleApps.length > 0 ||
189
+ hostedApps.length > 0 ||
190
+ Boolean(searchQuery.trim())) ? (
191
+ <WorkspaceAppSearch
192
+ className="w-full sm:w-[220px]"
193
+ query={searchQuery}
194
+ onQueryChange={setSearchQuery}
195
+ />
196
+ ) : undefined
197
+ }
198
+ newApp={<NewAppButton />}
199
+ />
200
+ }
201
+ appsFallback={
202
+ isLoading ? (
203
+ <OverviewAppsSkeleton />
204
+ ) : searchQuery.trim() ? (
205
+ hasSearchResults ? null : (
206
+ <WorkspaceAppSearchEmpty
204
207
  query={searchQuery}
205
- onQueryChange={setSearchQuery}
208
+ onClear={() => setSearchQuery("")}
206
209
  />
207
- ) : null}
208
- <CreateAppPopover
209
- align="end"
210
- trigger={
211
- <Button variant="default" size="sm" className="gap-1.5">
212
- <IconPlus size={14} />
213
- {t("dispatch.pages.newApp", { defaultValue: "New" })}
214
- </Button>
215
- }
216
- />
217
- </div>
210
+ )
211
+ ) : activeApps.length === 0 && pendingApps.length === 0 ? (
212
+ <NoWorkspaceAppsText />
213
+ ) : null
218
214
  }
219
215
  />
220
- {!showSkeletons && visibleApps.length > 0 && persistenceError ? (
221
- <p className="text-xs text-muted-foreground" role="status">
222
- {persistenceError === "both"
223
- ? t("dispatch.pages.appPinSaveFailed", {
224
- defaultValue: "App pins could not be saved.",
225
- })
226
- : t("dispatch.pages.appPinSavedLocally", {
227
- defaultValue: "App pins are saved on this device only.",
228
- })}
229
- </p>
230
- ) : null}
231
- {showSkeletons ? (
232
- <OverviewAppsSkeleton />
233
- ) : searchQuery.trim() && !hasSearchResults ? (
234
- <WorkspaceAppSearchEmpty
235
- query={searchQuery}
236
- onClear={() => setSearchQuery("")}
237
- />
238
- ) : (
239
- <>
240
- <AppList className={APP_LIST_GRID_CLASS}>
241
- {filteredActiveApps.map((app) => (
242
- <WorkspaceAppCard
243
- key={app.id}
244
- app={app}
245
- className={APP_LIST_GRID_ROW_CLASS}
246
- isPinned={layout.pinnedIds.includes(app.id.toLowerCase())}
247
- onTogglePinned={() => togglePinned(app.id)}
248
- />
249
- ))}
250
- {filteredActiveApps.length === 0 &&
251
- pendingApps.length === 0 &&
252
- !searchQuery.trim() ? (
253
- <p className="px-4 py-3 text-sm text-muted-foreground">
254
- {t("dispatch.pages.noApps", {
255
- defaultValue: "No apps yet.",
256
- })}
257
- </p>
258
- ) : null}
259
- </AppList>
260
- </>
261
- )}
262
216
  {pendingApps.length > 0 &&
263
217
  (!searchQuery.trim() || filteredPendingApps.length > 0) ? (
264
218
  <Collapsible
@@ -305,13 +259,9 @@ function AppsPanel({
305
259
  </CollapsibleTrigger>
306
260
  </div>
307
261
  <CollapsibleContent>
308
- <AppList className={APP_LIST_GRID_CLASS}>
262
+ <AppList layout="grid">
309
263
  {filteredPendingApps.map((app) => (
310
- <WorkspaceAppCard
311
- key={app.id}
312
- app={app}
313
- className={APP_LIST_GRID_ROW_CLASS}
314
- />
264
+ <WorkspaceAppCard key={app.id} app={app} />
315
265
  ))}
316
266
  </AppList>
317
267
  </CollapsibleContent>
@@ -324,22 +274,16 @@ function AppsPanel({
324
274
 
325
275
  function OverviewAppsSkeleton() {
326
276
  return (
327
- <AppList className={APP_LIST_GRID_CLASS}>
277
+ <AppList layout="grid">
328
278
  {Array.from({ length: 4 }).map((_, index) => (
329
- <div
330
- key={index}
331
- className={cn(
332
- "flex min-w-0 items-center gap-3 border-b px-4 py-3.5 last:border-b-0",
333
- APP_LIST_GRID_ROW_CLASS,
334
- )}
335
- >
279
+ <AppListRow key={index}>
336
280
  <Skeleton className="size-8 shrink-0 rounded-xl" />
337
281
  <div className="min-w-0 flex-1 space-y-2">
338
282
  <Skeleton className="h-4 w-32" />
339
283
  <Skeleton className="h-3 w-2/3" />
340
284
  </div>
341
285
  <Skeleton className="h-9 w-24 shrink-0 rounded-md" />
342
- </div>
286
+ </AppListRow>
343
287
  ))}
344
288
  </AppList>
345
289
  );
@@ -347,23 +291,45 @@ function OverviewAppsSkeleton() {
347
291
 
348
292
  export function DispatchControlPlane() {
349
293
  useSetPageTitle(<span aria-hidden />);
350
- const appsQuery = useActionQuery<WorkspaceAppSummary[]>(
351
- "list-workspace-apps",
352
- { includeAgentCards: false, includeArchived: true },
353
- );
354
- const { data: workspaceApps = [], isLoading: appsLoading } = appsQuery;
294
+ const {
295
+ status: appsStatus,
296
+ workspaceApps,
297
+ hostedApps,
298
+ appsQuery,
299
+ connectedAppsQuery,
300
+ } = useWorkspaceAppLists();
355
301
 
356
302
  return (
357
303
  <div className="flex flex-col gap-8">
358
304
  <CommandPanel />
359
- {appsQuery.isError ? (
305
+ {appsStatus === "error" ? (
360
306
  <ActionQueryError
361
307
  error={appsQuery.error}
362
308
  onRetry={() => void appsQuery.refetch()}
363
309
  queryName="list-workspace-apps"
364
310
  />
365
311
  ) : (
366
- <AppsPanel apps={workspaceApps ?? []} isLoading={appsLoading} />
312
+ <>
313
+ {appsQuery.isError ? (
314
+ <ActionQueryError
315
+ error={appsQuery.error}
316
+ onRetry={() => void appsQuery.refetch()}
317
+ queryName="list-workspace-apps"
318
+ />
319
+ ) : null}
320
+ {connectedAppsQuery.isError ? (
321
+ <ActionQueryError
322
+ error={connectedAppsQuery.error}
323
+ onRetry={() => void connectedAppsQuery.refetch()}
324
+ queryName="list-connected-agents"
325
+ />
326
+ ) : null}
327
+ <AppsPanel
328
+ apps={workspaceApps}
329
+ hostedApps={hostedApps}
330
+ isLoading={appsStatus === "loading"}
331
+ />
332
+ </>
367
333
  )}
368
334
  </div>
369
335
  );
@@ -0,0 +1,218 @@
1
+ // @vitest-environment happy-dom
2
+ import React, { act } from "react";
3
+ import { createRoot, type Root } from "react-dom/client";
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
+
6
+ const state = vi.hoisted(() => ({
7
+ data: undefined as unknown,
8
+ error: null as unknown,
9
+ connected: [] as unknown[] | undefined,
10
+ connectedError: null as unknown,
11
+ mutate: vi.fn(),
12
+ refetch: vi.fn(),
13
+ options: {} as {
14
+ onError?: (error: Error) => void;
15
+ onSuccess?: () => void;
16
+ },
17
+ toastError: vi.fn(),
18
+ }));
19
+
20
+ vi.mock("@agent-native/core/client/hooks", async (importOriginal) => ({
21
+ ...(await importOriginal<typeof import("@agent-native/core/client/hooks")>()),
22
+ useActionQuery: (name: string) =>
23
+ name === "get-hosted-apps-settings"
24
+ ? {
25
+ data: state.data,
26
+ isError: state.error !== null,
27
+ error: state.error,
28
+ refetch: state.refetch,
29
+ }
30
+ : name === "list-connected-agents"
31
+ ? {
32
+ data: state.connectedError ? undefined : state.connected,
33
+ isError: state.connectedError !== null,
34
+ error: state.connectedError,
35
+ refetch: state.refetch,
36
+ }
37
+ : { data: [], isError: false, error: null, refetch: state.refetch },
38
+ useActionMutation: (_name: string, options: typeof state.options) => {
39
+ state.options = options;
40
+ return { mutate: state.mutate, isPending: false };
41
+ },
42
+ }));
43
+
44
+ vi.mock("@agent-native/core/client/i18n", () => ({
45
+ useT: () => (key: string, options?: { name?: string }) =>
46
+ options?.name ? `${key}:${options.name}` : key,
47
+ }));
48
+
49
+ vi.mock("sonner", () => ({ toast: { error: state.toastError } }));
50
+
51
+ import { HostedAppsSettingsGroup } from "./hosted-apps-settings-group";
52
+
53
+ const hosted = (id: string, name: string, url: string, extra = {}) => ({
54
+ id,
55
+ name,
56
+ description: "",
57
+ url,
58
+ source: "builtin",
59
+ ...extra,
60
+ });
61
+
62
+ describe("HostedAppsSettingsGroup", () => {
63
+ let container: HTMLDivElement;
64
+ let root: Root;
65
+
66
+ beforeEach(() => {
67
+ vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
68
+ state.data = { showHostedApps: true };
69
+ state.error = null;
70
+ state.connected = [
71
+ hosted("mail", "Mail", "https://mail.agent-native.com/"),
72
+ ];
73
+ state.connectedError = null;
74
+ state.mutate.mockReset();
75
+ state.refetch.mockReset().mockResolvedValue(undefined);
76
+ state.toastError.mockReset();
77
+ container = document.createElement("div");
78
+ document.body.appendChild(container);
79
+ root = createRoot(container);
80
+ });
81
+
82
+ afterEach(() => {
83
+ act(() => root.unmount());
84
+ container.remove();
85
+ vi.unstubAllGlobals();
86
+ });
87
+
88
+ const toggle = () =>
89
+ container.querySelector<HTMLButtonElement>('[role="switch"]')!;
90
+ const mailRow = () => container.querySelector("#hosted-app-mail");
91
+ const rowsState = () =>
92
+ mailRow()?.closest("[data-state]")?.getAttribute("data-state");
93
+
94
+ it("lists each hosted app with its host and an Open link", async () => {
95
+ await act(async () => root.render(<HostedAppsSettingsGroup />));
96
+
97
+ expect(toggle().getAttribute("aria-checked")).toBe("true");
98
+ expect(mailRow()?.textContent).toContain("Mail");
99
+ expect(mailRow()?.textContent).toContain("mail.agent-native.com");
100
+ const open = mailRow()?.querySelector("a");
101
+ expect(open?.getAttribute("href")).toBe("https://mail.agent-native.com/");
102
+ expect(open?.getAttribute("target")).toBe("_blank");
103
+ expect(open?.getAttribute("aria-label")).toBe(
104
+ "dispatch.pages.chatFirstOpenApp:Mail",
105
+ );
106
+ expect(rowsState()).toBe("shown");
107
+ });
108
+
109
+ it("lists the launchers' hosted apps A to Z, including ones the setting hides", async () => {
110
+ state.data = { showHostedApps: false };
111
+ state.connected = [
112
+ hosted("slides", "Slides", "https://s/", { hiddenFromLaunchers: true }),
113
+ hosted("chat", "Chat", "https://c/"),
114
+ hosted("analytics", "Analytics", "https://a/"),
115
+ { ...hosted("partner", "Partner", "https://p/"), source: "custom" },
116
+ ];
117
+ await act(async () => root.render(<HostedAppsSettingsGroup />));
118
+
119
+ expect(
120
+ [...container.querySelectorAll('[id^="hosted-app-"]')].map(
121
+ (row) => row.id,
122
+ ),
123
+ ).toEqual(["hosted-app-analytics", "hosted-app-slides"]);
124
+ expect(
125
+ container
126
+ .querySelector("#hosted-app-analytics")
127
+ ?.closest("[data-state]")
128
+ ?.getAttribute("data-state"),
129
+ ).toBe("hidden");
130
+ });
131
+
132
+ it("keeps the switch when a refresh fails after a read", async () => {
133
+ state.error = new Error("refresh failed");
134
+ await act(async () => root.render(<HostedAppsSettingsGroup />));
135
+
136
+ expect(toggle().getAttribute("aria-checked")).toBe("true");
137
+ });
138
+
139
+ it("shows a placeholder row, not an empty list, while hosted apps load", async () => {
140
+ state.connected = undefined;
141
+ await act(async () => root.render(<HostedAppsSettingsGroup />));
142
+
143
+ expect(toggle()).not.toBeNull();
144
+ expect(
145
+ container.querySelector("[data-hosted-apps-loading]"),
146
+ ).not.toBeNull();
147
+ expect(container.querySelector('[id^="hosted-app-"]')).toBeNull();
148
+ });
149
+
150
+ it("reports a failed app list instead of showing no hosted apps", async () => {
151
+ state.connectedError = new Error("discovery down");
152
+ await act(async () => root.render(<HostedAppsSettingsGroup />));
153
+
154
+ expect(toggle()).not.toBeNull();
155
+ expect(container.textContent).toContain("list-connected-agents");
156
+ });
157
+
158
+ it("switches at once, dims the rows and rolls back when saving fails", async () => {
159
+ await act(async () => root.render(<HostedAppsSettingsGroup />));
160
+
161
+ await act(async () => toggle().click());
162
+ expect(state.mutate).toHaveBeenCalledWith({ showHostedApps: false });
163
+ expect(toggle().getAttribute("aria-checked")).toBe("false");
164
+ expect(rowsState()).toBe("hidden");
165
+
166
+ await act(async () => state.options.onError?.(new Error("Forbidden")));
167
+ expect(toggle().getAttribute("aria-checked")).toBe("true");
168
+ expect(rowsState()).toBe("shown");
169
+ expect(state.toastError).toHaveBeenCalledWith(
170
+ "dispatch.pages.hostedAppsSaveFailed",
171
+ { description: "Forbidden" },
172
+ );
173
+ });
174
+
175
+ it("keeps the switched value until the setting reads back, then follows reads", async () => {
176
+ await act(async () => root.render(<HostedAppsSettingsGroup />));
177
+
178
+ await act(async () => toggle().click());
179
+ await act(async () => state.options.onSuccess?.());
180
+ expect(state.refetch).not.toHaveBeenCalled();
181
+ expect(toggle().getAttribute("aria-checked")).toBe("false");
182
+
183
+ state.data = { ...(state.data as object), showHostedApps: false };
184
+ await act(async () => root.render(<HostedAppsSettingsGroup />));
185
+ expect(toggle().getAttribute("aria-checked")).toBe("false");
186
+
187
+ state.data = { ...(state.data as object), showHostedApps: true };
188
+ await act(async () => root.render(<HostedAppsSettingsGroup />));
189
+ expect(toggle().getAttribute("aria-checked")).toBe("true");
190
+ });
191
+
192
+ it("offers the switch to repair an invalid stored value", async () => {
193
+ state.data = undefined;
194
+ state.error = Object.assign(new Error("Stored value is invalid"), {
195
+ errorCode: "dispatch_hosted_apps_settings_invalid",
196
+ });
197
+ await act(async () => root.render(<HostedAppsSettingsGroup />));
198
+
199
+ expect(container.querySelector('[role="alert"]')?.textContent).toBe(
200
+ "dispatch.pages.hostedAppsSettingInvalid",
201
+ );
202
+ expect(toggle().getAttribute("aria-checked")).toBe("false");
203
+ await act(async () => toggle().click());
204
+ expect(state.mutate).toHaveBeenCalledWith({ showHostedApps: true });
205
+ expect(toggle().getAttribute("aria-checked")).toBe("true");
206
+ });
207
+
208
+ it("shows any other read failure without a switch", async () => {
209
+ state.data = undefined;
210
+ state.error = Object.assign(new Error("db down"), {
211
+ errorCode: "action_failed",
212
+ });
213
+ await act(async () => root.render(<HostedAppsSettingsGroup />));
214
+
215
+ expect(container.querySelector('[role="switch"]')).toBeNull();
216
+ expect(container.textContent).toContain("get-hosted-apps-settings");
217
+ });
218
+ });