@agent-native/dispatch 0.22.1 → 0.23.1
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 +1 -1
- package/dist/components/app-icon.d.ts +2 -1
- package/dist/components/app-icon.d.ts.map +1 -1
- package/dist/components/app-icon.js +51 -29
- package/dist/components/app-icon.js.map +1 -1
- package/dist/components/app-list-row.d.ts +1 -1
- package/dist/components/app-list-row.d.ts.map +1 -1
- package/dist/components/app-list-row.js +1 -1
- package/dist/components/app-list-row.js.map +1 -1
- package/dist/components/app-open-actions.d.ts +2 -1
- package/dist/components/app-open-actions.d.ts.map +1 -1
- package/dist/components/app-open-actions.js +2 -2
- package/dist/components/app-open-actions.js.map +1 -1
- package/dist/components/dispatch-control-plane.d.ts.map +1 -1
- package/dist/components/dispatch-control-plane.js +16 -17
- package/dist/components/dispatch-control-plane.js.map +1 -1
- package/dist/components/layout/Layout.d.ts.map +1 -1
- package/dist/components/layout/Layout.js +137 -66
- package/dist/components/layout/Layout.js.map +1 -1
- package/dist/components/layout/workspace-apps-rail.d.ts.map +1 -1
- package/dist/components/layout/workspace-apps-rail.js +1 -1
- package/dist/components/layout/workspace-apps-rail.js.map +1 -1
- package/dist/components/other-apps-section.d.ts +3 -1
- package/dist/components/other-apps-section.d.ts.map +1 -1
- package/dist/components/other-apps-section.js +9 -6
- package/dist/components/other-apps-section.js.map +1 -1
- package/dist/components/workspace-app-card.d.ts.map +1 -1
- package/dist/components/workspace-app-card.js +7 -2
- package/dist/components/workspace-app-card.js.map +1 -1
- package/dist/components/workspace-template-card.d.ts.map +1 -1
- package/dist/components/workspace-template-card.js +2 -5
- package/dist/components/workspace-template-card.js.map +1 -1
- package/dist/lib/overview-chat.d.ts.map +1 -1
- package/dist/lib/overview-chat.js +0 -8
- package/dist/lib/overview-chat.js.map +1 -1
- package/dist/routes/pages/chat.js +1 -1
- package/dist/routes/pages/chat.js.map +1 -1
- package/dist/server/lib/mcp-gateway.d.ts.map +1 -1
- package/dist/server/lib/mcp-gateway.js +14 -1
- package/dist/server/lib/mcp-gateway.js.map +1 -1
- package/package.json +3 -3
- package/src/components/app-icon.tsx +71 -43
- package/src/components/app-list-row.tsx +1 -1
- package/src/components/app-open-actions.tsx +4 -2
- package/src/components/dispatch-control-plane.spec.tsx +45 -7
- package/src/components/dispatch-control-plane.tsx +56 -75
- package/src/components/layout/Layout.spec.tsx +53 -0
- package/src/components/layout/Layout.tsx +227 -99
- package/src/components/layout/workspace-apps-rail.tsx +1 -0
- package/src/components/other-apps-section.tsx +61 -40
- package/src/components/workspace-app-card.spec.tsx +63 -66
- package/src/components/workspace-app-card.tsx +10 -3
- package/src/components/workspace-template-card.spec.tsx +7 -8
- package/src/components/workspace-template-card.tsx +21 -11
- package/src/lib/overview-chat.spec.ts +0 -19
- package/src/lib/overview-chat.ts +0 -9
- package/src/routes/pages/chat.tsx +1 -1
- package/src/server/lib/mcp-gateway.spec.ts +64 -2
- package/src/server/lib/mcp-gateway.ts +19 -1
- package/src/styles/dispatch-css.spec.ts +11 -0
- package/src/styles/dispatch.css +2 -2
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { useT } from "@agent-native/core/client/i18n";
|
|
2
|
-
import { IconStack2 } from "@tabler/icons-react";
|
|
3
2
|
|
|
4
3
|
import { filterOtherApps, type ConnectedAppSummary } from "../lib/other-apps";
|
|
5
4
|
import type { WorkspaceAppId } from "../lib/other-apps";
|
|
@@ -80,6 +79,8 @@ export function OtherAppsSection({
|
|
|
80
79
|
onRetryConnectedApps,
|
|
81
80
|
templateLabels,
|
|
82
81
|
onRemixSuccess,
|
|
82
|
+
heading = "Other apps",
|
|
83
|
+
embeddedInList = false,
|
|
83
84
|
className,
|
|
84
85
|
}: {
|
|
85
86
|
templates?: CuratedWorkspaceTemplatesResult;
|
|
@@ -96,6 +97,8 @@ export function OtherAppsSection({
|
|
|
96
97
|
result: unknown,
|
|
97
98
|
template: CuratedWorkspaceTemplate,
|
|
98
99
|
) => void;
|
|
100
|
+
heading?: string | null;
|
|
101
|
+
embeddedInList?: boolean;
|
|
99
102
|
className?: string;
|
|
100
103
|
}) {
|
|
101
104
|
const t = useT();
|
|
@@ -109,26 +112,15 @@ export function OtherAppsSection({
|
|
|
109
112
|
|
|
110
113
|
if (!isLoading && !hasError && entries.length === 0) return null;
|
|
111
114
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
<
|
|
116
|
-
size={16}
|
|
117
|
-
className="mt-0.5 shrink-0 text-muted-foreground"
|
|
118
|
-
/>
|
|
119
|
-
<div className="min-w-0">
|
|
115
|
+
const content = (
|
|
116
|
+
<>
|
|
117
|
+
{heading ? (
|
|
118
|
+
<div className="flex min-w-0 items-center">
|
|
120
119
|
<h2 className="truncate text-sm font-semibold text-foreground">
|
|
121
|
-
{t("dispatch.pages.otherApps", { defaultValue:
|
|
120
|
+
{t("dispatch.pages.otherApps", { defaultValue: heading })}
|
|
122
121
|
</h2>
|
|
123
|
-
{entries.length > 0 ? (
|
|
124
|
-
<p className="mt-0.5 text-xs text-muted-foreground">
|
|
125
|
-
{t("dispatch.pages.availableCount", {
|
|
126
|
-
count: entries.length,
|
|
127
|
-
})}
|
|
128
|
-
</p>
|
|
129
|
-
) : null}
|
|
130
122
|
</div>
|
|
131
|
-
|
|
123
|
+
) : null}
|
|
132
124
|
|
|
133
125
|
{templatesError ? (
|
|
134
126
|
<ActionQueryError
|
|
@@ -144,31 +136,37 @@ export function OtherAppsSection({
|
|
|
144
136
|
) : null}
|
|
145
137
|
|
|
146
138
|
{isLoading && entries.length === 0 ? (
|
|
147
|
-
|
|
139
|
+
embeddedInList ? (
|
|
140
|
+
<OtherAppsSkeletonRows />
|
|
141
|
+
) : (
|
|
142
|
+
<OtherAppsSkeletonList />
|
|
143
|
+
)
|
|
148
144
|
) : entries.length > 0 ? (
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
)}
|
|
168
|
-
</AppList>
|
|
145
|
+
entries.map((entry) =>
|
|
146
|
+
entry.kind === "template" ? (
|
|
147
|
+
<WorkspaceTemplateCard
|
|
148
|
+
key={`template:${templateKey(entry.template)}`}
|
|
149
|
+
template={entry.template}
|
|
150
|
+
labels={templateLabels}
|
|
151
|
+
catalog
|
|
152
|
+
className={APP_LIST_GRID_ROW_CLASS}
|
|
153
|
+
onRemixSuccess={onRemixSuccess}
|
|
154
|
+
/>
|
|
155
|
+
) : (
|
|
156
|
+
<ConnectedAppCard
|
|
157
|
+
key={`connected:${entry.app.id}`}
|
|
158
|
+
app={entry.app}
|
|
159
|
+
className={APP_LIST_GRID_ROW_CLASS}
|
|
160
|
+
/>
|
|
161
|
+
),
|
|
162
|
+
)
|
|
169
163
|
) : null}
|
|
170
|
-
|
|
164
|
+
</>
|
|
171
165
|
);
|
|
166
|
+
|
|
167
|
+
if (embeddedInList) return content;
|
|
168
|
+
|
|
169
|
+
return <section className={cn("space-y-3", className)}>{content}</section>;
|
|
172
170
|
}
|
|
173
171
|
|
|
174
172
|
function OtherAppsSkeletonList() {
|
|
@@ -193,3 +191,26 @@ function OtherAppsSkeletonList() {
|
|
|
193
191
|
</AppList>
|
|
194
192
|
);
|
|
195
193
|
}
|
|
194
|
+
|
|
195
|
+
function OtherAppsSkeletonRows() {
|
|
196
|
+
return (
|
|
197
|
+
<>
|
|
198
|
+
{Array.from({ length: 4 }).map((_, index) => (
|
|
199
|
+
<div
|
|
200
|
+
key={index}
|
|
201
|
+
className={cn(
|
|
202
|
+
"flex min-w-0 items-center gap-3 border-b px-4 py-3.5 last:border-b-0",
|
|
203
|
+
APP_LIST_GRID_ROW_CLASS,
|
|
204
|
+
)}
|
|
205
|
+
>
|
|
206
|
+
<Skeleton className="size-8 shrink-0 rounded-xl" />
|
|
207
|
+
<div className="min-w-0 flex-1 space-y-2">
|
|
208
|
+
<Skeleton className="h-4 w-32" />
|
|
209
|
+
<Skeleton className="h-3 w-2/3" />
|
|
210
|
+
</div>
|
|
211
|
+
<Skeleton className="h-9 w-24 shrink-0 rounded-md" />
|
|
212
|
+
</div>
|
|
213
|
+
))}
|
|
214
|
+
</>
|
|
215
|
+
);
|
|
216
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// @vitest-environment happy-dom
|
|
2
2
|
import React, { act } from "react";
|
|
3
3
|
import { createRoot, type Root } from "react-dom/client";
|
|
4
|
+
import { MemoryRouter } from "react-router";
|
|
4
5
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
6
|
|
|
6
7
|
import { TooltipProvider } from "./ui/tooltip";
|
|
@@ -44,22 +45,24 @@ describe("WorkspaceAppCard", () => {
|
|
|
44
45
|
it("uses one explicit open action and one settings control", async () => {
|
|
45
46
|
await act(async () => {
|
|
46
47
|
root.render(
|
|
47
|
-
<
|
|
48
|
-
<
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
48
|
+
<MemoryRouter>
|
|
49
|
+
<TooltipProvider>
|
|
50
|
+
<WorkspaceAppCard
|
|
51
|
+
app={{
|
|
52
|
+
id: "analytics",
|
|
53
|
+
name: "Analytics",
|
|
54
|
+
path: "/analytics",
|
|
55
|
+
description: "Explore product and growth performance.",
|
|
56
|
+
status: "ready",
|
|
57
|
+
}}
|
|
58
|
+
/>
|
|
59
|
+
</TooltipProvider>
|
|
60
|
+
</MemoryRouter>,
|
|
58
61
|
);
|
|
59
62
|
});
|
|
60
63
|
|
|
61
64
|
const appLink = container.querySelector<HTMLAnchorElement>(
|
|
62
|
-
'a[href="/analytics"]',
|
|
65
|
+
'a[href="/apps/analytics"]',
|
|
63
66
|
);
|
|
64
67
|
expect(appLink).not.toBeNull();
|
|
65
68
|
expect(appLink?.textContent).toContain("Open app");
|
|
@@ -67,25 +70,13 @@ describe("WorkspaceAppCard", () => {
|
|
|
67
70
|
expect(appLink?.getAttribute("target")).toBeNull();
|
|
68
71
|
expect(appLink?.getAttribute("rel")).toBeNull();
|
|
69
72
|
expect(appLink?.querySelector("svg")).toBeNull();
|
|
70
|
-
expect(container.textContent).not.toContain("/analytics");
|
|
73
|
+
expect(container.textContent).not.toContain("/apps/analytics");
|
|
71
74
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
openOptionsButton?.dispatchEvent(
|
|
78
|
-
new MouseEvent("pointerdown", { bubbles: true, button: 0 }),
|
|
79
|
-
);
|
|
80
|
-
});
|
|
81
|
-
const openInlineLink = Array.from(
|
|
82
|
-
document.querySelectorAll<HTMLAnchorElement>('[role="menu"] a'),
|
|
83
|
-
).find((anchor) => anchor.textContent?.includes("Open inline"));
|
|
84
|
-
const openNewTabLink = Array.from(
|
|
85
|
-
document.querySelectorAll<HTMLAnchorElement>('[role="menu"] a'),
|
|
86
|
-
).find((anchor) => anchor.textContent?.includes("Open in new tab"));
|
|
87
|
-
expect(openInlineLink?.getAttribute("target")).toBeNull();
|
|
88
|
-
expect(openNewTabLink?.getAttribute("target")).toBe("_blank");
|
|
75
|
+
expect(
|
|
76
|
+
container.querySelector(
|
|
77
|
+
'button[aria-label="Open options for Analytics"]',
|
|
78
|
+
),
|
|
79
|
+
).toBeNull();
|
|
89
80
|
|
|
90
81
|
const settingsButton = container.querySelector<HTMLButtonElement>(
|
|
91
82
|
'button[aria-label="Settings for Analytics"]',
|
|
@@ -107,17 +98,19 @@ describe("WorkspaceAppCard", () => {
|
|
|
107
98
|
it("labels per-app context as agent resources while preserving workspace scope", async () => {
|
|
108
99
|
await act(async () => {
|
|
109
100
|
root.render(
|
|
110
|
-
<
|
|
111
|
-
<
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
101
|
+
<MemoryRouter>
|
|
102
|
+
<TooltipProvider>
|
|
103
|
+
<WorkspaceAppCard
|
|
104
|
+
app={{
|
|
105
|
+
id: "analytics",
|
|
106
|
+
name: "Analytics",
|
|
107
|
+
path: "/analytics",
|
|
108
|
+
description: "Explore product and growth performance.",
|
|
109
|
+
status: "ready",
|
|
110
|
+
}}
|
|
111
|
+
/>
|
|
112
|
+
</TooltipProvider>
|
|
113
|
+
</MemoryRouter>,
|
|
121
114
|
);
|
|
122
115
|
});
|
|
123
116
|
|
|
@@ -149,17 +142,19 @@ describe("WorkspaceAppCard", () => {
|
|
|
149
142
|
it("opens pending Builder apps in a new tab", async () => {
|
|
150
143
|
await act(async () => {
|
|
151
144
|
root.render(
|
|
152
|
-
<
|
|
153
|
-
<
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
145
|
+
<MemoryRouter>
|
|
146
|
+
<TooltipProvider>
|
|
147
|
+
<WorkspaceAppCard
|
|
148
|
+
app={{
|
|
149
|
+
id: "new-app",
|
|
150
|
+
name: "New app",
|
|
151
|
+
path: "/new-app",
|
|
152
|
+
builderUrl: "https://builder.example.com/projects/new-app",
|
|
153
|
+
status: "pending",
|
|
154
|
+
}}
|
|
155
|
+
/>
|
|
156
|
+
</TooltipProvider>
|
|
157
|
+
</MemoryRouter>,
|
|
163
158
|
);
|
|
164
159
|
});
|
|
165
160
|
|
|
@@ -177,20 +172,22 @@ describe("WorkspaceAppCard", () => {
|
|
|
177
172
|
it("shows ownership metadata in the settings menu", async () => {
|
|
178
173
|
await act(async () => {
|
|
179
174
|
root.render(
|
|
180
|
-
<
|
|
181
|
-
<
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
175
|
+
<MemoryRouter>
|
|
176
|
+
<TooltipProvider>
|
|
177
|
+
<WorkspaceAppCard
|
|
178
|
+
app={{
|
|
179
|
+
id: "analytics",
|
|
180
|
+
name: "Analytics",
|
|
181
|
+
path: "/analytics",
|
|
182
|
+
createdAt: "2026-07-28T12:00:00.000Z",
|
|
183
|
+
createdBy: "creator@example.com",
|
|
184
|
+
owner: "owner@example.com",
|
|
185
|
+
teams: ["Growth", "Operations"],
|
|
186
|
+
status: "ready",
|
|
187
|
+
}}
|
|
188
|
+
/>
|
|
189
|
+
</TooltipProvider>
|
|
190
|
+
</MemoryRouter>,
|
|
194
191
|
);
|
|
195
192
|
});
|
|
196
193
|
|
|
@@ -20,12 +20,14 @@ import {
|
|
|
20
20
|
IconWorld,
|
|
21
21
|
} from "@tabler/icons-react";
|
|
22
22
|
import { useEffect, useState, type FormEvent } from "react";
|
|
23
|
+
import { useNavigate } from "react-router";
|
|
23
24
|
import { toast } from "sonner";
|
|
24
25
|
|
|
25
26
|
import { cn } from "../lib/utils";
|
|
26
27
|
import {
|
|
27
28
|
isPendingBuilderHref,
|
|
28
29
|
workspaceAppHref,
|
|
30
|
+
workspaceAppRoute,
|
|
29
31
|
type WorkspaceAppSummary,
|
|
30
32
|
} from "../lib/workspace-apps";
|
|
31
33
|
import { ActionQueryError } from "./action-query-error";
|
|
@@ -273,7 +275,9 @@ function WorkspaceAppOpenActions({
|
|
|
273
275
|
app: WorkspaceAppSummary;
|
|
274
276
|
href: string | null;
|
|
275
277
|
}) {
|
|
278
|
+
const navigate = useNavigate();
|
|
276
279
|
const openInNewTab = isPendingBuilderHref(app);
|
|
280
|
+
const appRoute = workspaceAppRoute(app.id);
|
|
277
281
|
|
|
278
282
|
if (!href) {
|
|
279
283
|
return (
|
|
@@ -286,11 +290,14 @@ function WorkspaceAppOpenActions({
|
|
|
286
290
|
return (
|
|
287
291
|
<AppOpenActions
|
|
288
292
|
name={app.name}
|
|
289
|
-
href={href}
|
|
293
|
+
href={openInNewTab ? href : appRoute}
|
|
290
294
|
target={openInNewTab ? "_blank" : undefined}
|
|
291
295
|
rel={openInNewTab ? "noreferrer" : undefined}
|
|
292
|
-
showInlineOption
|
|
293
|
-
|
|
296
|
+
showInlineOption={openInNewTab}
|
|
297
|
+
onOpenInline={() => {
|
|
298
|
+
navigate(appRoute);
|
|
299
|
+
}}
|
|
300
|
+
showNewTabOption={openInNewTab}
|
|
294
301
|
/>
|
|
295
302
|
);
|
|
296
303
|
}
|
|
@@ -153,16 +153,15 @@ describe("WorkspaceTemplateCard", () => {
|
|
|
153
153
|
);
|
|
154
154
|
});
|
|
155
155
|
|
|
156
|
-
it("keeps add app
|
|
156
|
+
it("keeps add app out of the catalog split-button menu", async () => {
|
|
157
157
|
await act(async () => {
|
|
158
158
|
root.render(<WorkspaceTemplateCard template={template} catalog />);
|
|
159
159
|
});
|
|
160
160
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
).toBe(false);
|
|
161
|
+
const addButton = Array.from(container.querySelectorAll("button")).find(
|
|
162
|
+
(button) => button.textContent?.includes("Add app"),
|
|
163
|
+
);
|
|
164
|
+
expect(addButton).toBeDefined();
|
|
166
165
|
|
|
167
166
|
const optionsButton = container.querySelector<HTMLButtonElement>(
|
|
168
167
|
'button[aria-label="Open options for Weekly report"]',
|
|
@@ -177,8 +176,8 @@ describe("WorkspaceTemplateCard", () => {
|
|
|
177
176
|
const addItem = Array.from(
|
|
178
177
|
document.querySelectorAll<HTMLElement>('[role="menuitem"]'),
|
|
179
178
|
).find((item) => item.textContent?.includes("Add app"));
|
|
180
|
-
expect(addItem).
|
|
181
|
-
await act(async () =>
|
|
179
|
+
expect(addItem).toBeUndefined();
|
|
180
|
+
await act(async () => addButton?.click());
|
|
182
181
|
expect(document.body.textContent).toContain("Choose the URL-safe id");
|
|
183
182
|
});
|
|
184
183
|
|
|
@@ -214,17 +214,27 @@ export function WorkspaceTemplateCard({
|
|
|
214
214
|
</div>
|
|
215
215
|
<div className="flex shrink-0 items-center gap-2">
|
|
216
216
|
{catalog ? (
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
217
|
+
<>
|
|
218
|
+
{liveUrl ? (
|
|
219
|
+
<AppOpenActions
|
|
220
|
+
name={template.name}
|
|
221
|
+
href={liveUrl}
|
|
222
|
+
target="_blank"
|
|
223
|
+
rel="noreferrer"
|
|
224
|
+
labels={{ openApp: labels.openApp }}
|
|
225
|
+
showNewTabOption
|
|
226
|
+
/>
|
|
227
|
+
) : null}
|
|
228
|
+
<Button
|
|
229
|
+
type="button"
|
|
230
|
+
variant="outline"
|
|
231
|
+
size="sm"
|
|
232
|
+
onClick={() => setOpen(true)}
|
|
233
|
+
>
|
|
234
|
+
<IconPlus />
|
|
235
|
+
{labels.remix}
|
|
236
|
+
</Button>
|
|
237
|
+
</>
|
|
228
238
|
) : liveUrl ? (
|
|
229
239
|
<Button variant="ghost" size="sm" className="shrink-0" asChild>
|
|
230
240
|
<a href={liveUrl} target="_blank" rel="noreferrer">
|
|
@@ -1,21 +1,15 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
2
|
|
|
3
|
-
const frameState = vi.hoisted(() => ({ inBuilderFrame: false }));
|
|
4
3
|
const sendToAgentChatMock = vi.hoisted(() => vi.fn(() => "chat-tab"));
|
|
5
4
|
|
|
6
5
|
vi.mock("@agent-native/core/client/agent-chat", () => ({
|
|
7
6
|
sendToAgentChat: sendToAgentChatMock,
|
|
8
7
|
}));
|
|
9
8
|
|
|
10
|
-
vi.mock("@agent-native/core/client/host", () => ({
|
|
11
|
-
isInBuilderFrame: () => frameState.inBuilderFrame,
|
|
12
|
-
}));
|
|
13
|
-
|
|
14
9
|
const { submitOverviewPrompt } = await import("./overview-chat.js");
|
|
15
10
|
|
|
16
11
|
describe("submitOverviewPrompt", () => {
|
|
17
12
|
beforeEach(() => {
|
|
18
|
-
frameState.inBuilderFrame = false;
|
|
19
13
|
sendToAgentChatMock.mockClear();
|
|
20
14
|
});
|
|
21
15
|
|
|
@@ -50,19 +44,6 @@ describe("submitOverviewPrompt", () => {
|
|
|
50
44
|
});
|
|
51
45
|
});
|
|
52
46
|
|
|
53
|
-
it("routes overview prompts to Builder chat inside Builder", () => {
|
|
54
|
-
frameState.inBuilderFrame = true;
|
|
55
|
-
|
|
56
|
-
const tabId = submitOverviewPrompt("ship the onboarding flow", "auto");
|
|
57
|
-
|
|
58
|
-
expect(tabId).toBe("chat-tab");
|
|
59
|
-
expect(sendToAgentChatMock).toHaveBeenCalledWith({
|
|
60
|
-
message: "ship the onboarding flow",
|
|
61
|
-
submit: true,
|
|
62
|
-
type: "code",
|
|
63
|
-
});
|
|
64
|
-
});
|
|
65
|
-
|
|
66
47
|
it("ignores empty prompts", () => {
|
|
67
48
|
expect(submitOverviewPrompt(" ", "auto")).toBeNull();
|
|
68
49
|
expect(sendToAgentChatMock).not.toHaveBeenCalled();
|
package/src/lib/overview-chat.ts
CHANGED
|
@@ -2,7 +2,6 @@ import {
|
|
|
2
2
|
sendToAgentChat,
|
|
3
3
|
type AgentChatMessage,
|
|
4
4
|
} from "@agent-native/core/client/agent-chat";
|
|
5
|
-
import { isInBuilderFrame } from "@agent-native/core/client/host";
|
|
6
5
|
|
|
7
6
|
interface OverviewChatOptions {
|
|
8
7
|
openSidebar?: boolean;
|
|
@@ -18,14 +17,6 @@ export function submitOverviewPrompt(
|
|
|
18
17
|
const trimmed = message.trim();
|
|
19
18
|
if (!trimmed) return null;
|
|
20
19
|
|
|
21
|
-
if (isInBuilderFrame()) {
|
|
22
|
-
return sendToAgentChat({
|
|
23
|
-
message: trimmed,
|
|
24
|
-
submit: true,
|
|
25
|
-
type: "code",
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
|
-
|
|
29
20
|
return sendToAgentChat({
|
|
30
21
|
message: trimmed,
|
|
31
22
|
submit: true,
|
|
@@ -174,7 +174,7 @@ export default function ChatRoute() {
|
|
|
174
174
|
<DispatchAgentChatSurface
|
|
175
175
|
mode="page"
|
|
176
176
|
chatViewTransition
|
|
177
|
-
className="dispatch-chat-panel"
|
|
177
|
+
className="dispatch-chat-panel px-4 sm:px-6"
|
|
178
178
|
defaultMode="chat"
|
|
179
179
|
storageKey="dispatch"
|
|
180
180
|
threadUrlSync={threadUrlSync}
|
|
@@ -880,6 +880,54 @@ describe("createGrantedDispatchMcpEmbedSession", () => {
|
|
|
880
880
|
).rejects.toThrow(/safe app-relative route/);
|
|
881
881
|
});
|
|
882
882
|
|
|
883
|
+
it("resolves target-relative embed start URLs against the granted app", async () => {
|
|
884
|
+
mocks.managerCallTool.mockResolvedValueOnce({
|
|
885
|
+
structuredContent: {
|
|
886
|
+
startUrl: "/_agent-native/embed/start?ticket=relative",
|
|
887
|
+
},
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
const result = await runWithRequestContext(
|
|
891
|
+
{
|
|
892
|
+
userEmail: "owner@example.test",
|
|
893
|
+
requestOrigin: "http://localhost:8092",
|
|
894
|
+
},
|
|
895
|
+
() =>
|
|
896
|
+
createGrantedDispatchMcpEmbedSession({
|
|
897
|
+
app: "analytics",
|
|
898
|
+
path: "/overview",
|
|
899
|
+
chrome: "minimal",
|
|
900
|
+
}),
|
|
901
|
+
);
|
|
902
|
+
|
|
903
|
+
expect(result.startUrl).toBe(
|
|
904
|
+
"http://localhost:8086/_agent-native/embed/start?ticket=relative",
|
|
905
|
+
);
|
|
906
|
+
});
|
|
907
|
+
|
|
908
|
+
it("rejects cross-origin embed start URLs from a granted app", async () => {
|
|
909
|
+
mocks.managerCallTool.mockResolvedValueOnce({
|
|
910
|
+
structuredContent: {
|
|
911
|
+
startUrl: "https://attacker.example/steal?ticket=remote",
|
|
912
|
+
},
|
|
913
|
+
});
|
|
914
|
+
|
|
915
|
+
await expect(
|
|
916
|
+
runWithRequestContext(
|
|
917
|
+
{
|
|
918
|
+
userEmail: "owner@example.test",
|
|
919
|
+
requestOrigin: "http://localhost:8092",
|
|
920
|
+
},
|
|
921
|
+
() =>
|
|
922
|
+
createGrantedDispatchMcpEmbedSession({
|
|
923
|
+
app: "analytics",
|
|
924
|
+
path: "/overview",
|
|
925
|
+
chrome: "minimal",
|
|
926
|
+
}),
|
|
927
|
+
),
|
|
928
|
+
).rejects.toThrow(/invalid embed start URL/);
|
|
929
|
+
});
|
|
930
|
+
|
|
883
931
|
it("rejects a URL that does not belong to the explicitly named app", async () => {
|
|
884
932
|
await expect(
|
|
885
933
|
runWithRequestContext(
|
|
@@ -905,6 +953,12 @@ describe("createGrantedDispatchMcpEmbedSession", () => {
|
|
|
905
953
|
url: "http://localhost:8092/analytics",
|
|
906
954
|
},
|
|
907
955
|
]);
|
|
956
|
+
mocks.managerCallTool.mockResolvedValueOnce({
|
|
957
|
+
structuredContent: {
|
|
958
|
+
startUrl:
|
|
959
|
+
"http://localhost:8092/analytics/_agent-native/embed/start?ticket=remote",
|
|
960
|
+
},
|
|
961
|
+
});
|
|
908
962
|
|
|
909
963
|
const result = await runWithRequestContext(
|
|
910
964
|
{
|
|
@@ -934,7 +988,8 @@ describe("createGrantedDispatchMcpEmbedSession", () => {
|
|
|
934
988
|
);
|
|
935
989
|
expect(result).toEqual({
|
|
936
990
|
app: "analytics",
|
|
937
|
-
startUrl:
|
|
991
|
+
startUrl:
|
|
992
|
+
"http://localhost:8092/analytics/_agent-native/embed/start?ticket=remote",
|
|
938
993
|
});
|
|
939
994
|
});
|
|
940
995
|
|
|
@@ -955,6 +1010,12 @@ describe("createGrantedDispatchMcpEmbedSession", () => {
|
|
|
955
1010
|
color: "#2563EB",
|
|
956
1011
|
},
|
|
957
1012
|
]);
|
|
1013
|
+
mocks.managerCallTool.mockResolvedValueOnce({
|
|
1014
|
+
structuredContent: {
|
|
1015
|
+
startUrl:
|
|
1016
|
+
"https://mail.agent-native.com/_agent-native/embed/start?ticket=remote",
|
|
1017
|
+
},
|
|
1018
|
+
});
|
|
958
1019
|
|
|
959
1020
|
const result = await runWithRequestContext(
|
|
960
1021
|
{
|
|
@@ -976,7 +1037,8 @@ describe("createGrantedDispatchMcpEmbedSession", () => {
|
|
|
976
1037
|
});
|
|
977
1038
|
expect(result).toEqual({
|
|
978
1039
|
app: "mail",
|
|
979
|
-
startUrl:
|
|
1040
|
+
startUrl:
|
|
1041
|
+
"https://mail.agent-native.com/_agent-native/embed/start?ticket=remote",
|
|
980
1042
|
});
|
|
981
1043
|
});
|
|
982
1044
|
|
|
@@ -451,6 +451,24 @@ function appBaseUrl(app: DispatchMcpAccessibleApp): string {
|
|
|
451
451
|
return app.url.replace(/\/+$/, "");
|
|
452
452
|
}
|
|
453
453
|
|
|
454
|
+
function resolveGrantedAppEmbedStartUrl(
|
|
455
|
+
app: DispatchMcpAccessibleApp,
|
|
456
|
+
startUrl: string,
|
|
457
|
+
): string {
|
|
458
|
+
try {
|
|
459
|
+
const baseUrl = appBaseUrl(app);
|
|
460
|
+
const resolved = new URL(startUrl, `${baseUrl}/`);
|
|
461
|
+
if (!appMatchesUrlPath(app, resolved)) {
|
|
462
|
+
throw new Error(
|
|
463
|
+
"Target app returned an embed start URL outside the granted app.",
|
|
464
|
+
);
|
|
465
|
+
}
|
|
466
|
+
return resolved.toString();
|
|
467
|
+
} catch {
|
|
468
|
+
throw new Error("Target app returned an invalid embed start URL.");
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
454
472
|
function appBasePath(app: DispatchMcpAccessibleApp): string {
|
|
455
473
|
const pathname = new URL(appBaseUrl(app)).pathname.replace(/\/+$/, "");
|
|
456
474
|
return pathname === "/" ? "" : pathname;
|
|
@@ -994,7 +1012,7 @@ export async function createGrantedDispatchMcpEmbedSession(input: {
|
|
|
994
1012
|
expiresAt?: number;
|
|
995
1013
|
app: string;
|
|
996
1014
|
} = {
|
|
997
|
-
startUrl: parsed.startUrl,
|
|
1015
|
+
startUrl: resolveGrantedAppEmbedStartUrl(target.app, parsed.startUrl),
|
|
998
1016
|
app: target.app.id,
|
|
999
1017
|
};
|
|
1000
1018
|
if (parsed.targetPath) output.targetPath = parsed.targetPath;
|
|
@@ -39,6 +39,17 @@ describe("dispatch Tailwind styles", () => {
|
|
|
39
39
|
'@import "@agent-native/dispatch/styles/dispatch.css";',
|
|
40
40
|
);
|
|
41
41
|
});
|
|
42
|
+
|
|
43
|
+
it("keeps the full-page composer on the shared wide width contract", () => {
|
|
44
|
+
const stylesheet = fs.readFileSync(
|
|
45
|
+
path.join(packageRoot, "src/styles/dispatch.css"),
|
|
46
|
+
"utf-8",
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
expect(stylesheet).toMatch(
|
|
50
|
+
/\.dispatch-chat-panel \[data-agent-empty-state="centered"\] \.agent-composer-area \{[\s\S]*?max-width: min\(750px, 100%\);/,
|
|
51
|
+
);
|
|
52
|
+
});
|
|
42
53
|
});
|
|
43
54
|
|
|
44
55
|
describe("dispatch route shells", () => {
|