@agent-native/dispatch 0.40.13 → 0.40.14-nightly-20261006033736

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 (38) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/actions/provider-api-register.d.ts +10 -10
  3. package/dist/components/connected-app-card.d.ts.map +1 -1
  4. package/dist/components/connected-app-card.js +23 -4
  5. package/dist/components/connected-app-card.js.map +1 -1
  6. package/dist/components/layout/Layout.d.ts.map +1 -1
  7. package/dist/components/layout/Layout.js +116 -60
  8. package/dist/components/layout/Layout.js.map +1 -1
  9. package/dist/components/layout/workspace-apps-rail.d.ts.map +1 -1
  10. package/dist/components/layout/workspace-apps-rail.js +35 -9
  11. package/dist/components/layout/workspace-apps-rail.js.map +1 -1
  12. package/dist/lib/other-apps.d.ts +1 -0
  13. package/dist/lib/other-apps.d.ts.map +1 -1
  14. package/dist/lib/other-apps.js +29 -1
  15. package/dist/lib/other-apps.js.map +1 -1
  16. package/dist/lib/workspace-apps.d.ts +2 -1
  17. package/dist/lib/workspace-apps.d.ts.map +1 -1
  18. package/dist/lib/workspace-apps.js +15 -5
  19. package/dist/lib/workspace-apps.js.map +1 -1
  20. package/dist/routes/pages/apps.d.ts.map +1 -1
  21. package/dist/routes/pages/apps.js +17 -4
  22. package/dist/routes/pages/apps.js.map +1 -1
  23. package/dist/server/plugins/auth.js +2 -2
  24. package/dist/server/plugins/auth.js.map +1 -1
  25. package/package.json +4 -4
  26. package/src/components/connected-app-card.spec.tsx +100 -0
  27. package/src/components/connected-app-card.tsx +38 -9
  28. package/src/components/layout/Layout.spec.tsx +397 -6
  29. package/src/components/layout/Layout.tsx +173 -85
  30. package/src/components/layout/workspace-apps-rail.spec.tsx +69 -7
  31. package/src/components/layout/workspace-apps-rail.tsx +49 -7
  32. package/src/lib/other-apps.spec.ts +84 -2
  33. package/src/lib/other-apps.ts +39 -1
  34. package/src/lib/workspace-apps.spec.ts +53 -0
  35. package/src/lib/workspace-apps.ts +15 -7
  36. package/src/routes/pages/apps.spec.tsx +132 -0
  37. package/src/routes/pages/apps.tsx +46 -10
  38. package/src/server/plugins/auth.ts +2 -2
@@ -1,10 +1,12 @@
1
+ import { emitChatFirstOpenApp } from "@agent-native/core/client/agent-chat";
1
2
  import { isSettingsPathname } from "@agent-native/toolkit/app/settings";
2
3
  // @vitest-environment happy-dom
3
4
  import React, { act } from "react";
4
5
  import { createRoot, type Root } from "react-dom/client";
5
- import { MemoryRouter, useLocation } from "react-router";
6
+ import { MemoryRouter, useLocation, useNavigate } from "react-router";
6
7
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
7
8
 
9
+ import { CHAT_FIRST_PANE_STATE_KEY } from "../../shared/chat-first-pane";
8
10
  import { AdminShell } from "../admin-navigation";
9
11
  import { Tooltip, TooltipProvider, TooltipTrigger } from "../ui/tooltip";
10
12
  import {
@@ -26,11 +28,26 @@ const clientState = vi.hoisted(() => ({
26
28
  switchThread: vi.fn(),
27
29
  threads: [] as Array<Record<string, unknown>>,
28
30
  workspaceApps: [] as Array<Record<string, unknown>>,
31
+ workspaceAppsError: null as Error | null,
32
+ workspaceAppsLoading: false,
33
+ connectedApps: [] as Array<Record<string, unknown>>,
34
+ connectedAppsError: null as Error | null,
35
+ connectedAppsLoading: false,
36
+ appState: {} as Record<string, unknown>,
37
+ appStatePromises: {} as Record<string, Promise<unknown>>,
29
38
  basePath: "",
30
39
  createEmbedSessionMutateAsync: vi
31
40
  .fn()
32
41
  .mockResolvedValue({ startUrl: "about:blank" }),
33
42
  }));
43
+ const workspaceAppNavigation = vi.hoisted(() => ({
44
+ navigateToWorkspaceApp: vi.fn((_href: string) => true),
45
+ }));
46
+
47
+ vi.mock("../../lib/workspace-apps", async (importOriginal) => ({
48
+ ...(await importOriginal<typeof import("../../lib/workspace-apps")>()),
49
+ navigateToWorkspaceApp: workspaceAppNavigation.navigateToWorkspaceApp,
50
+ }));
34
51
 
35
52
  vi.mock("@agent-native/core/client/agent-chat", async (importOriginal) => ({
36
53
  ...(await importOriginal<
@@ -99,16 +116,51 @@ vi.mock("@agent-native/core/client/api-path", () => ({
99
116
 
100
117
  vi.mock("@agent-native/core/client/hooks", async (importOriginal) => ({
101
118
  ...(await importOriginal<typeof import("@agent-native/core/client/hooks")>()),
102
- useActionQuery: (action: string) => ({
103
- data:
104
- action === "list-workspace-apps" ? clientState.workspaceApps : undefined,
105
- isLoading: false,
106
- }),
119
+ useActionQuery: (action: string) => {
120
+ const workspaceAppsQuery = action === "list-workspace-apps";
121
+ const connectedAppsQuery = action === "list-connected-agents";
122
+ const error = workspaceAppsQuery
123
+ ? clientState.workspaceAppsError
124
+ : connectedAppsQuery
125
+ ? clientState.connectedAppsError
126
+ : null;
127
+ return {
128
+ data: workspaceAppsQuery
129
+ ? clientState.workspaceApps
130
+ : connectedAppsQuery
131
+ ? clientState.connectedApps
132
+ : undefined,
133
+ error,
134
+ isError: Boolean(error),
135
+ isLoading: workspaceAppsQuery
136
+ ? clientState.workspaceAppsLoading
137
+ : connectedAppsQuery
138
+ ? clientState.connectedAppsLoading
139
+ : false,
140
+ };
141
+ },
107
142
  useActionMutation: () => ({
108
143
  mutateAsync: clientState.createEmbedSessionMutateAsync,
109
144
  }),
110
145
  }));
111
146
 
147
+ vi.mock(
148
+ "@agent-native/core/client/application-state",
149
+ async (importOriginal) => ({
150
+ ...(await importOriginal<
151
+ typeof import("@agent-native/core/client/application-state")
152
+ >()),
153
+ readClientAppState: async (key: string) =>
154
+ (await clientState.appStatePromises[key]) ??
155
+ clientState.appState[key] ??
156
+ null,
157
+ writeClientAppState: async (key: string, value: unknown) => {
158
+ clientState.appState[key] = value;
159
+ return value;
160
+ },
161
+ }),
162
+ );
163
+
112
164
  vi.mock("@agent-native/core/client/feature-flags", () => ({
113
165
  useFeatureFlag: () => false,
114
166
  useFeatureFlagState: () => ({ enabled: false, status: "ready" }),
@@ -171,6 +223,15 @@ function LocationProbe({ onChange }: { onChange: (path: string) => void }) {
171
223
  return null;
172
224
  }
173
225
 
226
+ function ChatRouteSwitcher({ target }: { target: string }) {
227
+ const navigate = useNavigate();
228
+ return (
229
+ <button type="button" onClick={() => navigate(target)}>
230
+ Switch chat
231
+ </button>
232
+ );
233
+ }
234
+
174
235
  describe("Dispatch navigation paths", () => {
175
236
  afterEach(() => {
176
237
  clientState.basePath = "";
@@ -304,6 +365,14 @@ describe("Dispatch NavContent", () => {
304
365
  },
305
366
  ];
306
367
  clientState.workspaceApps = [];
368
+ clientState.workspaceAppsError = null;
369
+ clientState.workspaceAppsLoading = false;
370
+ clientState.connectedApps = [];
371
+ clientState.connectedAppsError = null;
372
+ clientState.connectedAppsLoading = false;
373
+ clientState.appState = {};
374
+ clientState.appStatePromises = {};
375
+ workspaceAppNavigation.navigateToWorkspaceApp.mockClear();
307
376
  container = document.createElement("div");
308
377
  document.body.appendChild(container);
309
378
  root = createRoot(container);
@@ -359,6 +428,328 @@ describe("Dispatch NavContent", () => {
359
428
  }
360
429
  });
361
430
 
431
+ it("opens mounted apps when optional connected-app discovery fails", async () => {
432
+ clientState.workspaceApps = [
433
+ {
434
+ id: "mail",
435
+ name: "Mail",
436
+ path: "/mail",
437
+ url: "/mail",
438
+ status: "ready",
439
+ },
440
+ ];
441
+ clientState.connectedAppsError = new Error("Connected apps unavailable");
442
+
443
+ await act(async () => {
444
+ root.render(
445
+ <MemoryRouter initialEntries={["/chat/mounted-optional-error"]}>
446
+ <Layout extensions={{ chatFirst: true }}>
447
+ <div />
448
+ </Layout>
449
+ </MemoryRouter>,
450
+ );
451
+ });
452
+
453
+ await act(async () => {
454
+ emitChatFirstOpenApp({ app: "mail" });
455
+ });
456
+
457
+ expect(
458
+ container.querySelector("[data-chat-first-app-pane]"),
459
+ ).not.toBeNull();
460
+ });
461
+
462
+ it("opens a loaded mounted app while optional built-in discovery is loading", async () => {
463
+ clientState.workspaceApps = [
464
+ {
465
+ id: "mail",
466
+ name: "Mail",
467
+ path: "/mail",
468
+ url: "/mail",
469
+ status: "ready",
470
+ },
471
+ ];
472
+ clientState.connectedAppsLoading = true;
473
+
474
+ await act(async () => {
475
+ root.render(
476
+ <MemoryRouter initialEntries={["/chat/mounted-while-loading"]}>
477
+ <Layout extensions={{ chatFirst: true }}>
478
+ <div />
479
+ </Layout>
480
+ </MemoryRouter>,
481
+ );
482
+ });
483
+
484
+ await act(async () => {
485
+ emitChatFirstOpenApp({ app: "mail" });
486
+ });
487
+
488
+ expect(
489
+ container.querySelector("[data-chat-first-app-pane]"),
490
+ ).not.toBeNull();
491
+ });
492
+
493
+ it("opens a connected built-in when mounted workspace-app discovery fails", async () => {
494
+ clientState.workspaceAppsError = new Error("Workspace apps unavailable");
495
+ clientState.connectedApps = [
496
+ {
497
+ id: "clips",
498
+ name: "Clips",
499
+ url: "https://clips.agent-native.com",
500
+ source: "builtin",
501
+ },
502
+ ];
503
+
504
+ await act(async () => {
505
+ root.render(
506
+ <MemoryRouter initialEntries={["/chat/builtin-mounted-error"]}>
507
+ <Layout extensions={{ chatFirst: true }}>
508
+ <div />
509
+ </Layout>
510
+ </MemoryRouter>,
511
+ );
512
+ });
513
+
514
+ await act(async () => {
515
+ emitChatFirstOpenApp({ app: "clips" });
516
+ });
517
+
518
+ expect(
519
+ container.querySelector("[data-chat-first-app-pane]"),
520
+ ).not.toBeNull();
521
+ });
522
+
523
+ it("retries an app open after connected built-in discovery recovers", async () => {
524
+ clientState.workspaceApps = [
525
+ {
526
+ id: "mail",
527
+ name: "Mail",
528
+ path: "/mail",
529
+ url: "/mail",
530
+ status: "ready",
531
+ },
532
+ ];
533
+ clientState.connectedAppsError = new Error("Connected apps unavailable");
534
+
535
+ await act(async () => {
536
+ root.render(
537
+ <MemoryRouter initialEntries={["/chat/discovery-retry"]}>
538
+ <Layout extensions={{ chatFirst: true }}>
539
+ <div />
540
+ </Layout>
541
+ </MemoryRouter>,
542
+ );
543
+ });
544
+ await act(async () => {
545
+ emitChatFirstOpenApp({ app: "clips" });
546
+ });
547
+
548
+ expect(container.querySelector("[data-chat-first-app-pane]")).toBeNull();
549
+
550
+ clientState.connectedAppsError = null;
551
+ clientState.connectedApps = [
552
+ {
553
+ id: "clips",
554
+ name: "Clips",
555
+ url: "https://clips.agent-native.com",
556
+ source: "builtin",
557
+ },
558
+ ];
559
+ await act(async () => {
560
+ root.render(
561
+ <MemoryRouter initialEntries={["/chat/review-fixture"]}>
562
+ <Layout extensions={{ chatFirst: true }}>
563
+ <div />
564
+ </Layout>
565
+ </MemoryRouter>,
566
+ );
567
+ });
568
+
569
+ expect(
570
+ container.querySelector("[data-chat-first-app-pane]"),
571
+ ).not.toBeNull();
572
+ });
573
+
574
+ it("keeps hidden Dispatch registered for agent app targets", async () => {
575
+ clientState.workspaceApps = [
576
+ {
577
+ id: "dispatch",
578
+ name: "Dispatch",
579
+ path: "/dispatch",
580
+ url: "https://dispatch.agent-native.com",
581
+ isDispatch: true,
582
+ status: "ready",
583
+ },
584
+ ];
585
+
586
+ await act(async () => {
587
+ root.render(
588
+ <MemoryRouter initialEntries={["/chat/review-fixture"]}>
589
+ <Layout extensions={{ chatFirst: true }}>
590
+ <div />
591
+ </Layout>
592
+ </MemoryRouter>,
593
+ );
594
+ });
595
+ await act(async () => {
596
+ emitChatFirstOpenApp({ app: "dispatch" });
597
+ });
598
+
599
+ expect(container.querySelector(".dispatch-chat-first-notice")).toBeNull();
600
+ });
601
+
602
+ it("does not restore a deferred pane after switching chats", async () => {
603
+ clientState.connectedAppsLoading = true;
604
+ clientState.appState[CHAT_FIRST_PANE_STATE_KEY] = {
605
+ appId: "clips",
606
+ path: "/inbox",
607
+ };
608
+ const onPathChange = vi.fn();
609
+ const renderLayout = () => (
610
+ <MemoryRouter initialEntries={["/chat/pane-restore"]}>
611
+ <Layout extensions={{ chatFirst: true }}>
612
+ <ChatRouteSwitcher target="/chat/next-chat" />
613
+ </Layout>
614
+ <LocationProbe onChange={onPathChange} />
615
+ </MemoryRouter>
616
+ );
617
+
618
+ await act(async () => {
619
+ root.render(renderLayout());
620
+ });
621
+ expect(container.querySelector("[data-chat-first-app-pane]")).toBeNull();
622
+
623
+ await act(async () => {
624
+ [...container.querySelectorAll("button")]
625
+ .find((button) => button.textContent === "Switch chat")
626
+ ?.click();
627
+ });
628
+ expect(onPathChange).toHaveBeenLastCalledWith("/chat/next-chat");
629
+
630
+ clientState.connectedAppsLoading = false;
631
+ clientState.connectedApps = [
632
+ {
633
+ id: "clips",
634
+ name: "Clips",
635
+ url: "https://clips.agent-native.com",
636
+ source: "builtin",
637
+ },
638
+ ];
639
+ await act(async () => {
640
+ root.render(renderLayout());
641
+ });
642
+
643
+ expect(container.querySelector("[data-chat-first-app-pane]")).toBeNull();
644
+ });
645
+
646
+ it("ignores pane hydration that finishes after switching chats", async () => {
647
+ let resolvePaneState!: (value: unknown) => void;
648
+ clientState.appStatePromises[CHAT_FIRST_PANE_STATE_KEY] = new Promise(
649
+ (resolve) => {
650
+ resolvePaneState = resolve;
651
+ },
652
+ );
653
+ clientState.connectedApps = [
654
+ {
655
+ id: "clips",
656
+ name: "Clips",
657
+ url: "https://clips.agent-native.com",
658
+ source: "builtin",
659
+ },
660
+ ];
661
+ const onPathChange = vi.fn();
662
+ const renderLayout = () => (
663
+ <MemoryRouter initialEntries={["/chat/pane-read-hydration"]}>
664
+ <Layout extensions={{ chatFirst: true }}>
665
+ <ChatRouteSwitcher target="/chat/new-scope" />
666
+ </Layout>
667
+ <LocationProbe onChange={onPathChange} />
668
+ </MemoryRouter>
669
+ );
670
+
671
+ await act(async () => {
672
+ root.render(renderLayout());
673
+ });
674
+ await act(async () => {
675
+ [...container.querySelectorAll("button")]
676
+ .find((button) => button.textContent === "Switch chat")
677
+ ?.click();
678
+ });
679
+ expect(onPathChange).toHaveBeenLastCalledWith("/chat/new-scope");
680
+
681
+ await act(async () => {
682
+ resolvePaneState({ appId: "clips", path: "/inbox" });
683
+ });
684
+
685
+ expect(container.querySelector("[data-chat-first-app-pane]")).toBeNull();
686
+ });
687
+
688
+ it("opens connected built-ins from the chat-first app rail", async () => {
689
+ clientState.connectedApps = [
690
+ {
691
+ id: "clips",
692
+ name: "Clips",
693
+ url: "https://clips.agent-native.com",
694
+ source: "builtin",
695
+ },
696
+ ];
697
+
698
+ await act(async () => {
699
+ root.render(
700
+ <MemoryRouter initialEntries={["/chat/rail-built-in"]}>
701
+ <Layout extensions={{ chatFirst: true }}>
702
+ <div />
703
+ </Layout>
704
+ </MemoryRouter>,
705
+ );
706
+ });
707
+ await act(async () => {
708
+ const clipsApp = container.querySelector<HTMLElement>(
709
+ '[data-chat-first-app][data-app-id="clips"]',
710
+ );
711
+ const openButton =
712
+ clipsApp?.querySelector<HTMLButtonElement>("button") ??
713
+ (clipsApp as HTMLButtonElement | null);
714
+ openButton?.click();
715
+ });
716
+
717
+ expect(workspaceAppNavigation.navigateToWorkspaceApp).toHaveBeenCalledWith(
718
+ "https://clips.agent-native.com/home",
719
+ );
720
+ });
721
+
722
+ it("restores a mounted pane without waiting for optional built-in discovery", async () => {
723
+ clientState.workspaceApps = [
724
+ {
725
+ id: "mail",
726
+ name: "Mail",
727
+ path: "/mail",
728
+ url: "/mail",
729
+ status: "ready",
730
+ },
731
+ ];
732
+ clientState.connectedAppsLoading = true;
733
+ clientState.appState[CHAT_FIRST_PANE_STATE_KEY] = {
734
+ appId: "mail",
735
+ path: "/inbox",
736
+ };
737
+
738
+ await act(async () => {
739
+ root.render(
740
+ <MemoryRouter initialEntries={["/chat/review-fixture"]}>
741
+ <Layout extensions={{ chatFirst: true }}>
742
+ <div />
743
+ </Layout>
744
+ </MemoryRouter>,
745
+ );
746
+ });
747
+
748
+ expect(
749
+ container.querySelector("[data-chat-first-app-pane]"),
750
+ ).not.toBeNull();
751
+ });
752
+
362
753
  it("puts Overview before Chat in the primary navigation", async () => {
363
754
  await act(async () => {
364
755
  root.render(