@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
@@ -29,6 +29,7 @@ import getAppCreationSettings from "./get-app-creation-settings.js";
29
29
  import getDispatchSettings from "./get-dispatch-settings.js";
30
30
  import getDreamSettings from "./get-dream-settings.js";
31
31
  import getDream from "./get-dream.js";
32
+ import getHostedAppsSettings from "./get-hosted-apps-settings.js";
32
33
  import getVaultAccessSettings from "./get-vault-access-settings.js";
33
34
  import getWorkspaceInfo from "./get-workspace-info.js";
34
35
  import getWorkspaceResourceEffectiveContext from "./get-workspace-resource-effective-context.js";
@@ -96,6 +97,7 @@ import sendPlatformMessage from "./send-platform-message.js";
96
97
  import setAppCreationSettings from "./set-app-creation-settings.js";
97
98
  import setDispatchApprovalPolicy from "./set-dispatch-approval-policy.js";
98
99
  import setDreamSettings from "./set-dream-settings.js";
100
+ import setHostedAppsSettings from "./set-hosted-apps-settings.js";
99
101
  import setMcpAppAccess from "./set-mcp-app-access.js";
100
102
  import setVaultAccessSettings from "./set-vault-access-settings.js";
101
103
  import startWorkspaceAppCreation from "./start-workspace-app-creation.js";
@@ -137,6 +139,7 @@ export const dispatchActions: Record<string, ActionEntry> = {
137
139
  "get-dream": getDream,
138
140
  "get-dream-settings": getDreamSettings,
139
141
  "get-dispatch-settings": getDispatchSettings,
142
+ "get-hosted-apps-settings": getHostedAppsSettings,
140
143
  "get-vault-access-settings": getVaultAccessSettings,
141
144
  "get-workspace-resource-effective-context":
142
145
  getWorkspaceResourceEffectiveContext,
@@ -206,6 +209,7 @@ export const dispatchActions: Record<string, ActionEntry> = {
206
209
  "set-app-creation-settings": setAppCreationSettings,
207
210
  "set-dispatch-approval-policy": setDispatchApprovalPolicy,
208
211
  "set-dream-settings": setDreamSettings,
212
+ "set-hosted-apps-settings": setHostedAppsSettings,
209
213
  "set-mcp-app-access": setMcpAppAccess,
210
214
  "set-vault-access-settings": setVaultAccessSettings,
211
215
  "start-workspace-app-creation": startWorkspaceAppCreation,
@@ -13,6 +13,9 @@ const mocks = vi.hoisted(() => ({
13
13
  loadWorkspaceAppsManifest: vi.fn(
14
14
  async (): Promise<Array<{ id: string }> | null> => null,
15
15
  ),
16
+ getOrgSetting: vi.fn(
17
+ async (): Promise<Record<string, unknown> | null> => null,
18
+ ),
16
19
  }));
17
20
 
18
21
  vi.mock("@agent-native/core", () => ({
@@ -33,9 +36,17 @@ vi.mock("@agent-native/core/resources/store", () => ({
33
36
  }));
34
37
 
35
38
  vi.mock("@agent-native/core/server", () => ({
39
+ getRequestOrgId: () => "org-1",
36
40
  getRequestUserEmail: () => mocks.getRequestUserEmail(),
37
41
  }));
38
42
 
43
+ vi.mock("@agent-native/core/settings", () => ({
44
+ getOrgSetting: (...args: unknown[]) => mocks.getOrgSetting(...args),
45
+ getUserSetting: vi.fn(),
46
+ putOrgSetting: vi.fn(),
47
+ putUserSetting: vi.fn(),
48
+ }));
49
+
39
50
  vi.mock("@agent-native/core/server/agent-discovery", () => ({
40
51
  discoverAgents: (...args: unknown[]) => mocks.discoverAgents(...args),
41
52
  getBuiltinAgents: (...args: unknown[]) => mocks.getBuiltinAgents(...args),
@@ -83,6 +94,62 @@ describe("list-connected-agents", () => {
83
94
  });
84
95
  });
85
96
 
97
+ it("marks hosted apps hidden from launchers when the organization hides them", async () => {
98
+ const clips = {
99
+ id: "clips",
100
+ name: "Clips",
101
+ description: "",
102
+ url: "https://clips.agent-native.com",
103
+ color: "#000000",
104
+ };
105
+ const partner = { ...clips, id: "partner", url: "https://p.example.test" };
106
+ mocks.getBuiltinAgents.mockReturnValue([clips]);
107
+ mocks.discoverAgents.mockResolvedValue([clips, partner]);
108
+ mocks.resourceListAccessible.mockImplementation(async () => []);
109
+ mocks.getOrgSetting.mockResolvedValueOnce({ showHostedApps: false });
110
+
111
+ const { default: action } = await import("./list-connected-agents.js");
112
+ const agents = await action.run({});
113
+
114
+ expect(
115
+ agents.map((agent: { id: string; hiddenFromLaunchers?: boolean }) => [
116
+ agent.id,
117
+ agent.hiddenFromLaunchers === true,
118
+ ]),
119
+ ).toEqual([
120
+ ["clips", true],
121
+ ["partner", false],
122
+ ]);
123
+ });
124
+
125
+ it("hides hosted apps instead of failing the list when the stored setting is invalid", async () => {
126
+ const clips = {
127
+ id: "clips",
128
+ name: "Clips",
129
+ description: "",
130
+ url: "https://clips.agent-native.com",
131
+ color: "#000000",
132
+ };
133
+ const partner = { ...clips, id: "partner", url: "https://p.example.test" };
134
+ mocks.getBuiltinAgents.mockReturnValue([clips]);
135
+ mocks.discoverAgents.mockResolvedValue([clips, partner]);
136
+ mocks.resourceListAccessible.mockImplementation(async () => []);
137
+ mocks.getOrgSetting.mockResolvedValueOnce({ showHostedApps: "no" });
138
+
139
+ const { default: action } = await import("./list-connected-agents.js");
140
+ const agents = await action.run({});
141
+
142
+ expect(
143
+ agents.map((agent: { id: string; hiddenFromLaunchers?: boolean }) => [
144
+ agent.id,
145
+ agent.hiddenFromLaunchers === true,
146
+ ]),
147
+ ).toEqual([
148
+ ["clips", true],
149
+ ["partner", false],
150
+ ]);
151
+ });
152
+
86
153
  it("does not surface seeded manifests for built-ins that are not enabled as custom agents", async () => {
87
154
  mocks.getBuiltinAgents.mockReturnValue([]);
88
155
  mocks.discoverAgents.mockResolvedValue([]);
@@ -11,38 +11,34 @@ import {
11
11
  import { getRequestUserEmail } from "@agent-native/core/server";
12
12
  import {
13
13
  discoverAgents,
14
- getBuiltinAgents,
15
14
  isBuiltinAgentCatalogId,
16
- loadWorkspaceAppsManifest,
17
15
  normalizeAgentId,
18
16
  shouldIncludeRemoteAgentManifest,
19
17
  } from "@agent-native/core/server/agent-discovery";
20
18
  import { z } from "zod";
21
19
 
20
+ import {
21
+ readHostedAppsSettings,
22
+ listHostedApps,
23
+ } from "../server/lib/hosted-apps-settings.js";
24
+
22
25
  export default defineAction({
23
26
  description:
24
- "List agents available to dispatch for A2A delegation, including built-in apps and connected remote agents.",
27
+ "List agents available to dispatch for A2A delegation, including built-in apps and connected remote agents. When get-hosted-apps-settings reports showHostedApps false, built-in (hosted) apps are still listed but marked hiddenFromLaunchers: true.",
25
28
  schema: z.object({}),
26
29
  http: { method: "GET" },
27
30
  run: async () => {
28
31
  const { hiddenAgentIds = [] } = await import("../server/index.js").then(
29
32
  (m) => m.getDispatchConfig(),
30
33
  );
31
- const [discovered, workspaceApps] = await Promise.all([
34
+ const [discovered, hosted, hostedAppsSettings] = await Promise.all([
32
35
  discoverAgents("dispatch"),
33
- loadWorkspaceAppsManifest(),
36
+ listHostedApps(),
37
+ readHostedAppsSettings(),
34
38
  ]);
35
- // A mounted workspace app with a built-in's id is a workspace app; the
36
- // built-in config never applies to it.
37
- const workspaceIds = new Set(
38
- (workspaceApps ?? []).map((app) => normalizeAgentId(app.id)),
39
- );
40
- const builtins = getBuiltinAgents("dispatch");
41
- const builtinIds = new Set(
42
- builtins.map((agent) => agent.id).filter((id) => !workspaceIds.has(id)),
43
- );
39
+ const builtinIds = new Set(hosted.map((agent) => agent.id));
44
40
  const builtinHomeUrls = new Map(
45
- builtins.map((agent) => [agent.id, agent.url]),
41
+ hosted.map((agent) => [agent.id, agent.url]),
46
42
  );
47
43
  const ownerEmail = getRequestUserEmail();
48
44
  if (!ownerEmail) {
@@ -97,6 +93,9 @@ export default defineAction({
97
93
  return {
98
94
  ...agent,
99
95
  ...(homeUrl ? { homeUrl } : {}),
96
+ ...(isBuiltin && !hostedAppsSettings.showHostedApps
97
+ ? { hiddenFromLaunchers: true }
98
+ : {}),
100
99
  source: isBuiltin ? "builtin" : custom ? "custom" : "workspace",
101
100
  resourceId: custom?.resourceId,
102
101
  path: custom?.path,
@@ -2,6 +2,11 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
2
2
 
3
3
  const mocks = vi.hoisted(() => ({
4
4
  listGrantedApps: vi.fn(),
5
+ hideHostedAppsWhenOff: vi.fn(async <T>(apps: T[]) => apps),
6
+ }));
7
+
8
+ vi.mock("../server/lib/hosted-apps-settings.js", () => ({
9
+ hideHostedAppsWhenOff: mocks.hideHostedAppsWhenOff,
5
10
  }));
6
11
 
7
12
  vi.mock("../server/lib/mcp-gateway.js", () => ({
@@ -12,6 +17,7 @@ import listApps from "./list_apps.js";
12
17
 
13
18
  beforeEach(() => {
14
19
  vi.clearAllMocks();
20
+ mocks.hideHostedAppsWhenOff.mockImplementation(async (apps) => apps);
15
21
  mocks.listGrantedApps.mockResolvedValue([
16
22
  {
17
23
  id: "slides",
@@ -29,6 +35,16 @@ beforeEach(() => {
29
35
  });
30
36
 
31
37
  describe("list_apps", () => {
38
+ it("leaves out apps the hosted apps setting hides", async () => {
39
+ mocks.hideHostedAppsWhenOff.mockImplementation(async (apps) =>
40
+ apps.filter((app: { id: string }) => app.id !== "analytics"),
41
+ );
42
+ const result = await listApps.run({});
43
+ expect(result.apps.map((app: { id: string }) => app.id)).toEqual([
44
+ "slides",
45
+ ]);
46
+ });
47
+
32
48
  it("returns a compact valid JSON message for MCP hosts", async () => {
33
49
  const result = await listApps.run({});
34
50
 
@@ -1,17 +1,20 @@
1
1
  import { defineAction } from "@agent-native/core/action";
2
2
  import { z } from "zod";
3
3
 
4
+ import { hideHostedAppsWhenOff } from "../server/lib/hosted-apps-settings.js";
4
5
  import { listGrantedDispatchMcpApps } from "../server/lib/mcp-gateway.js";
5
6
 
6
7
  export default defineAction({
7
8
  description:
8
- 'List the apps this Dispatch MCP gateway can route to, including "dispatch" itself for Dispatch-owned pages such as extensions. The result is filtered by Dispatch\'s MCP app access policy.',
9
+ 'List the apps this Dispatch MCP gateway can route to, including "dispatch" itself for Dispatch-owned pages such as extensions. The result is filtered by Dispatch\'s MCP app access policy, and leaves out Agent-Native.com hosted apps while the organization hides them.',
9
10
  schema: z.object({}),
10
11
  http: { method: "GET" },
11
12
  readOnly: true,
12
13
  parallelSafe: true,
13
14
  run: async () => {
14
- const apps = await listGrantedDispatchMcpApps();
15
+ const apps = await hideHostedAppsWhenOff(
16
+ await listGrantedDispatchMcpApps(),
17
+ );
15
18
  const appSummaries = apps.map((app) => ({
16
19
  id: app.id,
17
20
  name: app.name,
@@ -0,0 +1,42 @@
1
+ import { defineAction } from "@agent-native/core/action";
2
+ import { z } from "zod";
3
+
4
+ import { authorizeOrgOwnerOrAdmin } from "../server/lib/app-roles.js";
5
+ import { recordAudit } from "../server/lib/dispatch-store.js";
6
+ import { hostedAppsSettingsViewSchema } from "../server/lib/hosted-apps-settings-schema.js";
7
+ import {
8
+ HOSTED_APPS_SETTINGS_KEY,
9
+ setHostedAppsSettings,
10
+ } from "../server/lib/hosted-apps-settings.js";
11
+
12
+ export default defineAction({
13
+ mcpAnnotations: {
14
+ readOnlyHint: false,
15
+ destructiveHint: false,
16
+ openWorldHint: false,
17
+ },
18
+ description:
19
+ "Show or hide Agent-Native.com hosted apps (built-in apps) in this organization's Dispatch app lists and agent context. Organization owners and admins only. Pins are kept while hidden.",
20
+ authorize: authorizeOrgOwnerOrAdmin,
21
+ schema: z.object({
22
+ showHostedApps: z
23
+ .boolean()
24
+ .describe(
25
+ "true shows Agent-Native.com hosted apps; false hides them for everyone.",
26
+ ),
27
+ }),
28
+ outputSchema: hostedAppsSettingsViewSchema,
29
+ run: async ({ showHostedApps }) => {
30
+ const saved = await setHostedAppsSettings({ showHostedApps });
31
+ await recordAudit({
32
+ action: "hosted-apps.updated",
33
+ targetType: "dispatch-settings",
34
+ targetId: HOSTED_APPS_SETTINGS_KEY,
35
+ summary: showHostedApps
36
+ ? "Showed Agent-Native.com hosted apps"
37
+ : "Hid Agent-Native.com hosted apps",
38
+ metadata: saved,
39
+ });
40
+ return saved;
41
+ },
42
+ });
@@ -0,0 +1,85 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ import type { ConnectedAppSummary } from "../lib/other-apps";
4
+ import {
5
+ tierWorkspaceApps,
6
+ useWorkspaceAppLayout,
7
+ } from "../lib/workspace-app-layout";
8
+ import type { WorkspaceAppSummary } from "../lib/workspace-apps";
9
+ import { AppList } from "./app-list-row";
10
+ import { AppTierSections } from "./app-tier-sections";
11
+ import { ConnectedAppCard } from "./connected-app-card";
12
+ import { WorkspaceAppCard } from "./workspace-app-card";
13
+
14
+ type AppCardEntry =
15
+ | { id: string; name: string; source?: undefined; app: WorkspaceAppSummary }
16
+ | {
17
+ id: string;
18
+ name: string;
19
+ source: "builtin";
20
+ hosted: ConnectedAppSummary;
21
+ };
22
+
23
+ /** Home and All apps: workspace app cards and hosted app cards, tiered. */
24
+ export function AppCardTiers({
25
+ workspaceApps,
26
+ hostedApps,
27
+ appsTitle,
28
+ actions,
29
+ hasPinnedApps,
30
+ appsFallback,
31
+ }: {
32
+ workspaceApps: readonly WorkspaceAppSummary[];
33
+ hostedApps: readonly ConnectedAppSummary[];
34
+ appsTitle: string;
35
+ actions?: ReactNode;
36
+ hasPinnedApps?: boolean;
37
+ appsFallback?: ReactNode;
38
+ }) {
39
+ const { layout, togglePinned } = useWorkspaceAppLayout();
40
+ const tiers = tierWorkspaceApps<AppCardEntry>(
41
+ [
42
+ ...workspaceApps.map((app) => ({ id: app.id, name: app.name, app })),
43
+ ...hostedApps.map((hosted) => ({
44
+ id: hosted.id,
45
+ name: hosted.name,
46
+ source: "builtin" as const,
47
+ hosted,
48
+ })),
49
+ ],
50
+ layout,
51
+ );
52
+ const isPinned = (id: string) =>
53
+ layout.pinnedIds.includes(id.trim().toLowerCase());
54
+
55
+ return (
56
+ <AppTierSections
57
+ tiers={tiers}
58
+ appsTitle={appsTitle}
59
+ actions={actions}
60
+ hasPinnedApps={hasPinnedApps}
61
+ appsFallback={appsFallback}
62
+ renderItems={(entries) => (
63
+ <AppList layout="grid">
64
+ {entries.map((entry) =>
65
+ "app" in entry ? (
66
+ <WorkspaceAppCard
67
+ key={entry.id}
68
+ app={entry.app}
69
+ isPinned={isPinned(entry.id)}
70
+ onTogglePinned={() => togglePinned(entry.id)}
71
+ />
72
+ ) : (
73
+ <ConnectedAppCard
74
+ key={entry.id}
75
+ app={entry.hosted}
76
+ isPinned={isPinned(entry.id)}
77
+ onTogglePinned={() => togglePinned(entry.id)}
78
+ />
79
+ ),
80
+ )}
81
+ </AppList>
82
+ )}
83
+ />
84
+ );
85
+ }
@@ -0,0 +1,54 @@
1
+ // @vitest-environment happy-dom
2
+ // oracle-change(packages/dispatch/src/components/other-apps-section.spec.ts): OtherAppsSection had no importers and was deleted with its spec.
3
+ import React, { act } from "react";
4
+ import { createRoot, type Root } from "react-dom/client";
5
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
6
+
7
+ import { AppList, AppListRow } from "./app-list-row";
8
+
9
+ describe("AppList", () => {
10
+ let container: HTMLDivElement;
11
+ let root: Root;
12
+
13
+ beforeEach(() => {
14
+ vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
15
+ container = document.createElement("div");
16
+ document.body.appendChild(container);
17
+ root = createRoot(container);
18
+ });
19
+
20
+ afterEach(() => {
21
+ act(() => root.unmount());
22
+ container.remove();
23
+ vi.unstubAllGlobals();
24
+ });
25
+
26
+ it("renders grid rows as muted shadcn Items and list rows as divided rows", async () => {
27
+ await act(async () =>
28
+ root.render(
29
+ <>
30
+ <AppList layout="grid">
31
+ <AppListRow>grid app</AppListRow>
32
+ </AppList>
33
+ <AppList>
34
+ <AppListRow>list app</AppListRow>
35
+ </AppList>
36
+ </>,
37
+ ),
38
+ );
39
+
40
+ const rows = [...container.querySelectorAll("div")].filter((row) =>
41
+ /^(grid|list) app$/.test(row.textContent ?? ""),
42
+ );
43
+ const grid = rows.find(
44
+ (row) => row.textContent === "grid app" && row.dataset.slot,
45
+ );
46
+ const list = rows.filter((row) => row.textContent === "list app").at(-1);
47
+ expect(grid?.dataset.slot).toBe("item");
48
+ expect(grid?.dataset.variant).toBe("muted");
49
+ // Tabbing into a row marks it; Item only styles its own focus.
50
+ expect(grid?.className).toContain("focus-within:border-ring/50");
51
+ expect(list?.dataset.slot).toBeUndefined();
52
+ expect(list?.className).toContain("border-b");
53
+ });
54
+ });
@@ -1,23 +1,61 @@
1
- import type { ReactNode } from "react";
1
+ import { Item } from "@agent-native/toolkit/ui/item";
2
+ import {
3
+ createContext,
4
+ useContext,
5
+ type ComponentProps,
6
+ type ReactNode,
7
+ } from "react";
2
8
 
3
9
  import { cn } from "../lib/utils";
4
10
 
5
- export const APP_LIST_GRID_CLASS =
6
- "w-full xl:grid xl:grid-cols-2 xl:gap-3 xl:overflow-visible xl:rounded-none";
7
- export const APP_LIST_GRID_ROW_CLASS =
8
- "xl:rounded-2xl xl:border xl:border-transparent xl:transition-[border-color] xl:focus-within:border-ring/50";
11
+ type AppListLayout = "list" | "grid";
9
12
 
13
+ const AppListLayoutContext = createContext<AppListLayout>("list");
14
+
15
+ /**
16
+ * `grid` is the two-column app list on Home, New chat, All apps and the
17
+ * catalogs; its rows are shadcn `Item`s. `list` is a bordered, divided list.
18
+ */
10
19
  export function AppList({
11
20
  children,
12
21
  className,
22
+ layout = "list",
13
23
  }: {
14
24
  children: ReactNode;
15
25
  className?: string;
26
+ layout?: AppListLayout;
16
27
  }) {
17
28
  return (
18
- <div className={cn("overflow-hidden rounded-2xl", className)}>
19
- {children}
20
- </div>
29
+ <AppListLayoutContext.Provider value={layout}>
30
+ <div
31
+ className={cn(
32
+ layout === "grid"
33
+ ? "grid w-full grid-cols-1 gap-2 sm:grid-cols-2"
34
+ : "overflow-hidden rounded-2xl",
35
+ className,
36
+ )}
37
+ >
38
+ {children}
39
+ </div>
40
+ </AppListLayoutContext.Provider>
41
+ );
42
+ }
43
+
44
+ /** One app in a grid app list. */
45
+ export function AppGridItem({
46
+ className,
47
+ ...props
48
+ }: ComponentProps<typeof Item>) {
49
+ return (
50
+ <Item
51
+ variant="muted"
52
+ size="sm"
53
+ className={cn(
54
+ "group min-w-0 flex-nowrap focus-within:border-ring/50",
55
+ className,
56
+ )}
57
+ {...props}
58
+ />
21
59
  );
22
60
  }
23
61
 
@@ -28,6 +66,10 @@ export function AppListRow({
28
66
  children: ReactNode;
29
67
  className?: string;
30
68
  }) {
69
+ const layout = useContext(AppListLayoutContext);
70
+ if (layout === "grid") {
71
+ return <AppGridItem className={className}>{children}</AppGridItem>;
72
+ }
31
73
  return (
32
74
  <div
33
75
  className={cn(
@@ -1,5 +1,21 @@
1
+ import type { useT } from "@agent-native/core/client/i18n";
2
+ import type { AppOpenActionLabels } from "@agent-native/toolkit/app/chat/chat-first/app-open-actions";
3
+
1
4
  export {
2
5
  AppOpenActions,
3
6
  type AppOpenActionLabels,
4
7
  type AppOpenActionMenuItem,
5
8
  } from "@agent-native/toolkit/app/chat/chat-first/app-open-actions";
9
+
10
+ /** Dispatch's localized labels for an app row's Open actions. */
11
+ export function appOpenActionLabels(
12
+ t: ReturnType<typeof useT>,
13
+ name: string,
14
+ ): Partial<AppOpenActionLabels> {
15
+ return {
16
+ openApp: t("dispatch.pages.openApp", { defaultValue: "Open" }),
17
+ openAppAccessible: t("dispatch.pages.chatFirstOpenApp", { name }),
18
+ openInNewTab: t("dispatch.pages.chatFirstOpenInNewTab"),
19
+ moreOptions: t("extensions.optionsFor", { name }),
20
+ };
21
+ }
@@ -0,0 +1,68 @@
1
+ // @vitest-environment happy-dom
2
+ import React, { act, useState } from "react";
3
+ import { createRoot, type Root } from "react-dom/client";
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
+
6
+ vi.mock("@agent-native/core/client/i18n", () => ({
7
+ useT: () => (key: string) => key,
8
+ }));
9
+
10
+ import { AppPinButton } from "./app-pin-button";
11
+ import { TooltipProvider } from "./ui/tooltip";
12
+
13
+ function MovingRow() {
14
+ const [pinned, setPinned] = useState(false);
15
+ const button = (
16
+ <AppPinButton
17
+ appId="mail"
18
+ name="Mail"
19
+ pinned={pinned}
20
+ onToggle={() => setPinned((value) => !value)}
21
+ />
22
+ );
23
+ return (
24
+ <TooltipProvider>
25
+ <section data-section="pinned">{pinned ? button : null}</section>
26
+ <section data-section="apps">{pinned ? null : button}</section>
27
+ </TooltipProvider>
28
+ );
29
+ }
30
+
31
+ describe("AppPinButton", () => {
32
+ let container: HTMLDivElement;
33
+ let root: Root;
34
+
35
+ beforeEach(() => {
36
+ vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
37
+ container = document.createElement("div");
38
+ document.body.appendChild(container);
39
+ root = createRoot(container);
40
+ });
41
+
42
+ afterEach(() => {
43
+ act(() => root.unmount());
44
+ container.remove();
45
+ vi.unstubAllGlobals();
46
+ });
47
+
48
+ it("keeps focus on the app's pin button after pinning moves its row", async () => {
49
+ await act(async () => root.render(<MovingRow />));
50
+ const before = container.querySelector<HTMLButtonElement>(
51
+ '[data-pin-app-id="mail"]',
52
+ )!;
53
+ before.focus();
54
+
55
+ await act(async () => before.click());
56
+ await act(
57
+ () =>
58
+ new Promise<void>((resolve) => requestAnimationFrame(() => resolve())),
59
+ );
60
+
61
+ const after = container.querySelector<HTMLButtonElement>(
62
+ '[data-section="pinned"] [data-pin-app-id="mail"]',
63
+ );
64
+ expect(after).not.toBeNull();
65
+ expect(after).not.toBe(before);
66
+ expect(document.activeElement).toBe(after);
67
+ });
68
+ });
@@ -0,0 +1,66 @@
1
+ import { useT } from "@agent-native/core/client/i18n";
2
+ import { IconPin, IconPinFilled } from "@tabler/icons-react";
3
+
4
+ import { cn } from "../lib/utils";
5
+ import { Button } from "./ui/button";
6
+ import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
7
+
8
+ /** Shown on row hover or focus, always when pinned or on touch screens. */
9
+ export function AppPinButton({
10
+ appId,
11
+ name,
12
+ pinned,
13
+ onToggle,
14
+ className,
15
+ }: {
16
+ appId: string;
17
+ name: string;
18
+ pinned: boolean;
19
+ onToggle: () => void;
20
+ className?: string;
21
+ }) {
22
+ const t = useT();
23
+ const label = t(
24
+ pinned ? "dispatch.pages.unpinApp" : "dispatch.pages.pinApp",
25
+ {
26
+ name,
27
+ },
28
+ );
29
+ const Icon = pinned ? IconPinFilled : IconPin;
30
+ const toggle = () => {
31
+ onToggle();
32
+ // Pinning moves the row to another section, which remounts it; keep
33
+ // keyboard focus on this app's pin button rather than dropping to <body>.
34
+ requestAnimationFrame(() => {
35
+ if (document.activeElement && document.activeElement !== document.body) {
36
+ return;
37
+ }
38
+ document
39
+ .querySelector<HTMLElement>(`[data-pin-app-id="${CSS.escape(appId)}"]`)
40
+ ?.focus();
41
+ });
42
+ };
43
+ return (
44
+ <Tooltip>
45
+ <TooltipTrigger asChild>
46
+ <Button
47
+ type="button"
48
+ variant="ghost"
49
+ size="sm"
50
+ aria-label={label}
51
+ aria-pressed={pinned}
52
+ data-pinned={pinned}
53
+ data-pin-app-id={appId}
54
+ onClick={toggle}
55
+ className={cn(
56
+ "size-7 shrink-0 p-0 text-muted-foreground opacity-0 transition-opacity hover:text-foreground focus-visible:opacity-100 group-hover:opacity-100 group-focus-within:opacity-100 data-[pinned=true]:text-foreground data-[pinned=true]:opacity-100 [@media(pointer:coarse)]:opacity-100",
57
+ className,
58
+ )}
59
+ >
60
+ <Icon size={15} aria-hidden="true" />
61
+ </Button>
62
+ </TooltipTrigger>
63
+ <TooltipContent>{label}</TooltipContent>
64
+ </Tooltip>
65
+ );
66
+ }