@agent-native/dispatch 0.31.14 → 0.31.16
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.
- package/dist/actions/provider-api-register.d.ts +7 -7
- package/dist/components/dispatch-control-plane.d.ts.map +1 -1
- package/dist/components/dispatch-control-plane.js +2 -2
- package/dist/components/dispatch-control-plane.js.map +1 -1
- package/dist/components/layout/Layout.d.ts +17 -4
- package/dist/components/layout/Layout.d.ts.map +1 -1
- package/dist/components/layout/Layout.js +45 -20
- package/dist/components/layout/Layout.js.map +1 -1
- package/dist/components/workspace-app-card.d.ts.map +1 -1
- package/dist/components/workspace-app-card.js +18 -1
- package/dist/components/workspace-app-card.js.map +1 -1
- package/dist/lib/catch-all-target.d.ts.map +1 -1
- package/dist/lib/catch-all-target.js +4 -2
- package/dist/lib/catch-all-target.js.map +1 -1
- package/dist/routes/pages/automations.d.ts.map +1 -1
- package/dist/routes/pages/automations.js +19 -16
- package/dist/routes/pages/automations.js.map +1 -1
- package/dist/server/lib/app-creation-store.d.ts.map +1 -1
- package/dist/server/lib/app-creation-store.js +31 -10
- package/dist/server/lib/app-creation-store.js.map +1 -1
- package/package.json +3 -3
- package/src/components/dispatch-control-plane.spec.tsx +3 -1
- package/src/components/dispatch-control-plane.tsx +2 -1
- package/src/components/layout/Layout.spec.tsx +143 -25
- package/src/components/layout/Layout.tsx +79 -47
- package/src/components/workspace-app-card.tsx +27 -1
- package/src/lib/catch-all-target.spec.ts +8 -0
- package/src/lib/catch-all-target.ts +4 -2
- package/src/routes/pages/automations.spec.tsx +240 -0
- package/src/routes/pages/automations.tsx +22 -16
- package/src/server/lib/app-creation-store.spec.ts +42 -15
- package/src/server/lib/app-creation-store.ts +36 -10
- package/src/styles/dispatch-css.spec.ts +3 -0
- package/src/styles/dispatch.css +5 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/dispatch",
|
|
3
|
-
"version": "0.31.
|
|
3
|
+
"version": "0.31.16",
|
|
4
4
|
"description": "Dispatch — workspace control plane for agent-native apps. Vault, integrations, destinations, scheduled jobs, and cross-app delegation, shipped as a single drop-in package.",
|
|
5
5
|
"homepage": "https://github.com/BuilderIO/agent-native#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -87,7 +87,7 @@
|
|
|
87
87
|
"tailwind-merge": "^3.5.0",
|
|
88
88
|
"vaul": "^1.1.2",
|
|
89
89
|
"zod": "^4.3.6",
|
|
90
|
-
"@agent-native/toolkit": "0.16.
|
|
90
|
+
"@agent-native/toolkit": "0.16.11"
|
|
91
91
|
},
|
|
92
92
|
"devDependencies": {
|
|
93
93
|
"@react-router/dev": "^8.1.0",
|
|
@@ -101,7 +101,7 @@
|
|
|
101
101
|
"typescript-7": "npm:typescript@^7.0.2",
|
|
102
102
|
"vite": "8.1.0",
|
|
103
103
|
"vitest": "^4.1.5",
|
|
104
|
-
"@agent-native/core": "0.
|
|
104
|
+
"@agent-native/core": "0.172.9"
|
|
105
105
|
},
|
|
106
106
|
"peerDependencies": {
|
|
107
107
|
"@agent-native/core": ">=0.8.0",
|
|
@@ -29,6 +29,8 @@ const clientState = vi.hoisted(() => ({
|
|
|
29
29
|
}));
|
|
30
30
|
|
|
31
31
|
vi.mock("@agent-native/core/client/agent-chat", () => ({
|
|
32
|
+
chatModelSelectionStorageKey: (namespace: string) =>
|
|
33
|
+
`agent-native:chat-models:selection:${namespace}`,
|
|
32
34
|
navigateWithAgentChatViewTransition: (
|
|
33
35
|
navigate: unknown,
|
|
34
36
|
path: string,
|
|
@@ -161,7 +163,7 @@ describe("DispatchControlPlane", () => {
|
|
|
161
163
|
container.querySelector('[data-placeholder="Ask Dispatch anything..."]'),
|
|
162
164
|
).not.toBeNull();
|
|
163
165
|
expect(clientState.useChatModels).toHaveBeenCalledWith({
|
|
164
|
-
storageKey: "dispatch",
|
|
166
|
+
storageKey: "agent-native:chat-models:selection:dispatch",
|
|
165
167
|
});
|
|
166
168
|
expect(clientState.promptComposerProps).toMatchObject({
|
|
167
169
|
availableModels: [],
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
chatModelSelectionStorageKey,
|
|
2
3
|
navigateWithAgentChatViewTransition,
|
|
3
4
|
useChatModels,
|
|
4
5
|
} from "@agent-native/core/client/agent-chat";
|
|
@@ -71,7 +72,7 @@ function CommandPanel() {
|
|
|
71
72
|
selectedEffort,
|
|
72
73
|
selectedEngine,
|
|
73
74
|
selectedModel,
|
|
74
|
-
} = useChatModels({ storageKey: "dispatch" });
|
|
75
|
+
} = useChatModels({ storageKey: chatModelSelectionStorageKey("dispatch") });
|
|
75
76
|
const navigate = useNavigate();
|
|
76
77
|
const promptSuggestions = [
|
|
77
78
|
t("dispatch.pages.suggestionOnboardingApp", {
|
|
@@ -8,9 +8,11 @@ import { AdminShell } from "../admin-navigation";
|
|
|
8
8
|
import { TooltipProvider } from "../ui/tooltip";
|
|
9
9
|
import {
|
|
10
10
|
buildChatFirstEmbedSessionInput,
|
|
11
|
+
CHAT_FIRST_SURFACE_PANEL_TOGGLE_CLASS_NAME,
|
|
11
12
|
formatThreadAge,
|
|
12
13
|
isElectronEmbeddedSearch,
|
|
13
14
|
NavContent,
|
|
15
|
+
renderChatFirstAppSurfaceTab,
|
|
14
16
|
shouldAutoCollapseDispatchSidebar,
|
|
15
17
|
} from "./Layout";
|
|
16
18
|
|
|
@@ -19,10 +21,17 @@ const clientState = vi.hoisted(() => ({
|
|
|
19
21
|
switchThread: vi.fn(),
|
|
20
22
|
threads: [] as Array<Record<string, unknown>>,
|
|
21
23
|
workspaceApps: [] as Array<Record<string, unknown>>,
|
|
24
|
+
// Stable identity: WorkspaceAppFrame's embed effect depends on this
|
|
25
|
+
// function, so a fresh mock per render would re-run the effect forever.
|
|
26
|
+
createEmbedSessionMutateAsync: vi
|
|
27
|
+
.fn()
|
|
28
|
+
.mockResolvedValue({ startUrl: "about:blank" }),
|
|
22
29
|
}));
|
|
23
30
|
|
|
24
31
|
vi.mock("@agent-native/core/client/agent-chat", () => ({
|
|
25
|
-
AgentSidebar: ({ children }: { children: React.ReactNode }) =>
|
|
32
|
+
AgentSidebar: ({ children }: { children: React.ReactNode }) => (
|
|
33
|
+
<div data-agent-sidebar>{children}</div>
|
|
34
|
+
),
|
|
26
35
|
focusAgentChat: vi.fn(),
|
|
27
36
|
navigateWithAgentChatViewTransition: (
|
|
28
37
|
navigate: (path: string) => void,
|
|
@@ -53,6 +62,17 @@ vi.mock("@agent-native/core/client/hooks", () => ({
|
|
|
53
62
|
action === "list-workspace-apps" ? clientState.workspaceApps : undefined,
|
|
54
63
|
isLoading: false,
|
|
55
64
|
}),
|
|
65
|
+
useActionMutation: () => ({
|
|
66
|
+
mutateAsync: clientState.createEmbedSessionMutateAsync,
|
|
67
|
+
}),
|
|
68
|
+
}));
|
|
69
|
+
|
|
70
|
+
vi.mock("@agent-native/core/client/feature-flags", () => ({
|
|
71
|
+
useFeatureFlag: () => false,
|
|
72
|
+
}));
|
|
73
|
+
|
|
74
|
+
vi.mock("next-themes", () => ({
|
|
75
|
+
useTheme: () => ({ resolvedTheme: "light" }),
|
|
56
76
|
}));
|
|
57
77
|
|
|
58
78
|
vi.mock("@agent-native/core/client/i18n", () => ({
|
|
@@ -226,7 +246,7 @@ describe("Dispatch NavContent", () => {
|
|
|
226
246
|
{ mode: "chat-first", chatFirstMode: true, collapsed: false },
|
|
227
247
|
{ mode: "chat-first", chatFirstMode: true, collapsed: true },
|
|
228
248
|
])(
|
|
229
|
-
"
|
|
249
|
+
"keeps footer controls flush with the bottom in the $mode desktop footer when collapsed is $collapsed",
|
|
230
250
|
async ({ mode, chatFirstMode, collapsed }) => {
|
|
231
251
|
await act(async () => {
|
|
232
252
|
root.render(
|
|
@@ -234,11 +254,7 @@ describe("Dispatch NavContent", () => {
|
|
|
234
254
|
initialEntries={[chatFirstMode ? "/chat" : "/overview"]}
|
|
235
255
|
>
|
|
236
256
|
<TooltipProvider>
|
|
237
|
-
<NavContent
|
|
238
|
-
chatFirstMode={chatFirstMode}
|
|
239
|
-
collapsed={collapsed}
|
|
240
|
-
reserveEnvironmentBadgeSpace
|
|
241
|
-
/>
|
|
257
|
+
<NavContent chatFirstMode={chatFirstMode} collapsed={collapsed} />
|
|
242
258
|
</TooltipProvider>
|
|
243
259
|
</MemoryRouter>,
|
|
244
260
|
);
|
|
@@ -258,7 +274,7 @@ describe("Dispatch NavContent", () => {
|
|
|
258
274
|
|
|
259
275
|
expect(footer?.className).toContain("mt-auto");
|
|
260
276
|
expect(footer?.className).toContain("shrink-0");
|
|
261
|
-
expect(footer?.className).toContain("pb-10");
|
|
277
|
+
expect(footer?.className).not.toContain("pb-10");
|
|
262
278
|
if (mode === "standard") {
|
|
263
279
|
expect(footer?.closest(".overflow-y-auto")).toBeNull();
|
|
264
280
|
}
|
|
@@ -278,23 +294,6 @@ describe("Dispatch NavContent", () => {
|
|
|
278
294
|
},
|
|
279
295
|
);
|
|
280
296
|
|
|
281
|
-
it("reserves environment pill space in the mobile footer", async () => {
|
|
282
|
-
await act(async () => {
|
|
283
|
-
root.render(
|
|
284
|
-
<MemoryRouter initialEntries={["/overview"]}>
|
|
285
|
-
<TooltipProvider>
|
|
286
|
-
<NavContent reserveEnvironmentBadgeSpace />
|
|
287
|
-
</TooltipProvider>
|
|
288
|
-
</MemoryRouter>,
|
|
289
|
-
);
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
expect(
|
|
293
|
-
container.querySelector('[data-dispatch-sidebar-footer="standard"]')
|
|
294
|
-
?.className,
|
|
295
|
-
).toContain("pb-10");
|
|
296
|
-
});
|
|
297
|
-
|
|
298
297
|
it("keeps chat-first primary actions in the collapsed sidebar", async () => {
|
|
299
298
|
await act(async () => {
|
|
300
299
|
root.render(
|
|
@@ -622,3 +621,122 @@ describe("Dispatch NavContent", () => {
|
|
|
622
621
|
expect(clientState.switchThread).toHaveBeenCalledWith("new-thread");
|
|
623
622
|
});
|
|
624
623
|
});
|
|
624
|
+
|
|
625
|
+
function readUnprefixedZIndexClass(className: string): number | null {
|
|
626
|
+
const match = className.match(/(?:^|\s)z-\[?(\d+)\]?(?=\s|$)/);
|
|
627
|
+
return match ? Number(match[1]) : null;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function readMobileZIndexClass(className: string): number | null {
|
|
631
|
+
const match = className.match(/max-\[767px\]:z-\[?(\d+)\]?(?=\s|$)/);
|
|
632
|
+
return match ? Number(match[1]) : null;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
describe("chat-first surface panel toggle stacking", () => {
|
|
636
|
+
it("keeps the toggle above the mobile full-screen surface panel overlay", async () => {
|
|
637
|
+
const { ChatFirstSurfacePanelToggle: RealChatFirstSurfacePanelToggle } =
|
|
638
|
+
await vi.importActual<
|
|
639
|
+
typeof import("@agent-native/core/client/agent-chat")
|
|
640
|
+
>("@agent-native/core/client/agent-chat");
|
|
641
|
+
const { ChatFirstSurfacePanel } =
|
|
642
|
+
await import("@agent-native/core/client/chat-first");
|
|
643
|
+
|
|
644
|
+
const container = document.createElement("div");
|
|
645
|
+
document.body.appendChild(container);
|
|
646
|
+
const root = createRoot(container);
|
|
647
|
+
|
|
648
|
+
await act(async () => {
|
|
649
|
+
root.render(
|
|
650
|
+
<>
|
|
651
|
+
<ChatFirstSurfacePanel width={320} onResizePointerDown={() => {}}>
|
|
652
|
+
<div>side surface content</div>
|
|
653
|
+
</ChatFirstSurfacePanel>
|
|
654
|
+
<RealChatFirstSurfacePanelToggle
|
|
655
|
+
open={false}
|
|
656
|
+
onToggle={() => {}}
|
|
657
|
+
className={CHAT_FIRST_SURFACE_PANEL_TOGGLE_CLASS_NAME}
|
|
658
|
+
/>
|
|
659
|
+
</>,
|
|
660
|
+
);
|
|
661
|
+
});
|
|
662
|
+
|
|
663
|
+
const panelClassName =
|
|
664
|
+
container.querySelector("[data-chat-first-surface-panel]")?.className ??
|
|
665
|
+
"";
|
|
666
|
+
const toggleClassName =
|
|
667
|
+
container.querySelector("[data-chat-first-surface-toggle]")?.className ??
|
|
668
|
+
"";
|
|
669
|
+
|
|
670
|
+
// Below 768px the panel becomes a full-screen absolute overlay at this
|
|
671
|
+
// z-index (surface-panel.tsx). The toggle is the only control that can
|
|
672
|
+
// dismiss it, so it must always paint above that overlay.
|
|
673
|
+
const panelMobileZIndex = readMobileZIndexClass(panelClassName);
|
|
674
|
+
const toggleZIndex = readUnprefixedZIndexClass(toggleClassName);
|
|
675
|
+
expect(panelMobileZIndex).not.toBeNull();
|
|
676
|
+
expect(toggleZIndex).not.toBeNull();
|
|
677
|
+
expect(toggleZIndex as number).toBeGreaterThan(panelMobileZIndex as number);
|
|
678
|
+
|
|
679
|
+
act(() => root.unmount());
|
|
680
|
+
container.remove();
|
|
681
|
+
});
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
describe("chat-first app surface tab chat rail", () => {
|
|
685
|
+
const registration = { id: "mail", name: "Mail", path: "/mail" };
|
|
686
|
+
|
|
687
|
+
beforeEach(() => {
|
|
688
|
+
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
|
|
689
|
+
});
|
|
690
|
+
|
|
691
|
+
afterEach(() => {
|
|
692
|
+
vi.unstubAllGlobals();
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
async function renderAppTab(isMobileSurface: boolean) {
|
|
696
|
+
const { defaultChatFirstCopy } =
|
|
697
|
+
await import("@agent-native/core/client/chat-first");
|
|
698
|
+
vi.stubGlobal(
|
|
699
|
+
"fetch",
|
|
700
|
+
vi.fn().mockResolvedValue(new Response("{}", { status: 200 })),
|
|
701
|
+
);
|
|
702
|
+
const container = document.createElement("div");
|
|
703
|
+
document.body.appendChild(container);
|
|
704
|
+
const root = createRoot(container);
|
|
705
|
+
await act(async () => {
|
|
706
|
+
root.render(
|
|
707
|
+
renderChatFirstAppSurfaceTab({
|
|
708
|
+
registration,
|
|
709
|
+
embedPath: "/mail",
|
|
710
|
+
loading: false,
|
|
711
|
+
isMobileSurface,
|
|
712
|
+
copy: defaultChatFirstCopy,
|
|
713
|
+
}),
|
|
714
|
+
);
|
|
715
|
+
});
|
|
716
|
+
return { container, root };
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
it("does not mount a second full-screen chat rail while the mobile surface panel already covers the screen", async () => {
|
|
720
|
+
const { container, root } = await renderAppTab(true);
|
|
721
|
+
|
|
722
|
+
// ChatFirstSurfacePanel is already a full-screen overlay below 768px
|
|
723
|
+
// (surface-panel.tsx). A nested AgentSidebar chat rail here would stack a
|
|
724
|
+
// second full-screen shell on top of it.
|
|
725
|
+
expect(container.querySelector("[data-agent-sidebar]")).toBeNull();
|
|
726
|
+
expect(
|
|
727
|
+
container.querySelector("[data-chat-first-app-pane]"),
|
|
728
|
+
).not.toBeNull();
|
|
729
|
+
|
|
730
|
+
act(() => root.unmount());
|
|
731
|
+
container.remove();
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
it("still gives the app its own chat rail on desktop, where the surface panel is inline", async () => {
|
|
735
|
+
const { container, root } = await renderAppTab(false);
|
|
736
|
+
|
|
737
|
+
expect(container.querySelector("[data-agent-sidebar]")).not.toBeNull();
|
|
738
|
+
|
|
739
|
+
act(() => root.unmount());
|
|
740
|
+
container.remove();
|
|
741
|
+
});
|
|
742
|
+
});
|
|
@@ -99,6 +99,7 @@ import {
|
|
|
99
99
|
import { Link, useLocation, useNavigate } from "react-router";
|
|
100
100
|
import { toast } from "sonner";
|
|
101
101
|
|
|
102
|
+
import { useIsMobile } from "../../hooks/use-mobile";
|
|
102
103
|
import { cn } from "../../lib/utils";
|
|
103
104
|
import {
|
|
104
105
|
isDispatchWorkspaceAppId,
|
|
@@ -211,6 +212,12 @@ const DISPATCH_SIDEBAR_LABEL = "Dispatch";
|
|
|
211
212
|
const CHROMELESS_PATHS = ["/approval", "/browser-chat", "/browser-connect"];
|
|
212
213
|
const SIDEBAR_COLLAPSE_KEY = "dispatch.sidebar.collapsed";
|
|
213
214
|
const CHAT_HISTORY_SOURCE_KEY = "dispatch.chat-history.source";
|
|
215
|
+
// Below 768px, ChatFirstSurfacePanel becomes a max-[767px]:z-10 full-screen
|
|
216
|
+
// overlay (surface-panel.tsx). This toggle is the only way to dismiss it, so
|
|
217
|
+
// its z-index must stay above that overlay in every stacking context or the
|
|
218
|
+
// panel becomes undismissable on mobile.
|
|
219
|
+
export const CHAT_FIRST_SURFACE_PANEL_TOGGLE_CLASS_NAME =
|
|
220
|
+
"absolute right-3 top-2 z-20";
|
|
214
221
|
|
|
215
222
|
interface DispatchChatFirstPane {
|
|
216
223
|
appId: string;
|
|
@@ -953,7 +960,6 @@ export function NavContent({
|
|
|
953
960
|
chatFirstEmbedded = false,
|
|
954
961
|
collapsed = false,
|
|
955
962
|
collapsible = false,
|
|
956
|
-
reserveEnvironmentBadgeSpace = false,
|
|
957
963
|
onCollapsedChange,
|
|
958
964
|
chatFirstAppLayout,
|
|
959
965
|
onChatFirstAppLayoutChange,
|
|
@@ -972,7 +978,6 @@ export function NavContent({
|
|
|
972
978
|
chatFirstEmbedded?: boolean;
|
|
973
979
|
collapsed?: boolean;
|
|
974
980
|
collapsible?: boolean;
|
|
975
|
-
reserveEnvironmentBadgeSpace?: boolean;
|
|
976
981
|
onCollapsedChange?: (collapsed: boolean) => void;
|
|
977
982
|
chatFirstAppLayout?: ChatFirstAppLayoutPreference;
|
|
978
983
|
onChatFirstAppLayoutChange?: (layout: ChatFirstAppLayoutPreference) => void;
|
|
@@ -1314,10 +1319,7 @@ export function NavContent({
|
|
|
1314
1319
|
</div>
|
|
1315
1320
|
) : null}
|
|
1316
1321
|
<div
|
|
1317
|
-
className=
|
|
1318
|
-
"mt-auto shrink-0",
|
|
1319
|
-
reserveEnvironmentBadgeSpace && "pb-10",
|
|
1320
|
-
)}
|
|
1322
|
+
className="mt-auto shrink-0"
|
|
1321
1323
|
data-dispatch-sidebar-footer={chatFirstMode ? "chat-first" : "standard"}
|
|
1322
1324
|
>
|
|
1323
1325
|
{bottomNavigation}
|
|
@@ -1328,6 +1330,64 @@ export function NavContent({
|
|
|
1328
1330
|
);
|
|
1329
1331
|
}
|
|
1330
1332
|
|
|
1333
|
+
/**
|
|
1334
|
+
* Below 768px `ChatFirstSurfacePanel` is already a full-screen overlay
|
|
1335
|
+
* (surface-panel.tsx). Mounting the sub-app's own `AgentSidebar` chat rail
|
|
1336
|
+
* inside it too would stack a second full-screen shell on top of it, so the
|
|
1337
|
+
* rail only gets its own chat surface once there is room beside the panel.
|
|
1338
|
+
*/
|
|
1339
|
+
export function renderChatFirstAppSurfaceTab({
|
|
1340
|
+
registration,
|
|
1341
|
+
embedPath,
|
|
1342
|
+
loading,
|
|
1343
|
+
isMobileSurface,
|
|
1344
|
+
copy,
|
|
1345
|
+
}: {
|
|
1346
|
+
registration: ChatFirstAppRegistration | null;
|
|
1347
|
+
embedPath: string;
|
|
1348
|
+
loading: boolean;
|
|
1349
|
+
isMobileSurface: boolean;
|
|
1350
|
+
copy: ChatFirstCopy;
|
|
1351
|
+
}): ReactNode {
|
|
1352
|
+
if (!registration) {
|
|
1353
|
+
return (
|
|
1354
|
+
<ChatFirstAppPane
|
|
1355
|
+
app={null}
|
|
1356
|
+
status={loading ? "loading" : "unresolved"}
|
|
1357
|
+
renderEmbed={() => null}
|
|
1358
|
+
copy={copy}
|
|
1359
|
+
/>
|
|
1360
|
+
);
|
|
1361
|
+
}
|
|
1362
|
+
if (!embedPath.startsWith("/")) {
|
|
1363
|
+
return (
|
|
1364
|
+
<ChatFirstAppPane
|
|
1365
|
+
app={{
|
|
1366
|
+
id: registration.id,
|
|
1367
|
+
name: registration.name ?? registration.id,
|
|
1368
|
+
}}
|
|
1369
|
+
status="error"
|
|
1370
|
+
errorMessage={copy("appUnavailable")}
|
|
1371
|
+
renderEmbed={() => null}
|
|
1372
|
+
copy={copy}
|
|
1373
|
+
/>
|
|
1374
|
+
);
|
|
1375
|
+
}
|
|
1376
|
+
return (
|
|
1377
|
+
<WorkspaceAppFrame
|
|
1378
|
+
app={{
|
|
1379
|
+
id: registration.id,
|
|
1380
|
+
name: registration.name ?? registration.id,
|
|
1381
|
+
path: registration.path,
|
|
1382
|
+
url: registration.url,
|
|
1383
|
+
}}
|
|
1384
|
+
embedPath={embedPath}
|
|
1385
|
+
chatSidebar={!isMobileSurface}
|
|
1386
|
+
copy={copy}
|
|
1387
|
+
/>
|
|
1388
|
+
);
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1331
1391
|
export function Layout({
|
|
1332
1392
|
children,
|
|
1333
1393
|
extensions,
|
|
@@ -1341,6 +1401,10 @@ export function Layout({
|
|
|
1341
1401
|
const location = useLocation();
|
|
1342
1402
|
const navigate = useNavigate();
|
|
1343
1403
|
const [mobileOpen, setMobileOpen] = useState(false);
|
|
1404
|
+
// Drives renderChatFirstSurfaceTab's app-tab chatSidebar decision below —
|
|
1405
|
+
// the chat-first surface panel is already a full-screen overlay at this
|
|
1406
|
+
// width, so an app tab must not also mount its own full-screen chat rail.
|
|
1407
|
+
const isMobileSurface = useIsMobile();
|
|
1344
1408
|
const [sidebarCollapsed, setSidebarCollapsed] = useState(() => {
|
|
1345
1409
|
if (typeof window === "undefined") return false;
|
|
1346
1410
|
try {
|
|
@@ -2093,44 +2157,13 @@ export function Layout({
|
|
|
2093
2157
|
(candidate) => candidate.id === tab.appId,
|
|
2094
2158
|
) ?? null)
|
|
2095
2159
|
: null;
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
/>
|
|
2104
|
-
);
|
|
2105
|
-
}
|
|
2106
|
-
const embedPath = tab.path ?? registration.path ?? "/";
|
|
2107
|
-
if (!embedPath.startsWith("/")) {
|
|
2108
|
-
return (
|
|
2109
|
-
<ChatFirstAppPane
|
|
2110
|
-
app={{
|
|
2111
|
-
id: registration.id,
|
|
2112
|
-
name: registration.name ?? registration.id,
|
|
2113
|
-
}}
|
|
2114
|
-
status="error"
|
|
2115
|
-
errorMessage={chatFirstCopy("appUnavailable")}
|
|
2116
|
-
renderEmbed={() => null}
|
|
2117
|
-
copy={chatFirstCopy}
|
|
2118
|
-
/>
|
|
2119
|
-
);
|
|
2120
|
-
}
|
|
2121
|
-
return (
|
|
2122
|
-
<WorkspaceAppFrame
|
|
2123
|
-
app={{
|
|
2124
|
-
id: registration.id,
|
|
2125
|
-
name: registration.name ?? registration.id,
|
|
2126
|
-
path: registration.path,
|
|
2127
|
-
url: registration.url,
|
|
2128
|
-
}}
|
|
2129
|
-
embedPath={embedPath}
|
|
2130
|
-
chatSidebar
|
|
2131
|
-
copy={chatFirstCopy}
|
|
2132
|
-
/>
|
|
2133
|
-
);
|
|
2160
|
+
return renderChatFirstAppSurfaceTab({
|
|
2161
|
+
registration,
|
|
2162
|
+
embedPath: tab.path ?? registration?.path ?? "/",
|
|
2163
|
+
loading: chatFirstAppsQuery.isLoading,
|
|
2164
|
+
isMobileSurface,
|
|
2165
|
+
copy: chatFirstCopy,
|
|
2166
|
+
});
|
|
2134
2167
|
}
|
|
2135
2168
|
if (tab.kind === "browser" && tab.url) {
|
|
2136
2169
|
return (
|
|
@@ -2203,6 +2236,7 @@ export function Layout({
|
|
|
2203
2236
|
chatFirstSurfaceTabs.activeTabId,
|
|
2204
2237
|
chatFirstAppRegistrations,
|
|
2205
2238
|
closeChatFirstSurfaceTab,
|
|
2239
|
+
isMobileSurface,
|
|
2206
2240
|
renderChatFirstWatchChat,
|
|
2207
2241
|
],
|
|
2208
2242
|
);
|
|
@@ -2270,7 +2304,7 @@ export function Layout({
|
|
|
2270
2304
|
<ChatFirstSurfacePanelToggle
|
|
2271
2305
|
open={chatFirstSurfacePanel.open}
|
|
2272
2306
|
onToggle={chatFirstSurfacePanel.toggle}
|
|
2273
|
-
className=
|
|
2307
|
+
className={CHAT_FIRST_SURFACE_PANEL_TOGGLE_CLASS_NAME}
|
|
2274
2308
|
/>
|
|
2275
2309
|
) : null}
|
|
2276
2310
|
{isChatRoute && chatFirstMode && chatFirstNotice ? (
|
|
@@ -2463,7 +2497,6 @@ export function Layout({
|
|
|
2463
2497
|
}}
|
|
2464
2498
|
onChatFirstAppsRetry={() => void chatFirstAppsQuery.refetch()}
|
|
2465
2499
|
collapsible
|
|
2466
|
-
reserveEnvironmentBadgeSpace
|
|
2467
2500
|
onCollapsedChange={setSidebarCollapsed}
|
|
2468
2501
|
/>
|
|
2469
2502
|
</aside>
|
|
@@ -2485,7 +2518,6 @@ export function Layout({
|
|
|
2485
2518
|
chatFirstMode={chatFirstMode}
|
|
2486
2519
|
chatFirstEmbedded={chatFirstEmbedded}
|
|
2487
2520
|
collapsed={false}
|
|
2488
|
-
reserveEnvironmentBadgeSpace
|
|
2489
2521
|
chatFirstAppLayout={chatFirstAppLayout}
|
|
2490
2522
|
onChatFirstAppLayoutChange={persistChatFirstAppLayout}
|
|
2491
2523
|
chatFirstApps={chatFirstAppItems}
|
|
@@ -85,6 +85,22 @@ function deferWorkspaceAppOverlayOpen(
|
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
// The settings menu's own DropdownMenuContent restores focus to its trigger
|
|
89
|
+
// once its FocusScope unmounts, independent of the requestAnimationFrame
|
|
90
|
+
// above. If that restore lands after the deferred overlay's DismissableLayer
|
|
91
|
+
// has already mounted, the resulting focusin event reads as an outside
|
|
92
|
+
// interaction and immediately dismisses the overlay we just opened. Skip the
|
|
93
|
+
// default restore whenever we're mid-handoff to a sibling overlay — mirrors
|
|
94
|
+
// AgentPanel's identical guard (consumeAgentPanelOverlayFocusRestore).
|
|
95
|
+
function consumeWorkspaceAppOverlayFocusRestore(
|
|
96
|
+
pendingOverlayRef: { current: boolean },
|
|
97
|
+
event: { preventDefault: () => void },
|
|
98
|
+
): void {
|
|
99
|
+
if (!pendingOverlayRef.current) return;
|
|
100
|
+
pendingOverlayRef.current = false;
|
|
101
|
+
event.preventDefault();
|
|
102
|
+
}
|
|
103
|
+
|
|
88
104
|
export function WorkspaceAppCard({
|
|
89
105
|
app,
|
|
90
106
|
className,
|
|
@@ -413,6 +429,7 @@ function WorkspaceAppSettings({
|
|
|
413
429
|
const [shareOpen, setShareOpen] = useState(false);
|
|
414
430
|
const [settingsOpen, setSettingsOpen] = useState(false);
|
|
415
431
|
const settingsTriggerRef = useRef<HTMLButtonElement>(null);
|
|
432
|
+
const pendingShareOverlayRef = useRef(false);
|
|
416
433
|
|
|
417
434
|
const handleShareOpenChange = (open: boolean) => {
|
|
418
435
|
setShareOpen(open);
|
|
@@ -452,6 +469,12 @@ function WorkspaceAppSettings({
|
|
|
452
469
|
<DropdownMenuContent
|
|
453
470
|
align="end"
|
|
454
471
|
className={cn(APP_ACTION_MENU_CONTENT_CLASS, "min-w-max")}
|
|
472
|
+
onCloseAutoFocus={(event) =>
|
|
473
|
+
consumeWorkspaceAppOverlayFocusRestore(
|
|
474
|
+
pendingShareOverlayRef,
|
|
475
|
+
event,
|
|
476
|
+
)
|
|
477
|
+
}
|
|
455
478
|
>
|
|
456
479
|
<DropdownMenuItem onSelect={onEdit}>
|
|
457
480
|
<IconEdit size={14} aria-hidden="true" />
|
|
@@ -462,7 +485,10 @@ function WorkspaceAppSettings({
|
|
|
462
485
|
onSelect={(event) =>
|
|
463
486
|
deferWorkspaceAppOverlayOpen(
|
|
464
487
|
event,
|
|
465
|
-
() =>
|
|
488
|
+
() => {
|
|
489
|
+
pendingShareOverlayRef.current = true;
|
|
490
|
+
setSettingsOpen(false);
|
|
491
|
+
},
|
|
466
492
|
() => setShareOpen(true),
|
|
467
493
|
)
|
|
468
494
|
}
|
|
@@ -12,6 +12,14 @@ describe("resolveCatchAllTarget", () => {
|
|
|
12
12
|
).toBe("/todo");
|
|
13
13
|
});
|
|
14
14
|
|
|
15
|
+
it("keeps the legacy mounted path for normalized agent IDs", () => {
|
|
16
|
+
expect(
|
|
17
|
+
resolveCatchAllTarget("clips", {
|
|
18
|
+
workspaceApps: [{ id: "clips", path: "/videos" }],
|
|
19
|
+
}),
|
|
20
|
+
).toBe("/videos");
|
|
21
|
+
});
|
|
22
|
+
|
|
15
23
|
it("falls back to the built-in template URL when no workspace manifest exists", () => {
|
|
16
24
|
expect(
|
|
17
25
|
resolveCatchAllTarget("forms", {
|
|
@@ -116,9 +116,11 @@ export async function resolveServerCatchAllTarget(
|
|
|
116
116
|
appId: string,
|
|
117
117
|
): Promise<string | null> {
|
|
118
118
|
if (!import.meta.env.SSR) return null;
|
|
119
|
-
const { getBuiltinAgents, loadWorkspaceAppsManifest } =
|
|
119
|
+
const { getBuiltinAgents, loadWorkspaceAppsManifest, normalizeAgentId } =
|
|
120
120
|
await import("@agent-native/core/server/agent-discovery");
|
|
121
|
-
|
|
121
|
+
// Dispatch receives legacy route segments before discovery normalizes the
|
|
122
|
+
// manifest IDs, so normalize the lookup key at this boundary as well.
|
|
123
|
+
return resolveCatchAllTarget(normalizeAgentId(appId), {
|
|
122
124
|
workspaceApps: loadWorkspaceAppsManifest(),
|
|
123
125
|
builtinAgents: getBuiltinAgents("dispatch"),
|
|
124
126
|
});
|