@agent-native/dispatch 0.23.2 → 0.23.5

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 (86) hide show
  1. package/dist/actions/provider-api-register.d.ts +11 -11
  2. package/dist/components/app-list-row.d.ts +1 -1
  3. package/dist/components/app-list-row.d.ts.map +1 -1
  4. package/dist/components/app-list-row.js +2 -2
  5. package/dist/components/app-list-row.js.map +1 -1
  6. package/dist/components/app-open-actions.d.ts.map +1 -1
  7. package/dist/components/app-open-actions.js +3 -2
  8. package/dist/components/app-open-actions.js.map +1 -1
  9. package/dist/components/create-app-popover.d.ts.map +1 -1
  10. package/dist/components/create-app-popover.js +11 -0
  11. package/dist/components/create-app-popover.js.map +1 -1
  12. package/dist/components/dispatch-control-plane.d.ts.map +1 -1
  13. package/dist/components/dispatch-control-plane.js +3 -2
  14. package/dist/components/dispatch-control-plane.js.map +1 -1
  15. package/dist/components/layout/Layout.d.ts +5 -2
  16. package/dist/components/layout/Layout.d.ts.map +1 -1
  17. package/dist/components/layout/Layout.js +122 -13
  18. package/dist/components/layout/Layout.js.map +1 -1
  19. package/dist/components/layout/workspace-apps-rail.d.ts.map +1 -1
  20. package/dist/components/layout/workspace-apps-rail.js +3 -23
  21. package/dist/components/layout/workspace-apps-rail.js.map +1 -1
  22. package/dist/components/other-apps-section.d.ts.map +1 -1
  23. package/dist/components/other-apps-section.js +5 -1
  24. package/dist/components/other-apps-section.js.map +1 -1
  25. package/dist/components/workspace-app-card.d.ts.map +1 -1
  26. package/dist/components/workspace-app-card.js +3 -4
  27. package/dist/components/workspace-app-card.js.map +1 -1
  28. package/dist/components/workspace-template-card.d.ts.map +1 -1
  29. package/dist/components/workspace-template-card.js +1 -1
  30. package/dist/components/workspace-template-card.js.map +1 -1
  31. package/dist/config.d.ts +2 -0
  32. package/dist/config.d.ts.map +1 -1
  33. package/dist/config.js.map +1 -1
  34. package/dist/lib/other-apps.d.ts.map +1 -1
  35. package/dist/lib/other-apps.js +2 -0
  36. package/dist/lib/other-apps.js.map +1 -1
  37. package/dist/lib/workspace-apps.d.ts +4 -0
  38. package/dist/lib/workspace-apps.d.ts.map +1 -1
  39. package/dist/lib/workspace-apps.js +23 -0
  40. package/dist/lib/workspace-apps.js.map +1 -1
  41. package/dist/routes/index.js +1 -1
  42. package/dist/routes/index.js.map +1 -1
  43. package/dist/routes/pages/agent-integrations.d.ts +5 -0
  44. package/dist/routes/pages/agent-integrations.d.ts.map +1 -0
  45. package/dist/routes/pages/agent-integrations.js +15 -0
  46. package/dist/routes/pages/agent-integrations.js.map +1 -0
  47. package/dist/routes/pages/apps.$appId.d.ts.map +1 -1
  48. package/dist/routes/pages/apps.$appId.js +14 -2
  49. package/dist/routes/pages/apps.$appId.js.map +1 -1
  50. package/dist/routes/pages/apps.d.ts.map +1 -1
  51. package/dist/routes/pages/apps.js +2 -1
  52. package/dist/routes/pages/apps.js.map +1 -1
  53. package/dist/routes/pages/overview.d.ts +2 -2
  54. package/dist/routes/pages/settings.d.ts.map +1 -1
  55. package/dist/routes/pages/settings.js +1 -1
  56. package/dist/routes/pages/settings.js.map +1 -1
  57. package/dist/server/plugins/auth.d.ts.map +1 -1
  58. package/dist/server/plugins/auth.js +1 -0
  59. package/dist/server/plugins/auth.js.map +1 -1
  60. package/package.json +3 -3
  61. package/src/components/app-list-row.tsx +3 -2
  62. package/src/components/app-open-actions.tsx +3 -2
  63. package/src/components/create-app-popover.spec.tsx +26 -2
  64. package/src/components/create-app-popover.tsx +10 -0
  65. package/src/components/dispatch-control-plane.spec.tsx +1 -0
  66. package/src/components/dispatch-control-plane.tsx +8 -2
  67. package/src/components/layout/Layout.spec.tsx +9 -0
  68. package/src/components/layout/Layout.tsx +181 -19
  69. package/src/components/layout/workspace-apps-rail.tsx +4 -26
  70. package/src/components/other-apps-section.tsx +8 -1
  71. package/src/components/workspace-app-card.spec.tsx +6 -6
  72. package/src/components/workspace-app-card.tsx +4 -7
  73. package/src/components/workspace-template-card.spec.tsx +4 -16
  74. package/src/components/workspace-template-card.tsx +0 -1
  75. package/src/config.ts +2 -0
  76. package/src/lib/other-apps.spec.ts +26 -0
  77. package/src/lib/other-apps.ts +3 -0
  78. package/src/lib/workspace-apps.spec.ts +44 -0
  79. package/src/lib/workspace-apps.ts +27 -0
  80. package/src/routes/index.ts +1 -1
  81. package/src/routes/pages/agent-integrations.tsx +24 -0
  82. package/src/routes/pages/apps.$appId.tsx +19 -1
  83. package/src/routes/pages/apps.tsx +5 -2
  84. package/src/routes/pages/settings.tsx +1 -19
  85. package/src/server/plugins/auth.spec.ts +45 -0
  86. package/src/server/plugins/auth.ts +1 -0
@@ -25,7 +25,6 @@ import { toast } from "sonner";
25
25
 
26
26
  import { cn } from "../lib/utils";
27
27
  import {
28
- isPendingBuilderHref,
29
28
  workspaceAppHref,
30
29
  workspaceAppRoute,
31
30
  type WorkspaceAppSummary,
@@ -276,7 +275,6 @@ function WorkspaceAppOpenActions({
276
275
  href: string | null;
277
276
  }) {
278
277
  const navigate = useNavigate();
279
- const openInNewTab = isPendingBuilderHref(app);
280
278
  const appRoute = workspaceAppRoute(app.id);
281
279
 
282
280
  if (!href) {
@@ -290,14 +288,13 @@ function WorkspaceAppOpenActions({
290
288
  return (
291
289
  <AppOpenActions
292
290
  name={app.name}
293
- href={openInNewTab ? href : appRoute}
294
- target={openInNewTab ? "_blank" : undefined}
295
- rel={openInNewTab ? "noreferrer" : undefined}
296
- showInlineOption={openInNewTab}
291
+ href={href}
292
+ target="_blank"
293
+ rel="noreferrer"
294
+ showInlineOption
297
295
  onOpenInline={() => {
298
296
  navigate(appRoute);
299
297
  }}
300
- showNewTabOption={openInNewTab}
301
298
  />
302
299
  );
303
300
  }
@@ -153,7 +153,7 @@ describe("WorkspaceTemplateCard", () => {
153
153
  );
154
154
  });
155
155
 
156
- it("keeps add app out of the catalog split-button menu", async () => {
156
+ it("keeps add app available in catalog cards", async () => {
157
157
  await act(async () => {
158
158
  root.render(<WorkspaceTemplateCard template={template} catalog />);
159
159
  });
@@ -162,23 +162,11 @@ describe("WorkspaceTemplateCard", () => {
162
162
  (button) => button.textContent?.includes("Add app"),
163
163
  );
164
164
  expect(addButton).toBeDefined();
165
-
166
- const optionsButton = container.querySelector<HTMLButtonElement>(
167
- 'button[aria-label="Open options for Weekly report"]',
168
- );
169
- expect(optionsButton).not.toBeNull();
170
- await act(async () => {
171
- optionsButton?.dispatchEvent(
172
- new MouseEvent("pointerdown", { bubbles: true, button: 0 }),
173
- );
174
- });
175
-
176
- const addItem = Array.from(
177
- document.querySelectorAll<HTMLElement>('[role="menuitem"]'),
178
- ).find((item) => item.textContent?.includes("Add app"));
179
- expect(addItem).toBeUndefined();
180
165
  await act(async () => addButton?.click());
181
166
  expect(document.body.textContent).toContain("Choose the URL-safe id");
167
+ expect(
168
+ container.querySelector('a[href="https://reports.example.test"]'),
169
+ ).not.toBeNull();
182
170
  });
183
171
 
184
172
  it("accepts the list action envelope and renders installed state", async () => {
@@ -222,7 +222,6 @@ export function WorkspaceTemplateCard({
222
222
  target="_blank"
223
223
  rel="noreferrer"
224
224
  labels={{ openApp: labels.openApp }}
225
- showNewTabOption
226
225
  />
227
226
  ) : null}
228
227
  <Button
package/src/config.ts CHANGED
@@ -13,6 +13,8 @@ export interface DispatchAuthConfig {
13
13
  googleOnly?: boolean;
14
14
  /** Marketing/branding copy passed straight through to `createAuthPlugin`. */
15
15
  marketing?: Record<string, unknown>;
16
+ /** Exact framework routes that perform their own authentication checks. */
17
+ publicPaths?: string[];
16
18
  }
17
19
 
18
20
  export interface DispatchIntegrationsConfig {
@@ -104,4 +104,30 @@ describe("filterOtherApps", () => {
104
104
  },
105
105
  ]);
106
106
  });
107
+
108
+ it("hides the generic chat starter from connected app launchers", () => {
109
+ expect(
110
+ filterOtherApps(
111
+ [
112
+ {
113
+ id: "chat",
114
+ name: "Chat",
115
+ url: "https://chat.agent-native.com",
116
+ },
117
+ {
118
+ id: "mail",
119
+ name: "Mail",
120
+ url: "https://mail.agent-native.com",
121
+ },
122
+ ],
123
+ [],
124
+ ),
125
+ ).toEqual([
126
+ {
127
+ id: "mail",
128
+ name: "Mail",
129
+ url: "https://mail.agent-native.com",
130
+ },
131
+ ]);
132
+ });
107
133
  });
@@ -1,3 +1,5 @@
1
+ import { isDefaultWorkspaceAppHiddenId } from "./workspace-apps";
2
+
1
3
  export interface ConnectedAppSummary {
2
4
  id: string;
3
5
  name: string;
@@ -38,6 +40,7 @@ export function filterOtherApps(
38
40
  const id = app.id.trim().toLowerCase();
39
41
  if (
40
42
  !id ||
43
+ isDefaultWorkspaceAppHiddenId(id) ||
41
44
  HIDDEN_OTHER_APP_IDS.has(id) ||
42
45
  workspaceAppIds.has(id) ||
43
46
  seen.has(id)
@@ -0,0 +1,44 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import {
4
+ isDefaultWorkspaceAppHiddenId,
5
+ isDispatchWorkspaceAppId,
6
+ isWorkspaceAppVisibleInDefaultLaunchers,
7
+ workspaceAppIdFromRoute,
8
+ workspaceAppRoute,
9
+ } from "./workspace-apps";
10
+
11
+ describe("workspace app routes", () => {
12
+ it("round-trips encoded app ids", () => {
13
+ const route = workspaceAppRoute("sales ops");
14
+ expect(route).toBe("/apps/sales%20ops");
15
+ expect(workspaceAppIdFromRoute(route)).toBe("sales ops");
16
+ });
17
+
18
+ it("does not mark app paths or the apps index as an active app route", () => {
19
+ expect(workspaceAppIdFromRoute("/sales-ops")).toBeNull();
20
+ expect(workspaceAppIdFromRoute("/apps")).toBeNull();
21
+ expect(workspaceAppIdFromRoute("/overview")).toBeNull();
22
+ });
23
+
24
+ it("accepts nested app routes while preserving the app id", () => {
25
+ expect(workspaceAppIdFromRoute("/apps/mail/inbox")).toBe("mail");
26
+ });
27
+
28
+ it("identifies Dispatch regardless of casing or surrounding whitespace", () => {
29
+ expect(isDispatchWorkspaceAppId(" Dispatch ")).toBe(true);
30
+ expect(isDispatchWorkspaceAppId("dispatch-tools")).toBe(false);
31
+ });
32
+
33
+ it("hides the generic chat starter and Dispatch from default launchers", () => {
34
+ expect(isDefaultWorkspaceAppHiddenId(" Chat ")).toBe(true);
35
+ expect(isWorkspaceAppVisibleInDefaultLaunchers({ id: "chat" })).toBe(false);
36
+ expect(
37
+ isWorkspaceAppVisibleInDefaultLaunchers({
38
+ id: "dispatch",
39
+ isDispatch: true,
40
+ }),
41
+ ).toBe(false);
42
+ expect(isWorkspaceAppVisibleInDefaultLaunchers({ id: "mail" })).toBe(true);
43
+ });
44
+ });
@@ -27,10 +27,37 @@ export interface WorkspaceAppSummary {
27
27
  archived?: boolean;
28
28
  }
29
29
 
30
+ export function isDispatchWorkspaceAppId(appId: string): boolean {
31
+ return appId.trim().toLowerCase() === "dispatch";
32
+ }
33
+
34
+ export function isDefaultWorkspaceAppHiddenId(appId: string): boolean {
35
+ const normalized = appId.trim().toLowerCase();
36
+ return normalized === "chat" || isDispatchWorkspaceAppId(normalized);
37
+ }
38
+
39
+ export function isWorkspaceAppVisibleInDefaultLaunchers(
40
+ app: Pick<WorkspaceAppSummary, "id" | "isDispatch">,
41
+ ): boolean {
42
+ return !app.isDispatch && !isDefaultWorkspaceAppHiddenId(app.id);
43
+ }
44
+
30
45
  export function workspaceAppRoute(appId: string): string {
31
46
  return `/apps/${encodeURIComponent(appId)}`;
32
47
  }
33
48
 
49
+ export function workspaceAppIdFromRoute(pathname: string): string | null {
50
+ const match = pathname.match(/^\/apps\/([^/]+)(?:\/|$)/);
51
+ if (!match) return null;
52
+ try {
53
+ const appId = decodeURIComponent(match[1]).trim();
54
+ return appId || null;
55
+ } catch {
56
+ // coercion-ok: malformed app routes are inactive, not app ids.
57
+ return null;
58
+ }
59
+ }
60
+
34
61
  export function workspaceAppHref(app: WorkspaceAppSummary): string | null {
35
62
  if (app.status === "pending") {
36
63
  return app.builderUrl
@@ -44,7 +44,7 @@ export const dispatchRoutes: RouteConfig = [
44
44
  route("automations", "./pages/automations.js"),
45
45
  route("approvals", "./pages/approvals.js"),
46
46
  route("destinations", "./pages/destinations.js"),
47
- route("integrations", "./pages/integrations.js"),
47
+ route("integrations", "./pages/agent-integrations.js"),
48
48
  route("transactional-email", "./pages/transactional-email.js"),
49
49
  route(
50
50
  "transactional-email/:appId/:id",
@@ -0,0 +1,24 @@
1
+ import { useAgentSettingsTabs } from "@agent-native/core/client/settings";
2
+
3
+ import { DispatchShell } from "../../components/dispatch-shell";
4
+
5
+ export function meta() {
6
+ return [{ title: "Integrations — Dispatch" }];
7
+ }
8
+
9
+ export default function AgentIntegrationsRoute() {
10
+ const settingsTabs = useAgentSettingsTabs();
11
+ const integrationsTab = settingsTabs.find((tab) => tab.id === "integrations");
12
+ if (!integrationsTab) {
13
+ throw new Error("The shared Integrations settings view is unavailable.");
14
+ }
15
+
16
+ return (
17
+ <DispatchShell
18
+ title="Integrations"
19
+ description="Connect the tools and services available to your agent."
20
+ >
21
+ <div className="mx-auto w-full max-w-5xl">{integrationsTab.content}</div>
22
+ </DispatchShell>
23
+ );
24
+ }
@@ -5,7 +5,7 @@ import {
5
5
  import { useT } from "@agent-native/core/client/i18n";
6
6
  import { withBuilderUtmTrackingParams } from "@agent-native/core/shared/builder-link-tracking";
7
7
  import { IconArrowLeft, IconClockHour4 } from "@tabler/icons-react";
8
- import { useEffect, useMemo, useState } from "react";
8
+ import { useEffect, useMemo, useRef, useState } from "react";
9
9
  import { Link, useParams } from "react-router";
10
10
 
11
11
  import { ActionQueryError } from "../../components/action-query-error";
@@ -52,6 +52,7 @@ export default function WorkspaceAppRoute() {
52
52
  const [embedUrl, setEmbedUrl] = useState<string | null>(null);
53
53
  const [embedError, setEmbedError] = useState<Error | null>(null);
54
54
  const [embedAttempt, setEmbedAttempt] = useState(0);
55
+ const embedFrameRef = useRef<HTMLIFrameElement>(null);
55
56
  const createEmbedSession = useActionMutation<
56
57
  EmbedSessionResult,
57
58
  EmbedSessionInput
@@ -96,6 +97,22 @@ export default function WorkspaceAppRoute() {
96
97
  embedInput,
97
98
  ]);
98
99
 
100
+ useEffect(() => {
101
+ const handleEmbedSessionExpired = (event: MessageEvent) => {
102
+ if (
103
+ event.data?.type !== "agentNative.embedSessionExpired" ||
104
+ event.source !== embedFrameRef.current?.contentWindow
105
+ ) {
106
+ return;
107
+ }
108
+ setEmbedAttempt((attempt) => attempt + 1);
109
+ };
110
+
111
+ window.addEventListener("message", handleEmbedSessionExpired);
112
+ return () =>
113
+ window.removeEventListener("message", handleEmbedSessionExpired);
114
+ }, []);
115
+
99
116
  if (appsQuery.isError) {
100
117
  return (
101
118
  <div className="flex h-full min-h-0 items-center justify-center p-6">
@@ -208,6 +225,7 @@ export default function WorkspaceAppRoute() {
208
225
  data-dispatch-workspace-app-frame
209
226
  src={embedUrl}
210
227
  title={app.name}
228
+ ref={embedFrameRef}
211
229
  referrerPolicy="no-referrer"
212
230
  className="h-full w-full border-0 bg-background"
213
231
  />
@@ -33,7 +33,10 @@ import type {
33
33
  } from "../../components/workspace-template-card";
34
34
  import type { ConnectedAppSummary } from "../../lib/other-apps";
35
35
  import { cn } from "../../lib/utils";
36
- import type { WorkspaceAppSummary } from "../../lib/workspace-apps";
36
+ import {
37
+ isWorkspaceAppVisibleInDefaultLaunchers,
38
+ type WorkspaceAppSummary,
39
+ } from "../../lib/workspace-apps";
37
40
 
38
41
  export function meta() {
39
42
  return [{ title: "Apps — Dispatch" }];
@@ -72,7 +75,7 @@ function AppsRoute() {
72
75
  const ws = workspace as WorkspaceInfo | undefined;
73
76
  const workspaceLabel = ws?.displayName ?? ws?.name ?? null;
74
77
  const allApps = (apps as WorkspaceAppSummary[]).filter(
75
- (app) => !app.isDispatch,
78
+ isWorkspaceAppVisibleInDefaultLaunchers,
76
79
  );
77
80
  const visibleApps = allApps.filter((app) => !app.archived);
78
81
  const activeApps = visibleApps.filter((app) => app.status !== "pending");
@@ -68,7 +68,7 @@ export default function SettingsRoute() {
68
68
  description={t("settings.description")}
69
69
  >
70
70
  <SettingsTabsPage
71
- extraTabs={agentSettingsTabs}
71
+ extraTabs={agentSettingsTabs.filter((tab) => tab.id !== "integrations")}
72
72
  general={
73
73
  <div className="mx-auto w-full max-w-3xl space-y-6">
74
74
  <Card>
@@ -141,24 +141,6 @@ export default function SettingsRoute() {
141
141
  </CardContent>
142
142
  </Card>
143
143
 
144
- <Card>
145
- <CardHeader>
146
- <CardTitle className="text-base">
147
- {t("settings.automationsTitle")}
148
- </CardTitle>
149
- <CardDescription>
150
- {t("settings.automationsDescription")}
151
- </CardDescription>
152
- </CardHeader>
153
- <CardContent>
154
- <Button variant="outline" asChild>
155
- <Link to="/admin/automations">
156
- {t("settings.openAutomations")}
157
- </Link>
158
- </Button>
159
- </CardContent>
160
- </Card>
161
-
162
144
  <Card>
163
145
  <CardHeader>
164
146
  <CardTitle className="text-base">
@@ -0,0 +1,45 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ const mocks = vi.hoisted(() => ({
4
+ authPlugin: vi.fn(),
5
+ createAuthPlugin: vi.fn(),
6
+ getDispatchConfig: vi.fn(),
7
+ }));
8
+
9
+ vi.mock("@agent-native/core/server", () => ({
10
+ createAuthPlugin: mocks.createAuthPlugin,
11
+ }));
12
+
13
+ vi.mock("../index.js", () => ({
14
+ getDispatchConfig: mocks.getDispatchConfig,
15
+ }));
16
+
17
+ describe("dispatchAuthPlugin", () => {
18
+ beforeEach(() => {
19
+ vi.resetModules();
20
+ mocks.authPlugin.mockReset();
21
+ mocks.createAuthPlugin.mockReset();
22
+ mocks.createAuthPlugin.mockReturnValue(mocks.authPlugin);
23
+ mocks.getDispatchConfig.mockReset();
24
+ });
25
+
26
+ it("installs template public routes on the primary auth guard", async () => {
27
+ const { default: dispatchAuthPlugin } = await import("./auth.js");
28
+ const nitroApp = {};
29
+ const publicPaths = [
30
+ "/_agent-native/identity/authorize",
31
+ "/_agent-native/org/apps",
32
+ ];
33
+
34
+ mocks.getDispatchConfig.mockReturnValue({
35
+ auth: { googleOnly: true, publicPaths },
36
+ });
37
+ await dispatchAuthPlugin(nitroApp);
38
+
39
+ expect(mocks.createAuthPlugin).toHaveBeenCalledOnce();
40
+ expect(mocks.createAuthPlugin).toHaveBeenCalledWith(
41
+ expect.objectContaining({ googleOnly: true, publicPaths }),
42
+ );
43
+ expect(mocks.authPlugin).toHaveBeenCalledWith(nitroApp);
44
+ });
45
+ });
@@ -28,6 +28,7 @@ const dispatchAuthPlugin = async (nitroApp: any) => {
28
28
  const plugin = createAuthPlugin({
29
29
  googleOnly,
30
30
  marketing: marketing as any,
31
+ publicPaths: authConfig.publicPaths,
31
32
  });
32
33
  return plugin(nitroApp);
33
34
  };