@agent-native/dispatch 0.23.3 → 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 (55) 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/create-app-popover.d.ts.map +1 -1
  7. package/dist/components/create-app-popover.js +11 -0
  8. package/dist/components/create-app-popover.js.map +1 -1
  9. package/dist/components/dispatch-control-plane.d.ts.map +1 -1
  10. package/dist/components/dispatch-control-plane.js +3 -2
  11. package/dist/components/dispatch-control-plane.js.map +1 -1
  12. package/dist/components/layout/Layout.d.ts +1 -0
  13. package/dist/components/layout/Layout.d.ts.map +1 -1
  14. package/dist/components/layout/Layout.js +41 -3
  15. package/dist/components/layout/Layout.js.map +1 -1
  16. package/dist/components/layout/workspace-apps-rail.d.ts.map +1 -1
  17. package/dist/components/layout/workspace-apps-rail.js +2 -2
  18. package/dist/components/layout/workspace-apps-rail.js.map +1 -1
  19. package/dist/components/other-apps-section.d.ts.map +1 -1
  20. package/dist/components/other-apps-section.js +5 -1
  21. package/dist/components/other-apps-section.js.map +1 -1
  22. package/dist/config.d.ts +2 -0
  23. package/dist/config.d.ts.map +1 -1
  24. package/dist/config.js.map +1 -1
  25. package/dist/lib/other-apps.d.ts.map +1 -1
  26. package/dist/lib/other-apps.js +2 -0
  27. package/dist/lib/other-apps.js.map +1 -1
  28. package/dist/lib/workspace-apps.d.ts +3 -0
  29. package/dist/lib/workspace-apps.d.ts.map +1 -1
  30. package/dist/lib/workspace-apps.js +10 -0
  31. package/dist/lib/workspace-apps.js.map +1 -1
  32. package/dist/routes/pages/apps.d.ts.map +1 -1
  33. package/dist/routes/pages/apps.js +2 -1
  34. package/dist/routes/pages/apps.js.map +1 -1
  35. package/dist/server/plugins/auth.d.ts.map +1 -1
  36. package/dist/server/plugins/auth.js +1 -0
  37. package/dist/server/plugins/auth.js.map +1 -1
  38. package/package.json +3 -3
  39. package/src/components/app-list-row.tsx +3 -2
  40. package/src/components/create-app-popover.spec.tsx +26 -2
  41. package/src/components/create-app-popover.tsx +10 -0
  42. package/src/components/dispatch-control-plane.spec.tsx +1 -0
  43. package/src/components/dispatch-control-plane.tsx +8 -2
  44. package/src/components/layout/Layout.spec.tsx +9 -0
  45. package/src/components/layout/Layout.tsx +71 -4
  46. package/src/components/layout/workspace-apps-rail.tsx +2 -1
  47. package/src/components/other-apps-section.tsx +8 -1
  48. package/src/config.ts +2 -0
  49. package/src/lib/other-apps.spec.ts +26 -0
  50. package/src/lib/other-apps.ts +3 -0
  51. package/src/lib/workspace-apps.spec.ts +24 -1
  52. package/src/lib/workspace-apps.ts +15 -0
  53. package/src/routes/pages/apps.tsx +5 -2
  54. package/src/server/plugins/auth.spec.ts +45 -0
  55. package/src/server/plugins/auth.ts +1 -0
@@ -9,6 +9,7 @@ import { TooltipProvider } from "../ui/tooltip";
9
9
  import {
10
10
  buildChatFirstEmbedSessionInput,
11
11
  formatThreadAge,
12
+ isElectronEmbeddedSearch,
12
13
  NavContent,
13
14
  } from "./Layout";
14
15
 
@@ -119,6 +120,14 @@ describe("chat-first embed sessions", () => {
119
120
  });
120
121
  });
121
122
 
123
+ describe("Electron control-plane mode", () => {
124
+ it("recognizes only the explicit Electron query flag", () => {
125
+ expect(isElectronEmbeddedSearch("?electron=1")).toBe(true);
126
+ expect(isElectronEmbeddedSearch("?electron=0")).toBe(false);
127
+ expect(isElectronEmbeddedSearch("?chatFirst=1")).toBe(false);
128
+ });
129
+ });
130
+
122
131
  describe("Dispatch NavContent", () => {
123
132
  let container: HTMLDivElement;
124
133
  let root: Root;
@@ -104,6 +104,8 @@ import { toast } from "sonner";
104
104
 
105
105
  import { cn } from "../../lib/utils";
106
106
  import {
107
+ isDispatchWorkspaceAppId,
108
+ isWorkspaceAppVisibleInDefaultLaunchers,
107
109
  mergeChatFirstWorkspaceApps,
108
110
  workspaceAppIdFromRoute,
109
111
  workspaceAppRoute,
@@ -299,6 +301,10 @@ function localDispatchPath(pathname: string): string {
299
301
  return pathname;
300
302
  }
301
303
 
304
+ export function isElectronEmbeddedSearch(search: string): boolean {
305
+ return new URLSearchParams(search).get("electron") === "1";
306
+ }
307
+
302
308
  function chatFirstPrimaryTabForPath(
303
309
  pathname: string,
304
310
  ): ChatFirstPrimaryTab | undefined {
@@ -1330,6 +1336,11 @@ export function Layout({
1330
1336
  }
1331
1337
  });
1332
1338
  const localPathname = localDispatchPath(location.pathname);
1339
+ const [electronEmbedded] = useState(() =>
1340
+ typeof window !== "undefined"
1341
+ ? isElectronEmbeddedSearch(window.location.search)
1342
+ : false,
1343
+ );
1333
1344
  const isChatRoute =
1334
1345
  localPathname === "/chat" || localPathname.startsWith("/chat/");
1335
1346
  const chatFirstSurfaceScope = threadIdFromPath(localPathname) ?? "new";
@@ -1347,6 +1358,17 @@ export function Layout({
1347
1358
  const chatFirstMode =
1348
1359
  extensions?.chatFirst === true || chatFirstPreference || chatFirstEmbedded;
1349
1360
  const chatFirstHasActiveChat = chatFirstSurfaceScope !== "new";
1361
+ useEffect(() => {
1362
+ if (!electronEmbedded || typeof window === "undefined") return;
1363
+ const url = new URL(window.location.href);
1364
+ if (isElectronEmbeddedSearch(url.search)) return;
1365
+ url.searchParams.set("electron", "1");
1366
+ window.history.replaceState(
1367
+ window.history.state,
1368
+ "",
1369
+ `${url.pathname}${url.search}${url.hash}`,
1370
+ );
1371
+ }, [electronEmbedded, location.pathname, location.search]);
1350
1372
  useEffect(() => {
1351
1373
  if (!chatFirstMode || chatFirstEmbedded || localPathname !== "/overview") {
1352
1374
  return;
@@ -1396,7 +1418,9 @@ export function Layout({
1396
1418
  const chatFirstAppItems = useMemo<ChatFirstAppItem[]>(
1397
1419
  () =>
1398
1420
  chatFirstAppRegistrations
1399
- .filter((app) => app.enabled)
1421
+ .filter(
1422
+ (app) => app.enabled && isWorkspaceAppVisibleInDefaultLaunchers(app),
1423
+ )
1400
1424
  .map((app) => ({
1401
1425
  id: app.id,
1402
1426
  name: app.name ?? app.id,
@@ -1729,6 +1753,19 @@ export function Layout({
1729
1753
  setChatFirstNotice(chatFirstResolutionMessage(resolution.reason));
1730
1754
  return;
1731
1755
  }
1756
+ if (isDispatchWorkspaceAppId(resolution.target.appId)) {
1757
+ closeChatFirstSessionWatch();
1758
+ chatFirstSurfaceTabsStore.closeAll();
1759
+ setChatFirstSurfacePanelOpen(false);
1760
+ persistChatFirstPane(null);
1761
+ if (resolution.target.path) {
1762
+ navigateWithAgentChatViewTransition(
1763
+ navigate,
1764
+ dispatchNavLinkTarget(resolution.target.path),
1765
+ );
1766
+ }
1767
+ return;
1768
+ }
1732
1769
  openChatFirstPane(resolution.target);
1733
1770
  },
1734
1771
  [
@@ -1736,7 +1773,12 @@ export function Layout({
1736
1773
  chatFirstAppsQuery.isError,
1737
1774
  chatFirstAppsQuery.isLoading,
1738
1775
  chatFirstGrantedAppsQuery.isLoading,
1776
+ chatFirstSurfaceTabsStore,
1777
+ closeChatFirstSessionWatch,
1739
1778
  openChatFirstPane,
1779
+ navigate,
1780
+ persistChatFirstPane,
1781
+ setChatFirstSurfacePanelOpen,
1740
1782
  ],
1741
1783
  );
1742
1784
  const chatHomeHandoffActive = useAgentChatHomeHandoff({
@@ -2204,6 +2246,29 @@ export function Layout({
2204
2246
  return <>{children}</>;
2205
2247
  }
2206
2248
 
2249
+ if (electronEmbedded) {
2250
+ return (
2251
+ <DispatchExtensionsContext.Provider value={extensions}>
2252
+ <HeaderActionsProvider>
2253
+ <div
2254
+ data-dispatch-electron-control-plane
2255
+ className="flex h-screen w-full overflow-hidden bg-background"
2256
+ >
2257
+ <div className="relative flex min-w-0 flex-1 flex-col overflow-hidden">
2258
+ <Header showAgentToggle={false} />
2259
+ <InvitationBanner />
2260
+ <main className="flex-1 overflow-y-auto">
2261
+ <div className="mx-auto max-w-7xl space-y-10 px-4 py-6 sm:px-6">
2262
+ {children}
2263
+ </div>
2264
+ </main>
2265
+ </div>
2266
+ </div>
2267
+ </HeaderActionsProvider>
2268
+ </DispatchExtensionsContext.Provider>
2269
+ );
2270
+ }
2271
+
2207
2272
  if (isWorkspaceAppHostRoute) {
2208
2273
  return (
2209
2274
  <DispatchExtensionsContext.Provider value={extensions}>
@@ -2403,9 +2468,11 @@ export function Layout({
2403
2468
  chatFirstSurfaceTabsStore.closeAll();
2404
2469
  setChatFirstSurfacePanelOpen(false);
2405
2470
  }}
2406
- onChatFirstAppOpen={(app) =>
2407
- navigate(dispatchNavLinkTarget(workspaceAppRoute(app.id)))
2408
- }
2471
+ onChatFirstAppOpen={(app) => {
2472
+ if (isDispatchWorkspaceAppId(app.id)) return;
2473
+ setSidebarCollapsed(true);
2474
+ navigate(dispatchNavLinkTarget(workspaceAppRoute(app.id)));
2475
+ }}
2409
2476
  onChatFirstAppsRetry={() => void chatFirstAppsQuery.refetch()}
2410
2477
  collapsible
2411
2478
  onCollapsedChange={setSidebarCollapsed}
@@ -5,6 +5,7 @@ import { Link, useLocation } from "react-router";
5
5
 
6
6
  import { cn } from "../../lib/utils";
7
7
  import {
8
+ isWorkspaceAppVisibleInDefaultLaunchers,
8
9
  workspaceAppIdFromRoute,
9
10
  workspaceAppRoute,
10
11
  workspaceAppHref,
@@ -41,7 +42,7 @@ export function WorkspaceAppsRail({
41
42
  const apps = appsQuery.data
42
43
  .filter(
43
44
  (app) =>
44
- !app.isDispatch &&
45
+ isWorkspaceAppVisibleInDefaultLaunchers(app) &&
45
46
  !app.archived &&
46
47
  app.status !== "pending" &&
47
48
  !!workspaceAppHref(app),
@@ -3,6 +3,7 @@ import { useT } from "@agent-native/core/client/i18n";
3
3
  import { filterOtherApps, type ConnectedAppSummary } from "../lib/other-apps";
4
4
  import type { WorkspaceAppId } from "../lib/other-apps";
5
5
  import { cn } from "../lib/utils";
6
+ import { isDefaultWorkspaceAppHiddenId } from "../lib/workspace-apps";
6
7
  import { ActionQueryError } from "./action-query-error";
7
8
  import {
8
9
  APP_LIST_GRID_CLASS,
@@ -52,7 +53,13 @@ export function mergeOtherAppEntries({
52
53
 
53
54
  for (const template of getTemplateItems(templates)) {
54
55
  const id = templateKey(template);
55
- if (!id || template.installed || workspaceAppIds.has(id)) continue;
56
+ if (
57
+ !id ||
58
+ isDefaultWorkspaceAppHiddenId(id) ||
59
+ template.installed ||
60
+ workspaceAppIds.has(id)
61
+ )
62
+ continue;
56
63
  seen.add(id);
57
64
  entries.push({ kind: "template", template });
58
65
  }
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)
@@ -1,6 +1,12 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
 
3
- import { workspaceAppIdFromRoute, workspaceAppRoute } from "./workspace-apps";
3
+ import {
4
+ isDefaultWorkspaceAppHiddenId,
5
+ isDispatchWorkspaceAppId,
6
+ isWorkspaceAppVisibleInDefaultLaunchers,
7
+ workspaceAppIdFromRoute,
8
+ workspaceAppRoute,
9
+ } from "./workspace-apps";
4
10
 
5
11
  describe("workspace app routes", () => {
6
12
  it("round-trips encoded app ids", () => {
@@ -18,4 +24,21 @@ describe("workspace app routes", () => {
18
24
  it("accepts nested app routes while preserving the app id", () => {
19
25
  expect(workspaceAppIdFromRoute("/apps/mail/inbox")).toBe("mail");
20
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
+ });
21
44
  });
@@ -27,6 +27,21 @@ 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,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");
@@ -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
  };