@agent-native/dispatch 0.23.5 → 0.24.0
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/list-connected-agents.d.ts +1 -0
- package/dist/actions/list-connected-agents.js +5 -1
- package/dist/actions/list-connected-agents.js.map +1 -1
- package/dist/actions/list-dispatch-usage-metrics.d.ts +2 -0
- package/dist/actions/list-dispatch-usage-metrics.js +11 -1
- package/dist/actions/list-dispatch-usage-metrics.js.map +1 -1
- package/dist/actions/remix-workspace-template.js +4 -3
- package/dist/actions/remix-workspace-template.js.map +1 -1
- package/dist/actions/scaffold-workspace-app.js +3 -2
- package/dist/actions/scaffold-workspace-app.js.map +1 -1
- package/dist/actions/start-workspace-app-creation.js +3 -2
- package/dist/actions/start-workspace-app-creation.js.map +1 -1
- package/dist/actions/view-screen.js +11 -1
- package/dist/actions/view-screen.js.map +1 -1
- package/dist/components/app-icon.js +1 -1
- package/dist/components/app-icon.js.map +1 -1
- package/dist/components/connected-app-card.d.ts.map +1 -1
- package/dist/components/connected-app-card.js +2 -1
- package/dist/components/connected-app-card.js.map +1 -1
- package/dist/components/layout/Layout.d.ts.map +1 -1
- package/dist/components/layout/Layout.js +6 -7
- package/dist/components/layout/Layout.js.map +1 -1
- package/dist/components/workspace-template-card.d.ts.map +1 -1
- package/dist/components/workspace-template-card.js +8 -14
- package/dist/components/workspace-template-card.js.map +1 -1
- package/dist/hooks/use-navigation-state.d.ts +2 -0
- package/dist/hooks/use-navigation-state.d.ts.map +1 -1
- package/dist/hooks/use-navigation-state.js +10 -0
- package/dist/hooks/use-navigation-state.js.map +1 -1
- package/dist/lib/embed-session-recovery.d.ts +3 -0
- package/dist/lib/embed-session-recovery.d.ts.map +1 -0
- package/dist/lib/embed-session-recovery.js +18 -0
- package/dist/lib/embed-session-recovery.js.map +1 -0
- package/dist/lib/other-apps.d.ts +2 -0
- package/dist/lib/other-apps.d.ts.map +1 -1
- package/dist/lib/other-apps.js.map +1 -1
- package/dist/lib/workspace-apps.d.ts +4 -0
- package/dist/lib/workspace-apps.d.ts.map +1 -1
- package/dist/lib/workspace-apps.js +7 -0
- package/dist/lib/workspace-apps.js.map +1 -1
- package/dist/lib/workspace-apps.test.d.ts +2 -0
- package/dist/lib/workspace-apps.test.d.ts.map +1 -0
- package/dist/lib/workspace-apps.test.js +16 -0
- package/dist/lib/workspace-apps.test.js.map +1 -0
- package/dist/routes/pages/apps.$appId.d.ts.map +1 -1
- package/dist/routes/pages/apps.$appId.js +10 -11
- package/dist/routes/pages/apps.$appId.js.map +1 -1
- package/dist/routes/pages/chat.d.ts.map +1 -1
- package/dist/routes/pages/chat.js +7 -2
- package/dist/routes/pages/chat.js.map +1 -1
- package/dist/routes/pages/metrics.d.ts.map +1 -1
- package/dist/routes/pages/metrics.js +143 -17
- package/dist/routes/pages/metrics.js.map +1 -1
- package/dist/routes/pages/settings.d.ts.map +1 -1
- package/dist/routes/pages/settings.js +13 -1
- package/dist/routes/pages/settings.js.map +1 -1
- package/dist/server/lib/usage-metrics-store.d.ts +17 -0
- package/dist/server/lib/usage-metrics-store.d.ts.map +1 -1
- package/dist/server/lib/usage-metrics-store.js +187 -30
- package/dist/server/lib/usage-metrics-store.js.map +1 -1
- package/dist/server/plugins/agent-chat.js +2 -0
- package/dist/server/plugins/agent-chat.js.map +1 -1
- package/dist/server/plugins/integrations.d.ts.map +1 -1
- package/dist/server/plugins/integrations.js +1 -0
- package/dist/server/plugins/integrations.js.map +1 -1
- package/package.json +3 -3
- package/src/actions/list-connected-agents.spec.ts +73 -0
- package/src/actions/list-connected-agents.ts +6 -2
- package/src/actions/list-dispatch-usage-metrics.ts +11 -1
- package/src/actions/remix-workspace-template.spec.ts +29 -0
- package/src/actions/remix-workspace-template.ts +13 -6
- package/src/actions/scaffold-workspace-app.ts +12 -5
- package/src/actions/start-workspace-app-creation.ts +8 -2
- package/src/actions/view-screen.ts +13 -1
- package/src/components/app-icon.spec.tsx +34 -0
- package/src/components/app-icon.tsx +1 -1
- package/src/components/connected-app-card.tsx +3 -1
- package/src/components/dispatch-control-plane.spec.tsx +8 -1
- package/src/components/layout/Layout.spec.tsx +3 -0
- package/src/components/layout/Layout.tsx +13 -9
- package/src/components/workspace-template-card.spec.tsx +1 -1
- package/src/components/workspace-template-card.tsx +9 -16
- package/src/hooks/use-navigation-state.ts +12 -0
- package/src/lib/embed-session-recovery.spec.ts +63 -0
- package/src/lib/embed-session-recovery.ts +26 -0
- package/src/lib/other-apps.ts +2 -0
- package/src/lib/workspace-apps.test.ts +20 -0
- package/src/lib/workspace-apps.ts +10 -0
- package/src/routes/pages/apps.$appId.tsx +10 -11
- package/src/routes/pages/chat.spec.tsx +10 -0
- package/src/routes/pages/chat.tsx +8 -0
- package/src/routes/pages/metrics.tsx +452 -102
- package/src/routes/pages/settings.tsx +13 -1
- package/src/server/lib/usage-metrics-store.spec.ts +117 -0
- package/src/server/lib/usage-metrics-store.ts +249 -30
- package/src/server/plugins/agent-chat.spec.ts +4 -0
- package/src/server/plugins/agent-chat.ts +2 -0
- package/src/server/plugins/integrations.ts +1 -0
- package/src/styles/dispatch.css +9 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/dispatch",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
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": {
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
"tailwind-merge": "^3.5.0",
|
|
85
85
|
"vaul": "^1.1.2",
|
|
86
86
|
"zod": "^4.3.6",
|
|
87
|
-
"@agent-native/toolkit": "0.
|
|
87
|
+
"@agent-native/toolkit": "0.14.0"
|
|
88
88
|
},
|
|
89
89
|
"devDependencies": {
|
|
90
90
|
"@react-router/dev": "^8.1.0",
|
|
@@ -98,7 +98,7 @@
|
|
|
98
98
|
"typescript-7": "npm:typescript@^7.0.2",
|
|
99
99
|
"vite": "8.1.0",
|
|
100
100
|
"vitest": "^4.1.5",
|
|
101
|
-
"@agent-native/core": "0.
|
|
101
|
+
"@agent-native/core": "0.152.0"
|
|
102
102
|
},
|
|
103
103
|
"peerDependencies": {
|
|
104
104
|
"@agent-native/core": ">=0.8.0",
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const mocks = vi.hoisted(() => ({
|
|
4
|
+
discoverAgents: vi.fn(),
|
|
5
|
+
getBuiltinAgents: vi.fn(),
|
|
6
|
+
getRequestUserEmail: vi.fn(() => "owner@example.test"),
|
|
7
|
+
resourceGet: vi.fn(),
|
|
8
|
+
resourceListAccessible: vi.fn(async () => []),
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
vi.mock("@agent-native/core", () => ({
|
|
12
|
+
defineAction: (options: unknown) => options,
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
vi.mock("@agent-native/core/resources/metadata", () => ({
|
|
16
|
+
parseRemoteAgentManifest: vi.fn(),
|
|
17
|
+
REMOTE_AGENT_RESOURCE_PREFIXES: [],
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
vi.mock("@agent-native/core/resources/store", () => ({
|
|
21
|
+
resourceGet: (...args: unknown[]) => mocks.resourceGet(...args),
|
|
22
|
+
resourceListAccessible: (...args: unknown[]) =>
|
|
23
|
+
mocks.resourceListAccessible(...args),
|
|
24
|
+
SHARED_OWNER: "shared",
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
vi.mock("@agent-native/core/server", () => ({
|
|
28
|
+
getRequestUserEmail: () => mocks.getRequestUserEmail(),
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
vi.mock("@agent-native/core/server/agent-discovery", () => ({
|
|
32
|
+
discoverAgents: (...args: unknown[]) => mocks.discoverAgents(...args),
|
|
33
|
+
getBuiltinAgents: (...args: unknown[]) => mocks.getBuiltinAgents(...args),
|
|
34
|
+
normalizeAgentId: (id: string) => id.trim().toLowerCase(),
|
|
35
|
+
shouldIncludeRemoteAgentManifest: vi.fn(),
|
|
36
|
+
}));
|
|
37
|
+
|
|
38
|
+
vi.mock("../server/index.js", () => ({
|
|
39
|
+
getDispatchConfig: vi.fn(async () => ({ hiddenAgentIds: [] })),
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
describe("list-connected-agents", () => {
|
|
43
|
+
it("keeps a built-in app home URL separate from its A2A endpoint", async () => {
|
|
44
|
+
mocks.getBuiltinAgents.mockReturnValue([
|
|
45
|
+
{
|
|
46
|
+
id: "clips",
|
|
47
|
+
name: "Clips",
|
|
48
|
+
description: "Record and share",
|
|
49
|
+
url: "https://clips.agent-native.com",
|
|
50
|
+
color: "#000000",
|
|
51
|
+
},
|
|
52
|
+
]);
|
|
53
|
+
mocks.discoverAgents.mockResolvedValue([
|
|
54
|
+
{
|
|
55
|
+
id: "clips",
|
|
56
|
+
name: "Clips",
|
|
57
|
+
description: "Record and share",
|
|
58
|
+
url: "https://clips.agent-native.com/share/WrA8ZQ3oxa2T?ref=clip_share",
|
|
59
|
+
color: "#000000",
|
|
60
|
+
},
|
|
61
|
+
]);
|
|
62
|
+
|
|
63
|
+
const { default: action } = await import("./list-connected-agents.js");
|
|
64
|
+
const [clips] = await action.run({});
|
|
65
|
+
|
|
66
|
+
expect(clips).toMatchObject({
|
|
67
|
+
id: "clips",
|
|
68
|
+
url: "https://clips.agent-native.com/share/WrA8ZQ3oxa2T?ref=clip_share",
|
|
69
|
+
homeUrl: "https://clips.agent-native.com",
|
|
70
|
+
source: "builtin",
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
});
|
|
@@ -27,8 +27,10 @@ export default defineAction({
|
|
|
27
27
|
(m) => m.getDispatchConfig(),
|
|
28
28
|
);
|
|
29
29
|
const discovered = await discoverAgents("dispatch");
|
|
30
|
-
const
|
|
31
|
-
|
|
30
|
+
const builtins = getBuiltinAgents("dispatch");
|
|
31
|
+
const builtinIds = new Set(builtins.map((agent) => agent.id));
|
|
32
|
+
const builtinHomeUrls = new Map(
|
|
33
|
+
builtins.map((agent) => [agent.id, agent.url]),
|
|
32
34
|
);
|
|
33
35
|
const ownerEmail = getRequestUserEmail();
|
|
34
36
|
if (!ownerEmail) throw new Error("no authenticated user");
|
|
@@ -78,8 +80,10 @@ export default defineAction({
|
|
|
78
80
|
const connected = discovered.map((agent) => {
|
|
79
81
|
const custom = customById.get(agent.id);
|
|
80
82
|
const isBuiltin = builtinIds.has(agent.id);
|
|
83
|
+
const homeUrl = isBuiltin ? builtinHomeUrls.get(agent.id) : undefined;
|
|
81
84
|
return {
|
|
82
85
|
...agent,
|
|
86
|
+
...(homeUrl ? { homeUrl } : {}),
|
|
83
87
|
source: isBuiltin ? "builtin" : custom ? "custom" : "workspace",
|
|
84
88
|
resourceId: custom?.resourceId,
|
|
85
89
|
path: custom?.path,
|
|
@@ -5,7 +5,7 @@ import { listDispatchUsageMetrics } from "../server/lib/usage-metrics-store.js";
|
|
|
5
5
|
|
|
6
6
|
export default defineAction({
|
|
7
7
|
description:
|
|
8
|
-
"Get workspace
|
|
8
|
+
"Get personal or workspace LLM usage, spend or Builder.io credit spend, app attribution, prompt previews, and recent activity metrics for Dispatch.",
|
|
9
9
|
schema: z.object({
|
|
10
10
|
sinceDays: z.coerce
|
|
11
11
|
.number()
|
|
@@ -14,6 +14,16 @@ export default defineAction({
|
|
|
14
14
|
.max(365)
|
|
15
15
|
.default(30)
|
|
16
16
|
.describe("Lookback window in days. Defaults to 30."),
|
|
17
|
+
scope: z
|
|
18
|
+
.enum(["me", "workspace"])
|
|
19
|
+
.default("workspace")
|
|
20
|
+
.describe("Whether to inspect your account or the whole workspace."),
|
|
21
|
+
userEmail: z
|
|
22
|
+
.string()
|
|
23
|
+
.trim()
|
|
24
|
+
.min(1)
|
|
25
|
+
.optional()
|
|
26
|
+
.describe("Optional workspace member email to filter all usage to."),
|
|
17
27
|
}),
|
|
18
28
|
http: { method: "GET" },
|
|
19
29
|
run: async (args) => listDispatchUsageMetrics(args),
|
|
@@ -68,6 +68,35 @@ describe("remix-workspace-template action", () => {
|
|
|
68
68
|
);
|
|
69
69
|
});
|
|
70
70
|
|
|
71
|
+
it("normalizes a human-friendly app name before scaffolding", async () => {
|
|
72
|
+
vi.stubEnv("NODE_ENV", "test");
|
|
73
|
+
vi.stubEnv("NETLIFY", "");
|
|
74
|
+
mocks.scaffoldWorkspaceAppFromTemplate.mockResolvedValue({
|
|
75
|
+
appId: "customer-portal",
|
|
76
|
+
template: "mail",
|
|
77
|
+
output: "scaffolded",
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
await action.run({
|
|
81
|
+
templateId: "mail",
|
|
82
|
+
appId: "Customer Portal",
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
expect(mocks.scaffoldWorkspaceAppFromTemplate).toHaveBeenCalledWith({
|
|
86
|
+
template: "mail",
|
|
87
|
+
appId: "customer-portal",
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("rejects names that normalize to an empty app id", async () => {
|
|
92
|
+
await expect(
|
|
93
|
+
action.run({ templateId: "mail", appId: "---" }),
|
|
94
|
+
).rejects.toThrow(
|
|
95
|
+
"Use a non-reserved app id with lowercase letters, numbers, and hyphens.",
|
|
96
|
+
);
|
|
97
|
+
expect(mocks.scaffoldWorkspaceAppFromTemplate).not.toHaveBeenCalled();
|
|
98
|
+
});
|
|
99
|
+
|
|
71
100
|
it("starts a hosted private app without copying data or secrets", async () => {
|
|
72
101
|
vi.stubEnv("NODE_ENV", "production");
|
|
73
102
|
mocks.startWorkspaceAppCreation.mockResolvedValue({
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { defineAction } from "@agent-native/core";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
getWorkspaceAppIdValidationError,
|
|
4
|
+
normalizeWorkspaceAppId,
|
|
5
|
+
} from "@agent-native/core/shared";
|
|
3
6
|
import { z } from "zod";
|
|
4
7
|
|
|
5
8
|
import {
|
|
@@ -42,14 +45,18 @@ export default defineAction({
|
|
|
42
45
|
appId: z
|
|
43
46
|
.string()
|
|
44
47
|
.max(64)
|
|
48
|
+
.transform(normalizeWorkspaceAppId)
|
|
45
49
|
.optional()
|
|
46
50
|
.nullable()
|
|
47
|
-
.refine(
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
+
.refine(
|
|
52
|
+
(appId) => appId == null || !getWorkspaceAppIdValidationError(appId),
|
|
53
|
+
{
|
|
54
|
+
message:
|
|
55
|
+
"Use a non-reserved app id with lowercase letters, numbers, and hyphens.",
|
|
56
|
+
},
|
|
57
|
+
)
|
|
51
58
|
.describe(
|
|
52
|
-
"Optional target workspace app id. Defaults to <template>-remix so the source app remains independent.",
|
|
59
|
+
"Optional target workspace app id or human-friendly name. Names are converted to lowercase, hyphenated ids. Defaults to <template>-remix so the source app remains independent.",
|
|
53
60
|
),
|
|
54
61
|
description: z
|
|
55
62
|
.string()
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { defineAction } from "@agent-native/core";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
getWorkspaceAppIdValidationError,
|
|
4
|
+
normalizeWorkspaceAppId,
|
|
5
|
+
} from "@agent-native/core/shared";
|
|
3
6
|
import { z } from "zod";
|
|
4
7
|
|
|
5
8
|
import { scaffoldWorkspaceAppFromTemplate } from "../server/lib/app-creation-store.js";
|
|
@@ -27,12 +30,16 @@ export default defineAction({
|
|
|
27
30
|
appId: z
|
|
28
31
|
.string()
|
|
29
32
|
.max(64)
|
|
33
|
+
.transform(normalizeWorkspaceAppId)
|
|
30
34
|
.optional()
|
|
31
35
|
.nullable()
|
|
32
|
-
.refine(
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
.refine(
|
|
37
|
+
(appId) => appId == null || !getWorkspaceAppIdValidationError(appId),
|
|
38
|
+
{
|
|
39
|
+
message:
|
|
40
|
+
"Use a non-reserved app id with lowercase letters, numbers, and hyphens.",
|
|
41
|
+
},
|
|
42
|
+
)
|
|
36
43
|
.describe(
|
|
37
44
|
"Optional override for the apps/<id>/ directory name; defaults to the template name",
|
|
38
45
|
),
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { defineAction } from "@agent-native/core";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
getWorkspaceAppIdValidationError,
|
|
4
|
+
normalizeWorkspaceAppId,
|
|
5
|
+
} from "@agent-native/core/shared";
|
|
3
6
|
import { z } from "zod";
|
|
4
7
|
|
|
5
8
|
import { startWorkspaceAppCreation } from "../server/lib/app-creation-store.js";
|
|
@@ -12,13 +15,16 @@ export default defineAction({
|
|
|
12
15
|
appId: z
|
|
13
16
|
.string()
|
|
14
17
|
.max(64)
|
|
18
|
+
.transform(normalizeWorkspaceAppId)
|
|
15
19
|
.refine((appId) => !getWorkspaceAppIdValidationError(appId), {
|
|
16
20
|
message:
|
|
17
21
|
"Use a non-reserved app id with lowercase letters, numbers, and hyphens.",
|
|
18
22
|
})
|
|
19
23
|
.optional()
|
|
20
24
|
.nullable()
|
|
21
|
-
.describe(
|
|
25
|
+
.describe(
|
|
26
|
+
"Desired workspace app id/path or human-friendly name. Names are converted to lowercase, hyphenated ids.",
|
|
27
|
+
),
|
|
22
28
|
template: z
|
|
23
29
|
.string()
|
|
24
30
|
.optional()
|
|
@@ -155,9 +155,21 @@ export default defineAction({
|
|
|
155
155
|
}
|
|
156
156
|
if (navigation?.view === "metrics") {
|
|
157
157
|
try {
|
|
158
|
-
const
|
|
158
|
+
const usageScope =
|
|
159
|
+
navigation.usageScope === "workspace" ? "workspace" : "me";
|
|
160
|
+
const usageUserEmail =
|
|
161
|
+
typeof navigation.usageUserEmail === "string"
|
|
162
|
+
? navigation.usageUserEmail
|
|
163
|
+
: undefined;
|
|
164
|
+
const metrics = await listDispatchUsageMetrics({
|
|
165
|
+
sinceDays: 30,
|
|
166
|
+
scope: usageScope,
|
|
167
|
+
userEmail: usageUserEmail,
|
|
168
|
+
});
|
|
159
169
|
screen.usageMetrics = {
|
|
160
170
|
billing: metrics.billing,
|
|
171
|
+
viewScope: metrics.viewScope,
|
|
172
|
+
selectedUserEmail: metrics.selectedUserEmail,
|
|
161
173
|
totals: metrics.totals,
|
|
162
174
|
byApp: metrics.byApp.slice(0, 8),
|
|
163
175
|
byUser: metrics.byUser.slice(0, 8),
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import React, { act } from "react";
|
|
3
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
5
|
+
|
|
6
|
+
import { AppIcon } from "./app-icon";
|
|
7
|
+
|
|
8
|
+
describe("AppIcon", () => {
|
|
9
|
+
let container: HTMLDivElement;
|
|
10
|
+
let root: Root;
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
container = document.createElement("div");
|
|
14
|
+
document.body.appendChild(container);
|
|
15
|
+
root = createRoot(container);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
act(() => root.unmount());
|
|
20
|
+
container.remove();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("gives Plan a colored generated accent instead of a neutral gray", () => {
|
|
24
|
+
act(() => {
|
|
25
|
+
root.render(<AppIcon id="plan" name="Plan" />);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const icon = container.firstElementChild as HTMLElement;
|
|
29
|
+
expect(icon.style.getPropertyValue("--dispatch-app-icon-color-rgb")).toBe(
|
|
30
|
+
"47 111 237",
|
|
31
|
+
);
|
|
32
|
+
expect(icon.style.color).toBe("rgb(var(--dispatch-app-icon-color-rgb))");
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -67,7 +67,7 @@ const APP_VISUALS_BY_ID: Record<
|
|
|
67
67
|
forms: { icon: IconStack2, colorRgb: "6 182 212" },
|
|
68
68
|
macros: { icon: IconCode, colorRgb: "113 113 122" },
|
|
69
69
|
mail: { icon: IconMail, colorRgb: "59 130 246" },
|
|
70
|
-
plan: { icon: IconLayoutBoard, colorRgb: "
|
|
70
|
+
plan: { icon: IconLayoutBoard, colorRgb: "47 111 237" },
|
|
71
71
|
slides: { icon: IconPresentation, colorRgb: "236 72 153" },
|
|
72
72
|
tasks: { icon: IconListCheck, colorRgb: "99 102 241" },
|
|
73
73
|
};
|
|
@@ -12,6 +12,8 @@ export function ConnectedAppCard({
|
|
|
12
12
|
app: ConnectedAppSummary;
|
|
13
13
|
className?: string;
|
|
14
14
|
}) {
|
|
15
|
+
const launchUrl = app.homeUrl ?? app.url;
|
|
16
|
+
|
|
15
17
|
return (
|
|
16
18
|
<AppListRow className={className}>
|
|
17
19
|
<AppIcon id={app.id} name={app.name} color={app.color} size="sm" />
|
|
@@ -25,7 +27,7 @@ export function ConnectedAppCard({
|
|
|
25
27
|
</div>
|
|
26
28
|
<AppOpenActions
|
|
27
29
|
name={app.name}
|
|
28
|
-
href={withBuilderUtmTrackingParams(
|
|
30
|
+
href={withBuilderUtmTrackingParams(launchUrl, {
|
|
29
31
|
campaign: "product",
|
|
30
32
|
content: "dispatch_app",
|
|
31
33
|
})}
|
|
@@ -231,7 +231,9 @@ describe("DispatchControlPlane", () => {
|
|
|
231
231
|
id: "clips",
|
|
232
232
|
name: "Clips",
|
|
233
233
|
description: "Record and share",
|
|
234
|
-
url: "https://clips.agent-native.com",
|
|
234
|
+
url: "https://clips.agent-native.com/share/WrA8ZQ3oxa2T?ref=clip_share",
|
|
235
|
+
homeUrl: "https://clips.agent-native.com",
|
|
236
|
+
source: "builtin",
|
|
235
237
|
},
|
|
236
238
|
{
|
|
237
239
|
id: "onboarding",
|
|
@@ -282,5 +284,10 @@ describe("DispatchControlPlane", () => {
|
|
|
282
284
|
anchor.getAttribute("href")?.includes("onboarding"),
|
|
283
285
|
),
|
|
284
286
|
).toHaveLength(1);
|
|
287
|
+
const clipsHref = Array.from(container.querySelectorAll("a"))
|
|
288
|
+
.map((anchor) => anchor.getAttribute("href"))
|
|
289
|
+
.find((href) => href?.includes("clips.agent-native.com"));
|
|
290
|
+
expect(clipsHref).toContain("https://clips.agent-native.com");
|
|
291
|
+
expect(clipsHref).not.toContain("/share/");
|
|
285
292
|
});
|
|
286
293
|
});
|
|
@@ -302,6 +302,9 @@ describe("Dispatch NavContent", () => {
|
|
|
302
302
|
);
|
|
303
303
|
expect(sidebarLabel?.textContent?.trim()).toBe("Dispatch");
|
|
304
304
|
expect(container.textContent).not.toContain("Agent-Native Dispatch");
|
|
305
|
+
expect(
|
|
306
|
+
sidebarLabel?.closest('a[data-dispatch-logo][href="/overview"]'),
|
|
307
|
+
).not.toBeNull();
|
|
305
308
|
|
|
306
309
|
const settingsLink = container.querySelector('a[href="/settings"]');
|
|
307
310
|
const adminLink = container.querySelector('a[href="/admin"]');
|
|
@@ -102,6 +102,7 @@ import {
|
|
|
102
102
|
import { Link, useLocation, useNavigate } from "react-router";
|
|
103
103
|
import { toast } from "sonner";
|
|
104
104
|
|
|
105
|
+
import { isEmbedSessionExpiredMessage } from "../../lib/embed-session-recovery";
|
|
105
106
|
import { cn } from "../../lib/utils";
|
|
106
107
|
import {
|
|
107
108
|
isDispatchWorkspaceAppId,
|
|
@@ -1193,7 +1194,7 @@ export function NavContent({
|
|
|
1193
1194
|
<div
|
|
1194
1195
|
className={cn("py-2", collapsed ? "flex justify-center px-1" : "px-3")}
|
|
1195
1196
|
>
|
|
1196
|
-
<OrgSwitcher compact={collapsed} reserveSpace />
|
|
1197
|
+
<OrgSwitcher compact={collapsed} reserveSpace currentAppId="dispatch" />
|
|
1197
1198
|
</div>
|
|
1198
1199
|
);
|
|
1199
1200
|
const sidebarFooterActions = (
|
|
@@ -1213,9 +1214,12 @@ export function NavContent({
|
|
|
1213
1214
|
collapsed ? "justify-center px-0" : "px-4",
|
|
1214
1215
|
)}
|
|
1215
1216
|
>
|
|
1216
|
-
<
|
|
1217
|
+
<Link
|
|
1218
|
+
to={dispatchNavLinkTarget("/overview")}
|
|
1219
|
+
aria-label={`${DISPATCH_SIDEBAR_LABEL} overview`}
|
|
1220
|
+
data-dispatch-logo
|
|
1217
1221
|
className={cn(
|
|
1218
|
-
"flex items-center",
|
|
1222
|
+
"flex items-center rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sidebar-ring",
|
|
1219
1223
|
collapsed ? "justify-center" : "gap-2",
|
|
1220
1224
|
)}
|
|
1221
1225
|
>
|
|
@@ -1251,7 +1255,7 @@ export function NavContent({
|
|
|
1251
1255
|
</div>
|
|
1252
1256
|
</div>
|
|
1253
1257
|
)}
|
|
1254
|
-
</
|
|
1258
|
+
</Link>
|
|
1255
1259
|
</div>
|
|
1256
1260
|
|
|
1257
1261
|
{chatFirstMode ? (
|
|
@@ -1586,11 +1590,9 @@ export function Layout({
|
|
|
1586
1590
|
|
|
1587
1591
|
const handleEmbedSessionExpired = (event: MessageEvent) => {
|
|
1588
1592
|
if (
|
|
1589
|
-
event
|
|
1590
|
-
|
|
1591
|
-
) {
|
|
1593
|
+
!isEmbedSessionExpiredMessage(event, chatFirstAppFrameRef.current, null)
|
|
1594
|
+
)
|
|
1592
1595
|
return;
|
|
1593
|
-
}
|
|
1594
1596
|
setChatFirstEmbedAttempt((attempt) => attempt + 1);
|
|
1595
1597
|
};
|
|
1596
1598
|
|
|
@@ -2109,13 +2111,14 @@ export function Layout({
|
|
|
2109
2111
|
chatOnly
|
|
2110
2112
|
dynamicSuggestions={false}
|
|
2111
2113
|
suggestions={[]}
|
|
2114
|
+
suppressInlineOpenApp={chatFirstMode}
|
|
2112
2115
|
emptyStateDisplay="hidden"
|
|
2113
2116
|
composerPlaceholder={t(
|
|
2114
2117
|
"dispatch.pages.chatFirstSessionMessagePlaceholder",
|
|
2115
2118
|
)}
|
|
2116
2119
|
/>
|
|
2117
2120
|
),
|
|
2118
|
-
[t],
|
|
2121
|
+
[chatFirstMode, t],
|
|
2119
2122
|
);
|
|
2120
2123
|
|
|
2121
2124
|
const renderChatFirstSurfaceTab = useCallback(
|
|
@@ -2152,6 +2155,7 @@ export function Layout({
|
|
|
2152
2155
|
onRetry={() => setChatFirstEmbedAttempt((value) => value + 1)}
|
|
2153
2156
|
renderEmbed={({ url, title }: ChatFirstEmbedTarget) => (
|
|
2154
2157
|
<iframe
|
|
2158
|
+
key={`${url}:${chatFirstEmbedAttempt}`}
|
|
2155
2159
|
data-dispatch-chat-first-app-frame
|
|
2156
2160
|
src={url}
|
|
2157
2161
|
title={title ?? chatFirstCopy("appUnavailable")}
|
|
@@ -130,7 +130,7 @@ describe("WorkspaceTemplateCard", () => {
|
|
|
130
130
|
Object.getOwnPropertyDescriptor(
|
|
131
131
|
HTMLInputElement.prototype,
|
|
132
132
|
"value",
|
|
133
|
-
)?.set?.call(input, "
|
|
133
|
+
)?.set?.call(input, "Sales Ops");
|
|
134
134
|
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
135
135
|
});
|
|
136
136
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useActionMutation } from "@agent-native/core/client/hooks";
|
|
2
|
+
import { normalizeWorkspaceAppId } from "@agent-native/core/shared";
|
|
2
3
|
import {
|
|
3
4
|
IconArrowUpRight,
|
|
4
5
|
IconCircleCheck,
|
|
@@ -94,17 +95,6 @@ export interface WorkspaceTemplateCardProps {
|
|
|
94
95
|
) => void;
|
|
95
96
|
}
|
|
96
97
|
|
|
97
|
-
function slugifyAppId(value: string): string {
|
|
98
|
-
return (
|
|
99
|
-
value
|
|
100
|
-
.trim()
|
|
101
|
-
.toLowerCase()
|
|
102
|
-
.replace(/[^a-z0-9]+/g, "-")
|
|
103
|
-
.replace(/^-+|-+$/g, "")
|
|
104
|
-
.slice(0, 48) || "new-app"
|
|
105
|
-
);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
98
|
function templateIdFor(template: CuratedWorkspaceTemplate): string {
|
|
109
99
|
return template.templateId || template.id || template.appId || template.name;
|
|
110
100
|
}
|
|
@@ -113,8 +103,9 @@ function defaultAppIdFor(
|
|
|
113
103
|
template: CuratedWorkspaceTemplate,
|
|
114
104
|
defaultAppId?: string,
|
|
115
105
|
): string {
|
|
116
|
-
if (defaultAppId) return
|
|
117
|
-
const sourceId =
|
|
106
|
+
if (defaultAppId) return normalizeWorkspaceAppId(defaultAppId) || "new-app";
|
|
107
|
+
const sourceId =
|
|
108
|
+
normalizeWorkspaceAppId(template.appId || template.name) || "new-app";
|
|
118
109
|
return `${sourceId}-app`;
|
|
119
110
|
}
|
|
120
111
|
|
|
@@ -173,15 +164,16 @@ export function WorkspaceTemplateCard({
|
|
|
173
164
|
}, [defaultAppId, open, template]);
|
|
174
165
|
|
|
175
166
|
function submitRemix() {
|
|
176
|
-
const
|
|
177
|
-
if (!
|
|
167
|
+
const normalizedAppId = normalizeWorkspaceAppId(appId);
|
|
168
|
+
if (!normalizedAppId) {
|
|
178
169
|
toast.error(labels.appIdRequired);
|
|
179
170
|
return;
|
|
180
171
|
}
|
|
172
|
+
setAppId(normalizedAppId);
|
|
181
173
|
|
|
182
174
|
remix.mutate({
|
|
183
175
|
templateId: templateIdFor(template),
|
|
184
|
-
appId:
|
|
176
|
+
appId: normalizedAppId,
|
|
185
177
|
});
|
|
186
178
|
}
|
|
187
179
|
|
|
@@ -280,6 +272,7 @@ export function WorkspaceTemplateCard({
|
|
|
280
272
|
value={appId}
|
|
281
273
|
autoComplete="off"
|
|
282
274
|
onChange={(event) => setAppId(event.target.value)}
|
|
275
|
+
onBlur={() => setAppId(normalizeWorkspaceAppId(appId))}
|
|
283
276
|
disabled={remix.isPending}
|
|
284
277
|
/>
|
|
285
278
|
</div>
|
|
@@ -32,6 +32,8 @@ export interface NavigationState {
|
|
|
32
32
|
query?: string;
|
|
33
33
|
runId?: string;
|
|
34
34
|
threadId?: string;
|
|
35
|
+
usageScope?: "me" | "workspace";
|
|
36
|
+
usageUserEmail?: string;
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
export function useNavigationState(extensions?: DispatchExtensionConfig) {
|
|
@@ -177,6 +179,16 @@ export function buildDispatchNavigationState(
|
|
|
177
179
|
if (selectedThreadId) state.threadId = selectedThreadId;
|
|
178
180
|
}
|
|
179
181
|
|
|
182
|
+
if (state.view === "metrics") {
|
|
183
|
+
const params = new URLSearchParams(search);
|
|
184
|
+
const usageScope = params.get("scope");
|
|
185
|
+
const usageUserEmail = params.get("user");
|
|
186
|
+
if (usageScope === "me" || usageScope === "workspace") {
|
|
187
|
+
state.usageScope = usageScope;
|
|
188
|
+
}
|
|
189
|
+
if (usageUserEmail) state.usageUserEmail = usageUserEmail;
|
|
190
|
+
}
|
|
191
|
+
|
|
180
192
|
return state;
|
|
181
193
|
}
|
|
182
194
|
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
EMBED_SESSION_EXPIRED_MESSAGE,
|
|
5
|
+
isEmbedSessionExpiredMessage,
|
|
6
|
+
} from "./embed-session-recovery";
|
|
7
|
+
|
|
8
|
+
const frameWindow = {} as Window;
|
|
9
|
+
const frame = {
|
|
10
|
+
contentWindow: frameWindow,
|
|
11
|
+
src: "https://mail.example.test/_agent-native/embed/start?ticket=one",
|
|
12
|
+
} as HTMLIFrameElement;
|
|
13
|
+
|
|
14
|
+
describe("isEmbedSessionExpiredMessage", () => {
|
|
15
|
+
it("accepts a message from the active app frame", () => {
|
|
16
|
+
expect(
|
|
17
|
+
isEmbedSessionExpiredMessage(
|
|
18
|
+
{ data: { type: EMBED_SESSION_EXPIRED_MESSAGE }, source: frameWindow },
|
|
19
|
+
frame,
|
|
20
|
+
),
|
|
21
|
+
).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("rejects messages from another frame", () => {
|
|
25
|
+
expect(
|
|
26
|
+
isEmbedSessionExpiredMessage(
|
|
27
|
+
{ data: { type: EMBED_SESSION_EXPIRED_MESSAGE }, source: {} as Window },
|
|
28
|
+
frame,
|
|
29
|
+
),
|
|
30
|
+
).toBe(false);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("accepts a matching start URL when the browser omits event.source", () => {
|
|
34
|
+
expect(
|
|
35
|
+
isEmbedSessionExpiredMessage(
|
|
36
|
+
{
|
|
37
|
+
data: {
|
|
38
|
+
type: EMBED_SESSION_EXPIRED_MESSAGE,
|
|
39
|
+
embedStartUrl: frame.src,
|
|
40
|
+
},
|
|
41
|
+
source: null,
|
|
42
|
+
},
|
|
43
|
+
frame,
|
|
44
|
+
),
|
|
45
|
+
).toBe(true);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("can match the expected URL before the iframe ref is available", () => {
|
|
49
|
+
expect(
|
|
50
|
+
isEmbedSessionExpiredMessage(
|
|
51
|
+
{
|
|
52
|
+
data: {
|
|
53
|
+
type: EMBED_SESSION_EXPIRED_MESSAGE,
|
|
54
|
+
embedStartUrl: frame.src,
|
|
55
|
+
},
|
|
56
|
+
source: null,
|
|
57
|
+
},
|
|
58
|
+
null,
|
|
59
|
+
frame.src,
|
|
60
|
+
),
|
|
61
|
+
).toBe(true);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export const EMBED_SESSION_EXPIRED_MESSAGE = "agentNative.embedSessionExpired";
|
|
2
|
+
|
|
3
|
+
interface EmbedSessionExpiredMessage {
|
|
4
|
+
type?: unknown;
|
|
5
|
+
embedStartUrl?: unknown;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function isEmbedSessionExpiredMessage(
|
|
9
|
+
event: Pick<MessageEvent, "data" | "source">,
|
|
10
|
+
frame: Pick<HTMLIFrameElement, "contentWindow" | "src"> | null,
|
|
11
|
+
expectedStartUrl?: string | null,
|
|
12
|
+
): boolean {
|
|
13
|
+
const data =
|
|
14
|
+
event.data && typeof event.data === "object"
|
|
15
|
+
? (event.data as EmbedSessionExpiredMessage)
|
|
16
|
+
: null;
|
|
17
|
+
if (data?.type !== EMBED_SESSION_EXPIRED_MESSAGE) return false;
|
|
18
|
+
|
|
19
|
+
const frameWindow = frame?.contentWindow;
|
|
20
|
+
if (frameWindow && event.source === frameWindow) return true;
|
|
21
|
+
if (event.source !== null) return false;
|
|
22
|
+
if (typeof data.embedStartUrl !== "string") return false;
|
|
23
|
+
|
|
24
|
+
const frameUrl = frame?.src || expectedStartUrl;
|
|
25
|
+
return Boolean(frameUrl && data.embedStartUrl === frameUrl);
|
|
26
|
+
}
|