@agent-native/dispatch 0.40.30-nightly-20261021093329 → 0.40.31
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/CHANGELOG.md +15 -60
- package/dist/actions/connect-external-agent.d.ts.map +1 -1
- package/dist/actions/connect-external-agent.js +12 -13
- package/dist/actions/connect-external-agent.js.map +1 -1
- package/dist/actions/create-workspace-resource.d.ts +13 -13
- package/dist/actions/delete-destination.d.ts +12 -12
- package/dist/actions/delete-workspace-resource.d.ts +13 -13
- package/dist/actions/import-agent.d.ts +13 -13
- package/dist/actions/index.d.ts.map +1 -1
- package/dist/actions/index.js +8 -0
- package/dist/actions/index.js.map +1 -1
- package/dist/actions/inspect-connect-agent-card.d.ts +5 -0
- package/dist/actions/inspect-connect-agent-card.d.ts.map +1 -0
- package/dist/actions/inspect-connect-agent-card.js +15 -0
- package/dist/actions/inspect-connect-agent-card.js.map +1 -0
- package/dist/actions/install-curated-community-app.d.ts +36 -0
- package/dist/actions/install-curated-community-app.d.ts.map +1 -0
- package/dist/actions/install-curated-community-app.js +26 -0
- package/dist/actions/install-curated-community-app.js.map +1 -0
- package/dist/actions/list-connect-marketplace-apps.d.ts +3 -0
- package/dist/actions/list-connect-marketplace-apps.d.ts.map +1 -0
- package/dist/actions/list-connect-marketplace-apps.js +15 -0
- package/dist/actions/list-connect-marketplace-apps.js.map +1 -0
- package/dist/actions/list-connected-agents.d.ts +2 -0
- package/dist/actions/list-connected-agents.js +7 -3
- package/dist/actions/list-connected-agents.js.map +1 -1
- package/dist/actions/list-curated-community-apps.d.ts +3 -0
- package/dist/actions/list-curated-community-apps.d.ts.map +1 -0
- package/dist/actions/list-curated-community-apps.js +10 -0
- package/dist/actions/list-curated-community-apps.js.map +1 -0
- package/dist/actions/list-workspace-connections.d.ts.map +1 -1
- package/dist/actions/list-workspace-connections.js +13 -18
- package/dist/actions/list-workspace-connections.js.map +1 -1
- package/dist/actions/provider-api-register.d.ts +14 -14
- package/dist/actions/remix-workspace-template.d.ts +6 -0
- package/dist/actions/update-workspace-app-metadata.d.ts +1 -0
- package/dist/actions/update-workspace-app-metadata.js +26 -5
- package/dist/actions/update-workspace-app-metadata.js.map +1 -1
- package/dist/components/action-query-error.d.ts +2 -1
- package/dist/components/action-query-error.d.ts.map +1 -1
- package/dist/components/action-query-error.js +25 -2
- package/dist/components/action-query-error.js.map +1 -1
- package/dist/components/chat-home-apps.d.ts.map +1 -1
- package/dist/components/chat-home-apps.js +2 -2
- package/dist/components/chat-home-apps.js.map +1 -1
- package/dist/components/community-apps-section.d.ts +2 -0
- package/dist/components/community-apps-section.d.ts.map +1 -0
- package/dist/components/community-apps-section.js +90 -0
- package/dist/components/community-apps-section.js.map +1 -0
- package/dist/components/dispatch-control-plane.d.ts.map +1 -1
- package/dist/components/dispatch-control-plane.js +1 -1
- package/dist/components/dispatch-control-plane.js.map +1 -1
- package/dist/components/layout/Layout.d.ts +1 -0
- package/dist/components/layout/Layout.d.ts.map +1 -1
- package/dist/components/layout/Layout.js +7 -0
- 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 +63 -9
- package/dist/components/workspace-app-card.js.map +1 -1
- package/dist/lib/connect-apps.d.ts +1 -2
- package/dist/lib/connect-apps.d.ts.map +1 -1
- package/dist/lib/connect-apps.js +41 -65
- package/dist/lib/connect-apps.js.map +1 -1
- package/dist/lib/workspace-apps.d.ts +1 -0
- package/dist/lib/workspace-apps.d.ts.map +1 -1
- package/dist/lib/workspace-apps.js.map +1 -1
- package/dist/routes/pages/$appId.d.ts.map +1 -1
- package/dist/routes/pages/$appId.js +1 -1
- package/dist/routes/pages/$appId.js.map +1 -1
- package/dist/routes/pages/apps.d.ts.map +1 -1
- package/dist/routes/pages/apps.js +3 -2
- package/dist/routes/pages/apps.js.map +1 -1
- package/dist/routes/pages/connect.d.ts.map +1 -1
- package/dist/routes/pages/connect.js +5 -7
- package/dist/routes/pages/connect.js.map +1 -1
- package/dist/server/index.d.ts +2 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +2 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/lib/app-creation-store.d.ts +11 -1
- package/dist/server/lib/app-creation-store.d.ts.map +1 -1
- package/dist/server/lib/app-creation-store.js +379 -92
- package/dist/server/lib/app-creation-store.js.map +1 -1
- package/dist/server/lib/community-apps.d.ts +45 -0
- package/dist/server/lib/community-apps.d.ts.map +1 -0
- package/dist/server/lib/community-apps.js +267 -0
- package/dist/server/lib/community-apps.js.map +1 -0
- package/dist/server/lib/community-source.d.ts +19 -0
- package/dist/server/lib/community-source.d.ts.map +1 -0
- package/dist/server/lib/community-source.js +118 -0
- package/dist/server/lib/community-source.js.map +1 -0
- package/dist/server/lib/connect-apps.d.ts +4 -0
- package/dist/server/lib/connect-apps.d.ts.map +1 -0
- package/dist/server/lib/connect-apps.js +99 -0
- package/dist/server/lib/connect-apps.js.map +1 -0
- package/dist/server/lib/org-apps-directory-cache.d.ts +20 -0
- package/dist/server/lib/org-apps-directory-cache.d.ts.map +1 -0
- package/dist/server/lib/org-apps-directory-cache.js +44 -0
- package/dist/server/lib/org-apps-directory-cache.js.map +1 -0
- package/dist/server/plugins/integrations.js +3 -3
- package/dist/server/plugins/integrations.js.map +1 -1
- package/dist/shared/labs.d.ts +2 -0
- package/dist/shared/labs.d.ts.map +1 -0
- package/dist/shared/labs.js +8 -0
- package/dist/shared/labs.js.map +1 -0
- package/package.json +5 -4
- package/src/actions/connect-external-agent.spec.ts +101 -3
- package/src/actions/connect-external-agent.ts +12 -11
- package/src/actions/index.spec.ts +13 -0
- package/src/actions/index.ts +8 -0
- package/src/actions/inspect-connect-agent-card.ts +17 -0
- package/src/actions/install-curated-community-app.ts +32 -0
- package/src/actions/list-connect-marketplace-apps.ts +17 -0
- package/src/actions/list-connected-agents.spec.ts +17 -1
- package/src/actions/list-connected-agents.ts +7 -2
- package/src/actions/list-curated-community-apps.ts +12 -0
- package/src/actions/list-workspace-connections.spec.ts +14 -3
- package/src/actions/list-workspace-connections.ts +13 -17
- package/src/actions/update-workspace-app-metadata.spec.ts +25 -0
- package/src/actions/update-workspace-app-metadata.ts +48 -24
- package/src/components/action-query-error.spec.tsx +20 -0
- package/src/components/action-query-error.tsx +40 -0
- package/src/components/chat-home-apps.spec.tsx +32 -1
- package/src/components/chat-home-apps.tsx +6 -1
- package/src/components/community-apps-section.tsx +224 -0
- package/src/components/dispatch-control-plane.tsx +1 -0
- package/src/components/layout/Layout.tsx +9 -0
- package/src/components/workspace-app-card.spec.tsx +226 -1
- package/src/components/workspace-app-card.tsx +115 -12
- package/src/lib/connect-apps.spec.ts +118 -66
- package/src/lib/connect-apps.ts +47 -82
- package/src/lib/workspace-apps.ts +1 -0
- package/src/routes/pages/$appId.tsx +1 -0
- package/src/routes/pages/apps.spec.tsx +227 -3
- package/src/routes/pages/apps.tsx +5 -0
- package/src/routes/pages/connect.spec.tsx +53 -6
- package/src/routes/pages/connect.tsx +14 -11
- package/src/server/index.ts +9 -0
- package/src/server/lib/app-creation-store.spec.ts +820 -16
- package/src/server/lib/app-creation-store.ts +504 -101
- package/src/server/lib/community-apps.spec.ts +367 -0
- package/src/server/lib/community-apps.ts +372 -0
- package/src/server/lib/community-source.spec.ts +151 -0
- package/src/server/lib/community-source.ts +186 -0
- package/src/server/lib/connect-apps.ts +149 -0
- package/src/server/lib/org-apps-directory-cache.spec.ts +71 -0
- package/src/server/lib/org-apps-directory-cache.ts +66 -0
- package/src/server/plugins/integrations.spec.ts +60 -0
- package/src/server/plugins/integrations.ts +3 -3
- package/src/shared/labs.ts +9 -0
|
@@ -10,6 +10,9 @@ const mocks = vi.hoisted(() => ({
|
|
|
10
10
|
hasWorkspaceProviderOAuthCredentials: vi.fn(),
|
|
11
11
|
isProviderApiId: vi.fn(() => false),
|
|
12
12
|
listProviderApiCatalog: vi.fn(() => []),
|
|
13
|
+
listWorkspaceApps: vi.fn(async () => [
|
|
14
|
+
{ id: "dispatch", name: "Dispatch", status: "ready" },
|
|
15
|
+
]),
|
|
13
16
|
listWorkspaceConnectionGrants: vi.fn(),
|
|
14
17
|
listWorkspaceConnectionProviders: vi.fn(),
|
|
15
18
|
listWorkspaceConnectionsForUser: vi.fn(),
|
|
@@ -54,9 +57,7 @@ vi.mock("@agent-native/core/workspace-connections", () => ({
|
|
|
54
57
|
vi.mock("@agent-native/dispatch/actions", () => ({
|
|
55
58
|
dispatchActions: {
|
|
56
59
|
"list-workspace-apps": {
|
|
57
|
-
run:
|
|
58
|
-
{ id: "dispatch", name: "Dispatch", status: "ready" },
|
|
59
|
-
]),
|
|
60
|
+
run: mocks.listWorkspaceApps,
|
|
60
61
|
},
|
|
61
62
|
},
|
|
62
63
|
}));
|
|
@@ -123,4 +124,14 @@ describe("list-workspace-connections", () => {
|
|
|
123
124
|
expect(result.providers.map((provider) => provider.id)).toEqual(["github"]);
|
|
124
125
|
expect(result.counts.providers).toBe(1);
|
|
125
126
|
});
|
|
127
|
+
|
|
128
|
+
it("preserves workspace app lookup failures instead of returning partial grants", async () => {
|
|
129
|
+
const error = Object.assign(new Error("Workspace registry unavailable"), {
|
|
130
|
+
statusCode: 503,
|
|
131
|
+
errorCode: "workspace_gateway_org_a2a_secret_unavailable",
|
|
132
|
+
});
|
|
133
|
+
mocks.listWorkspaceApps.mockRejectedValueOnce(error);
|
|
134
|
+
|
|
135
|
+
await expect(action.run({})).rejects.toBe(error);
|
|
136
|
+
});
|
|
126
137
|
});
|
|
@@ -79,23 +79,19 @@ async function listGrantApps(): Promise<GrantApp[]> {
|
|
|
79
79
|
const listWorkspaceApps = dispatchActions["list-workspace-apps"];
|
|
80
80
|
if (!listWorkspaceApps) return [{ id: "dispatch", label: "Dispatch" }];
|
|
81
81
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
.
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
: [{ id: "dispatch", label: "Dispatch" }];
|
|
96
|
-
} catch {
|
|
97
|
-
return [{ id: "dispatch", label: "Dispatch" }];
|
|
98
|
-
}
|
|
82
|
+
const apps = (await listWorkspaceApps.run({
|
|
83
|
+
includeAgentCards: false,
|
|
84
|
+
audience: "all",
|
|
85
|
+
} as any)) as WorkspaceApp[];
|
|
86
|
+
const grantApps = apps
|
|
87
|
+
.filter((app) => !app.archived && app.status !== "pending")
|
|
88
|
+
.map((app) => ({
|
|
89
|
+
id: app.id,
|
|
90
|
+
label: app.name || humanizeAppId(app.id),
|
|
91
|
+
}));
|
|
92
|
+
return grantApps.length > 0
|
|
93
|
+
? grantApps
|
|
94
|
+
: [{ id: "dispatch", label: "Dispatch" }];
|
|
99
95
|
}
|
|
100
96
|
|
|
101
97
|
export default defineAction({
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import action from "./update-workspace-app-metadata.js";
|
|
4
|
+
|
|
5
|
+
describe("update-workspace-app-metadata action schema", () => {
|
|
6
|
+
it("validates when-to-use examples when description is explicitly cleared", () => {
|
|
7
|
+
expect(
|
|
8
|
+
action.schema.safeParse({
|
|
9
|
+
appId: "todo",
|
|
10
|
+
description: null,
|
|
11
|
+
whenToUse: ["Create a task from a meeting note"],
|
|
12
|
+
}).success,
|
|
13
|
+
).toBe(true);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it("validates description when when-to-use examples are explicitly cleared", () => {
|
|
17
|
+
expect(
|
|
18
|
+
action.schema.safeParse({
|
|
19
|
+
appId: "todo",
|
|
20
|
+
description: "Track team tasks.",
|
|
21
|
+
whenToUse: null,
|
|
22
|
+
}).success,
|
|
23
|
+
).toBe(true);
|
|
24
|
+
});
|
|
25
|
+
});
|
|
@@ -1,33 +1,57 @@
|
|
|
1
1
|
import { defineAction } from "@agent-native/core/action";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
getWorkspaceAppIdValidationError,
|
|
4
|
+
parseAgentProfileMetadata,
|
|
5
|
+
} from "@agent-native/core/shared";
|
|
3
6
|
import { z } from "zod";
|
|
4
7
|
|
|
5
8
|
import { updateWorkspaceAppMetadata } from "../server/lib/app-creation-store.js";
|
|
6
9
|
|
|
7
10
|
export default defineAction({
|
|
8
11
|
description:
|
|
9
|
-
"Update
|
|
10
|
-
schema: z
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
12
|
+
"Update a workspace app's display name, description, and when-to-use profile. Only organization owners and admins can edit these shared details.",
|
|
13
|
+
schema: z
|
|
14
|
+
.object({
|
|
15
|
+
appId: z
|
|
16
|
+
.string()
|
|
17
|
+
.max(64)
|
|
18
|
+
.refine((appId) => !getWorkspaceAppIdValidationError(appId), {
|
|
19
|
+
message:
|
|
20
|
+
"Use a non-reserved app id with lowercase letters, numbers, and hyphens.",
|
|
21
|
+
})
|
|
22
|
+
.describe("Workspace app id, matching the apps/<id> folder"),
|
|
23
|
+
name: z
|
|
24
|
+
.string()
|
|
25
|
+
.max(120)
|
|
26
|
+
.optional()
|
|
27
|
+
.nullable()
|
|
28
|
+
.describe("Human-readable app name"),
|
|
29
|
+
description: z
|
|
30
|
+
.string()
|
|
31
|
+
.max(1000)
|
|
32
|
+
.optional()
|
|
33
|
+
.nullable()
|
|
34
|
+
.describe("Human-readable app description, up to 1,000 characters"),
|
|
35
|
+
whenToUse: z
|
|
36
|
+
.array(z.string())
|
|
37
|
+
.max(6)
|
|
38
|
+
.optional()
|
|
39
|
+
.nullable()
|
|
40
|
+
.describe("Example requests that indicate when this app is useful"),
|
|
41
|
+
})
|
|
42
|
+
.superRefine(({ description, whenToUse }, context) => {
|
|
43
|
+
if (description == null && whenToUse == null) return;
|
|
44
|
+
const profile = parseAgentProfileMetadata({
|
|
45
|
+
...(description != null ? { description } : {}),
|
|
46
|
+
...(whenToUse != null ? { whenToUse } : {}),
|
|
47
|
+
});
|
|
48
|
+
if (!profile) {
|
|
49
|
+
context.addIssue({
|
|
50
|
+
code: "custom",
|
|
51
|
+
message:
|
|
52
|
+
"Use a description up to 1,000 characters and up to 6 non-empty examples, each no longer than 140 characters.",
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}),
|
|
32
56
|
run: async (args) => updateWorkspaceAppMetadata(args),
|
|
33
57
|
});
|
|
@@ -53,4 +53,24 @@ describe("ActionQueryError", () => {
|
|
|
53
53
|
});
|
|
54
54
|
expect(onRetry).toHaveBeenCalledOnce();
|
|
55
55
|
});
|
|
56
|
+
|
|
57
|
+
it("shows the query and safe HTTP diagnostic without exposing the message", () => {
|
|
58
|
+
act(() => {
|
|
59
|
+
root.render(
|
|
60
|
+
<ActionQueryError
|
|
61
|
+
error={Object.assign(new Error("secret database detail"), {
|
|
62
|
+
status: 401,
|
|
63
|
+
errorCode: "workspace_gateway_rejected",
|
|
64
|
+
})}
|
|
65
|
+
queryName="list-workspace-apps"
|
|
66
|
+
onRetry={() => {}}
|
|
67
|
+
/>,
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
expect(container.textContent).toContain(
|
|
72
|
+
"list-workspace-apps · HTTP 401 · workspace_gateway_rejected",
|
|
73
|
+
);
|
|
74
|
+
expect(container.textContent).not.toContain("secret database detail");
|
|
75
|
+
});
|
|
56
76
|
});
|
|
@@ -8,13 +8,48 @@ interface ActionQueryErrorProps {
|
|
|
8
8
|
error?: unknown;
|
|
9
9
|
onRetry: () => unknown;
|
|
10
10
|
className?: string;
|
|
11
|
+
queryName?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function actionQueryDiagnostic(
|
|
15
|
+
error: unknown,
|
|
16
|
+
queryName: string | undefined,
|
|
17
|
+
): string | null {
|
|
18
|
+
const parts: string[] = [];
|
|
19
|
+
if (queryName) parts.push(queryName);
|
|
20
|
+
|
|
21
|
+
if (error && typeof error === "object") {
|
|
22
|
+
const details = error as Record<string, unknown>;
|
|
23
|
+
const status =
|
|
24
|
+
typeof details.statusCode === "number"
|
|
25
|
+
? details.statusCode
|
|
26
|
+
: details.status;
|
|
27
|
+
if (
|
|
28
|
+
typeof status === "number" &&
|
|
29
|
+
Number.isInteger(status) &&
|
|
30
|
+
status >= 100 &&
|
|
31
|
+
status <= 599
|
|
32
|
+
) {
|
|
33
|
+
parts.push(`HTTP ${status}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const code = details.errorCode ?? details.code;
|
|
37
|
+
if (typeof code === "string" && /^[a-zA-Z0-9_.-]{1,80}$/.test(code)) {
|
|
38
|
+
parts.push(code);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return parts.length > 0 ? parts.join(" · ") : null;
|
|
11
43
|
}
|
|
12
44
|
|
|
13
45
|
export function ActionQueryError({
|
|
46
|
+
error,
|
|
14
47
|
onRetry,
|
|
15
48
|
className,
|
|
49
|
+
queryName,
|
|
16
50
|
}: ActionQueryErrorProps) {
|
|
17
51
|
const t = useT();
|
|
52
|
+
const diagnostic = actionQueryDiagnostic(error, queryName);
|
|
18
53
|
|
|
19
54
|
return (
|
|
20
55
|
<Alert variant="destructive" className={className}>
|
|
@@ -22,6 +57,11 @@ export function ActionQueryError({
|
|
|
22
57
|
<AlertTitle>{t("dispatch.pages.dataLoadFailed")}</AlertTitle>
|
|
23
58
|
<AlertDescription className="flex flex-wrap items-center justify-between gap-3">
|
|
24
59
|
<span>{t("dispatch.pages.dataLoadFailedDescription")}</span>
|
|
60
|
+
{diagnostic ? (
|
|
61
|
+
<code className="w-full break-all text-xs text-muted-foreground">
|
|
62
|
+
{diagnostic}
|
|
63
|
+
</code>
|
|
64
|
+
) : null}
|
|
25
65
|
<Button type="button" variant="outline" size="sm" onClick={onRetry}>
|
|
26
66
|
<IconRefresh className="size-4" />
|
|
27
67
|
{t("dispatch.pages.tryAgain")}
|
|
@@ -25,7 +25,11 @@ const launcherState = vi.hoisted(() => ({
|
|
|
25
25
|
},
|
|
26
26
|
],
|
|
27
27
|
isLoading: false,
|
|
28
|
-
error: undefined,
|
|
28
|
+
error: undefined as unknown,
|
|
29
|
+
errorQueryName: undefined as
|
|
30
|
+
| "list-workspace-apps"
|
|
31
|
+
| "list-connected-agents"
|
|
32
|
+
| undefined,
|
|
29
33
|
openApp: vi.fn(),
|
|
30
34
|
retry: vi.fn(),
|
|
31
35
|
},
|
|
@@ -89,6 +93,8 @@ describe("DispatchChatHomeApps", () => {
|
|
|
89
93
|
];
|
|
90
94
|
launcherState.value.openApp.mockReset();
|
|
91
95
|
launcherState.value.retry.mockReset();
|
|
96
|
+
launcherState.value.error = undefined;
|
|
97
|
+
launcherState.value.errorQueryName = undefined;
|
|
92
98
|
container = document.createElement("div");
|
|
93
99
|
document.body.appendChild(container);
|
|
94
100
|
root = createRoot(container);
|
|
@@ -194,4 +200,29 @@ describe("DispatchChatHomeApps", () => {
|
|
|
194
200
|
expect(container.textContent).toContain("Localized Calendar description");
|
|
195
201
|
expect(container.textContent).not.toContain("Default English text");
|
|
196
202
|
});
|
|
203
|
+
|
|
204
|
+
it.each([
|
|
205
|
+
["list-workspace-apps", "Workspace apps failed"],
|
|
206
|
+
["list-connected-agents", "Connected agents failed"],
|
|
207
|
+
] as const)("identifies the failed %s query", async (queryName, message) => {
|
|
208
|
+
launcherState.value.workspaceApps = [];
|
|
209
|
+
launcherState.value.error = Object.assign(new Error(message), {
|
|
210
|
+
statusCode: 503,
|
|
211
|
+
errorCode: "workspace_gateway_unavailable",
|
|
212
|
+
});
|
|
213
|
+
launcherState.value.errorQueryName = queryName;
|
|
214
|
+
|
|
215
|
+
await act(async () => {
|
|
216
|
+
root.render(
|
|
217
|
+
<MemoryRouter initialEntries={["/chat"]}>
|
|
218
|
+
<DispatchChatHomeApps />
|
|
219
|
+
</MemoryRouter>,
|
|
220
|
+
);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
expect(container.textContent).toContain(
|
|
224
|
+
`${queryName} · HTTP 503 · workspace_gateway_unavailable`,
|
|
225
|
+
);
|
|
226
|
+
expect(container.textContent).not.toContain(message);
|
|
227
|
+
});
|
|
197
228
|
});
|
|
@@ -48,7 +48,11 @@ export function DispatchChatHomeApps() {
|
|
|
48
48
|
if (launcher.error && launcher.workspaceApps.length === 0) {
|
|
49
49
|
return (
|
|
50
50
|
<div className="mx-auto w-full max-w-[1000px]">
|
|
51
|
-
<ActionQueryError
|
|
51
|
+
<ActionQueryError
|
|
52
|
+
error={launcher.error}
|
|
53
|
+
onRetry={launcher.retry}
|
|
54
|
+
queryName={launcher.errorQueryName}
|
|
55
|
+
/>
|
|
52
56
|
</div>
|
|
53
57
|
);
|
|
54
58
|
}
|
|
@@ -61,6 +65,7 @@ export function DispatchChatHomeApps() {
|
|
|
61
65
|
error={launcher.error}
|
|
62
66
|
onRetry={launcher.retry}
|
|
63
67
|
className="mb-3"
|
|
68
|
+
queryName={launcher.errorQueryName}
|
|
64
69
|
/>
|
|
65
70
|
) : null}
|
|
66
71
|
<section aria-label={t("dispatch.nav.apps")}>
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { sendToAgentChat } from "@agent-native/core/client/agent-chat";
|
|
2
|
+
import {
|
|
3
|
+
useActionMutation,
|
|
4
|
+
useActionQuery,
|
|
5
|
+
} from "@agent-native/core/client/hooks";
|
|
6
|
+
import { useT } from "@agent-native/core/client/i18n";
|
|
7
|
+
import { useLab } from "@agent-native/core/client/labs";
|
|
8
|
+
import {
|
|
9
|
+
IconArrowUpRight,
|
|
10
|
+
IconCircleCheck,
|
|
11
|
+
IconPlus,
|
|
12
|
+
} from "@tabler/icons-react";
|
|
13
|
+
import { useState } from "react";
|
|
14
|
+
import { toast } from "sonner";
|
|
15
|
+
|
|
16
|
+
import { COMMUNITY_APPS_LAB } from "../shared/labs.js";
|
|
17
|
+
import { ActionQueryError } from "./action-query-error";
|
|
18
|
+
import { AppIcon } from "./app-icon";
|
|
19
|
+
import { AppList, AppListRow } from "./app-list-row";
|
|
20
|
+
import { Badge } from "./ui/badge";
|
|
21
|
+
import { Button } from "./ui/button";
|
|
22
|
+
import { Skeleton } from "./ui/skeleton";
|
|
23
|
+
import { Spinner } from "./ui/spinner";
|
|
24
|
+
|
|
25
|
+
interface CommunityAppSource {
|
|
26
|
+
id: string;
|
|
27
|
+
name: string;
|
|
28
|
+
description?: string;
|
|
29
|
+
appId: string;
|
|
30
|
+
repo: string;
|
|
31
|
+
ref: string;
|
|
32
|
+
license: string;
|
|
33
|
+
provenance: string;
|
|
34
|
+
templatePath: string;
|
|
35
|
+
installed: boolean;
|
|
36
|
+
pending: boolean;
|
|
37
|
+
installable: boolean;
|
|
38
|
+
installUnavailableReason?:
|
|
39
|
+
| "workspace-required"
|
|
40
|
+
| "app-id-conflict"
|
|
41
|
+
| "builder-install-unverified";
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function pinnedSourceUrl(source: CommunityAppSource): string {
|
|
45
|
+
return (
|
|
46
|
+
"https://github.com/" +
|
|
47
|
+
source.repo +
|
|
48
|
+
"/tree/" +
|
|
49
|
+
source.ref +
|
|
50
|
+
"/" +
|
|
51
|
+
source.templatePath
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function CommunityAppsSection() {
|
|
56
|
+
const t = useT();
|
|
57
|
+
const enabled = useLab(COMMUNITY_APPS_LAB);
|
|
58
|
+
const [installingId, setInstallingId] = useState<string | null>(null);
|
|
59
|
+
const catalogQuery = useActionQuery<CommunityAppSource[]>(
|
|
60
|
+
"list-curated-community-apps" as never,
|
|
61
|
+
{},
|
|
62
|
+
{ enabled },
|
|
63
|
+
);
|
|
64
|
+
const workspaceAppsQuery = useActionQuery("list-workspace-apps", {
|
|
65
|
+
includeAgentCards: false,
|
|
66
|
+
includeArchived: true,
|
|
67
|
+
});
|
|
68
|
+
const install = useActionMutation("install-curated-community-app", {
|
|
69
|
+
onSuccess: (result) => {
|
|
70
|
+
const mode = (result as { mode?: string } | null)?.mode;
|
|
71
|
+
const message = (result as { message?: string } | null)?.message;
|
|
72
|
+
if (mode === "builder") {
|
|
73
|
+
toast.success(t("dispatch.pages.communityAppsInstallStarted"));
|
|
74
|
+
} else if (mode === "pending") {
|
|
75
|
+
toast.info(t("dispatch.pages.communityAppsPending"));
|
|
76
|
+
} else if (mode === "already-installed") {
|
|
77
|
+
toast.success(t("dispatch.pages.communityAppsAlreadyInstalled"));
|
|
78
|
+
} else if (mode === "builder-unavailable" || mode === "app-id-taken") {
|
|
79
|
+
toast.error(message || t("dispatch.pages.communityAppsInstallFailed"));
|
|
80
|
+
} else if (mode === "local-agent") {
|
|
81
|
+
sendToAgentChat({
|
|
82
|
+
message:
|
|
83
|
+
(result as { prompt?: string } | null)?.prompt ||
|
|
84
|
+
t("dispatch.pages.communityAppsLocalRequestReady"),
|
|
85
|
+
attachments: (
|
|
86
|
+
result as {
|
|
87
|
+
attachments?: Array<{
|
|
88
|
+
type: "file";
|
|
89
|
+
name: string;
|
|
90
|
+
contentType: string;
|
|
91
|
+
text: string;
|
|
92
|
+
}>;
|
|
93
|
+
} | null
|
|
94
|
+
)?.attachments,
|
|
95
|
+
submit: true,
|
|
96
|
+
type: "code",
|
|
97
|
+
newTab: true,
|
|
98
|
+
reuseEmptyTab: true,
|
|
99
|
+
});
|
|
100
|
+
toast.info(t("dispatch.pages.communityAppsLocalRequestReady"));
|
|
101
|
+
} else {
|
|
102
|
+
toast.error(t("dispatch.pages.communityAppsInstallFailed"));
|
|
103
|
+
}
|
|
104
|
+
setInstallingId(null);
|
|
105
|
+
void catalogQuery.refetch();
|
|
106
|
+
void workspaceAppsQuery.refetch();
|
|
107
|
+
},
|
|
108
|
+
onError: () => {
|
|
109
|
+
setInstallingId(null);
|
|
110
|
+
toast.error(t("dispatch.pages.communityAppsInstallFailed"));
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
if (!enabled) return null;
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<section className="space-y-3 border-t pt-6">
|
|
118
|
+
<h2 className="text-sm font-semibold text-foreground">
|
|
119
|
+
{t("dispatch.pages.communityApps")}
|
|
120
|
+
</h2>
|
|
121
|
+
{catalogQuery.isError ? (
|
|
122
|
+
<ActionQueryError
|
|
123
|
+
error={catalogQuery.error}
|
|
124
|
+
onRetry={() => void catalogQuery.refetch()}
|
|
125
|
+
/>
|
|
126
|
+
) : catalogQuery.isLoading ? (
|
|
127
|
+
<AppList className="divide-y rounded-xl border">
|
|
128
|
+
<div className="flex items-center gap-3 px-4 py-3.5">
|
|
129
|
+
<Skeleton className="size-8 rounded-lg" />
|
|
130
|
+
<Skeleton className="h-4 w-40" />
|
|
131
|
+
</div>
|
|
132
|
+
</AppList>
|
|
133
|
+
) : catalogQuery.data?.length ? (
|
|
134
|
+
<AppList className="divide-y rounded-xl border">
|
|
135
|
+
{catalogQuery.data.map((source) => (
|
|
136
|
+
<AppListRow key={source.id} className="border-b-0">
|
|
137
|
+
<AppIcon id={source.id} name={source.name} size="sm" />
|
|
138
|
+
<div className="min-w-0 flex-1">
|
|
139
|
+
<div className="flex min-w-0 flex-wrap items-center gap-2">
|
|
140
|
+
<h3 className="truncate text-sm font-semibold text-foreground">
|
|
141
|
+
{source.name}
|
|
142
|
+
</h3>
|
|
143
|
+
{source.installed ? (
|
|
144
|
+
<Badge variant="secondary" className="gap-1">
|
|
145
|
+
<IconCircleCheck size={13} />
|
|
146
|
+
{t("dispatch.pages.communityAppsInstalled")}
|
|
147
|
+
</Badge>
|
|
148
|
+
) : source.pending ? (
|
|
149
|
+
<Badge variant="secondary">
|
|
150
|
+
{t("dispatch.pages.communityAppsPending")}
|
|
151
|
+
</Badge>
|
|
152
|
+
) : null}
|
|
153
|
+
</div>
|
|
154
|
+
{source.description ? (
|
|
155
|
+
<p className="mt-0.5 truncate text-xs text-muted-foreground">
|
|
156
|
+
{source.description}
|
|
157
|
+
</p>
|
|
158
|
+
) : null}
|
|
159
|
+
<div className="mt-1 flex flex-wrap items-center gap-x-2 text-xs text-muted-foreground">
|
|
160
|
+
<span>
|
|
161
|
+
{t("dispatch.pages.communityAppsLicense")}: {source.license}
|
|
162
|
+
</span>
|
|
163
|
+
<a
|
|
164
|
+
href={pinnedSourceUrl(source)}
|
|
165
|
+
target="_blank"
|
|
166
|
+
rel="noreferrer"
|
|
167
|
+
className="inline-flex items-center gap-1 underline-offset-4 hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
168
|
+
>
|
|
169
|
+
{t("dispatch.pages.communityAppsTemplateSource")}
|
|
170
|
+
<IconArrowUpRight size={12} aria-hidden="true" />
|
|
171
|
+
</a>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
{source.installable ? (
|
|
175
|
+
<Button
|
|
176
|
+
type="button"
|
|
177
|
+
size="sm"
|
|
178
|
+
variant="outline"
|
|
179
|
+
disabled={install.isPending}
|
|
180
|
+
onClick={() => {
|
|
181
|
+
setInstallingId(source.id);
|
|
182
|
+
install.mutate({ sourceId: source.id });
|
|
183
|
+
}}
|
|
184
|
+
>
|
|
185
|
+
{install.isPending && installingId === source.id ? (
|
|
186
|
+
<Spinner className="mr-1.5" />
|
|
187
|
+
) : (
|
|
188
|
+
<IconPlus size={14} className="mr-1.5" />
|
|
189
|
+
)}
|
|
190
|
+
{install.isPending && installingId === source.id
|
|
191
|
+
? t("dispatch.pages.communityAppsInstalling")
|
|
192
|
+
: t("dispatch.pages.communityAppsInstall")}
|
|
193
|
+
</Button>
|
|
194
|
+
) : !source.installed &&
|
|
195
|
+
!source.pending &&
|
|
196
|
+
source.installUnavailableReason === "workspace-required" ? (
|
|
197
|
+
<span className="max-w-48 text-right text-xs text-muted-foreground">
|
|
198
|
+
{t("dispatch.pages.communityAppsWorkspaceRequired")}
|
|
199
|
+
</span>
|
|
200
|
+
) : !source.installed &&
|
|
201
|
+
!source.pending &&
|
|
202
|
+
source.installUnavailableReason === "app-id-conflict" ? (
|
|
203
|
+
<span className="max-w-48 text-right text-xs text-muted-foreground">
|
|
204
|
+
{t("dispatch.pages.communityAppsIdConflict")}
|
|
205
|
+
</span>
|
|
206
|
+
) : !source.installed &&
|
|
207
|
+
!source.pending &&
|
|
208
|
+
source.installUnavailableReason ===
|
|
209
|
+
"builder-install-unverified" ? (
|
|
210
|
+
<span className="max-w-48 text-right text-xs text-muted-foreground">
|
|
211
|
+
{t("dispatch.pages.communityAppsBuilderInstallUnavailable")}
|
|
212
|
+
</span>
|
|
213
|
+
) : null}
|
|
214
|
+
</AppListRow>
|
|
215
|
+
))}
|
|
216
|
+
</AppList>
|
|
217
|
+
) : (
|
|
218
|
+
<p className="text-sm text-muted-foreground">
|
|
219
|
+
{t("dispatch.pages.communityAppsEmpty")}
|
|
220
|
+
</p>
|
|
221
|
+
)}
|
|
222
|
+
</section>
|
|
223
|
+
);
|
|
224
|
+
}
|
|
@@ -360,6 +360,7 @@ export function DispatchControlPlane() {
|
|
|
360
360
|
<ActionQueryError
|
|
361
361
|
error={appsQuery.error}
|
|
362
362
|
onRetry={() => void appsQuery.refetch()}
|
|
363
|
+
queryName="list-workspace-apps"
|
|
363
364
|
/>
|
|
364
365
|
) : (
|
|
365
366
|
<AppsPanel apps={workspaceApps ?? []} isLoading={appsLoading} />
|
|
@@ -299,6 +299,7 @@ interface DispatchWorkspaceAppLauncher {
|
|
|
299
299
|
workspaceApps: readonly WorkspaceAppSummary[];
|
|
300
300
|
isLoading: boolean;
|
|
301
301
|
error?: unknown;
|
|
302
|
+
errorQueryName?: "list-workspace-apps" | "list-connected-agents";
|
|
302
303
|
openApp: (app: ChatFirstAppItem) => void;
|
|
303
304
|
retry: () => void;
|
|
304
305
|
}
|
|
@@ -1487,6 +1488,12 @@ function DispatchLayout({
|
|
|
1487
1488
|
const chatFirstAppsFailed = chatFirstAppsQuery.isError;
|
|
1488
1489
|
const chatFirstAppsError =
|
|
1489
1490
|
chatFirstAppsQuery.error ?? chatFirstConnectedAppsQuery.error;
|
|
1491
|
+
const chatFirstAppsErrorQueryName =
|
|
1492
|
+
chatFirstAppsQuery.error != null
|
|
1493
|
+
? "list-workspace-apps"
|
|
1494
|
+
: chatFirstConnectedAppsQuery.error != null
|
|
1495
|
+
? "list-connected-agents"
|
|
1496
|
+
: undefined;
|
|
1490
1497
|
const chatFirstResolvableApps = useMemo(
|
|
1491
1498
|
() =>
|
|
1492
1499
|
mergeChatFirstWorkspaceApps(
|
|
@@ -1574,6 +1581,7 @@ function DispatchLayout({
|
|
|
1574
1581
|
workspaceApps: chatFirstWorkspaceApps,
|
|
1575
1582
|
isLoading: chatFirstAppsLoading,
|
|
1576
1583
|
error: chatFirstAppsError,
|
|
1584
|
+
errorQueryName: chatFirstAppsErrorQueryName,
|
|
1577
1585
|
openApp: openChatFirstApp,
|
|
1578
1586
|
retry: () => {
|
|
1579
1587
|
void chatFirstAppsQuery.refetch();
|
|
@@ -1582,6 +1590,7 @@ function DispatchLayout({
|
|
|
1582
1590
|
}),
|
|
1583
1591
|
[
|
|
1584
1592
|
chatFirstAppsError,
|
|
1593
|
+
chatFirstAppsErrorQueryName,
|
|
1585
1594
|
chatFirstAppsLoading,
|
|
1586
1595
|
chatFirstAppsQuery.refetch,
|
|
1587
1596
|
chatFirstConnectedAppsQuery.refetch,
|