@agent-native/dispatch 0.15.24 → 0.15.26
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/get-agent-thread-debug.d.ts +7 -1
- package/dist/actions/get-agent-thread-debug.js +16 -3
- package/dist/actions/get-agent-thread-debug.js.map +1 -1
- package/dist/actions/list-connected-agents.js +7 -3
- package/dist/actions/list-connected-agents.js.map +1 -1
- package/dist/actions/list-curated-workspace-templates.js +1 -1
- package/dist/actions/list-curated-workspace-templates.js.map +1 -1
- package/dist/actions/provider-api-register.d.ts +14 -14
- package/dist/actions/remix-workspace-template.js +5 -5
- package/dist/actions/remix-workspace-template.js.map +1 -1
- package/dist/actions/search-agent-threads.js +1 -1
- package/dist/actions/search-agent-threads.js.map +1 -1
- package/dist/actions/start-workspace-app-creation.js +1 -1
- package/dist/actions/start-workspace-app-creation.js.map +1 -1
- package/dist/components/create-app-popover.js +2 -2
- package/dist/components/create-app-popover.js.map +1 -1
- package/dist/components/dispatch-control-plane.d.ts.map +1 -1
- package/dist/components/dispatch-control-plane.js +13 -3
- package/dist/components/dispatch-control-plane.js.map +1 -1
- package/dist/components/workspace-app-card.d.ts.map +1 -1
- package/dist/components/workspace-app-card.js +35 -4
- package/dist/components/workspace-app-card.js.map +1 -1
- package/dist/components/workspace-template-card.js +4 -4
- package/dist/components/workspace-template-card.js.map +1 -1
- package/dist/lib/workspace-apps.d.ts +3 -0
- package/dist/lib/workspace-apps.d.ts.map +1 -1
- package/dist/lib/workspace-apps.js.map +1 -1
- package/dist/routes/pages/apps.d.ts.map +1 -1
- package/dist/routes/pages/apps.js +19 -4
- package/dist/routes/pages/apps.js.map +1 -1
- package/dist/routes/pages/thread-debug.d.ts.map +1 -1
- package/dist/routes/pages/thread-debug.js +25 -11
- package/dist/routes/pages/thread-debug.js.map +1 -1
- package/dist/server/lib/app-creation-store.d.ts +3 -0
- package/dist/server/lib/app-creation-store.d.ts.map +1 -1
- package/dist/server/lib/app-creation-store.js +54 -2
- package/dist/server/lib/app-creation-store.js.map +1 -1
- package/dist/server/lib/curated-workspace-templates.d.ts +1 -1
- package/dist/server/lib/curated-workspace-templates.js +2 -2
- package/dist/server/lib/curated-workspace-templates.js.map +1 -1
- package/dist/server/lib/thread-debug-store.d.ts +7 -1
- package/dist/server/lib/thread-debug-store.d.ts.map +1 -1
- package/dist/server/lib/thread-debug-store.js +43 -5
- package/dist/server/lib/thread-debug-store.js.map +1 -1
- package/dist/server/plugins/integrations.js +1 -1
- package/dist/server/plugins/integrations.js.map +1 -1
- package/package.json +3 -3
- package/src/actions/get-agent-thread-debug.ts +30 -15
- package/src/actions/list-connected-agents.ts +7 -2
- package/src/actions/list-curated-workspace-templates.ts +1 -1
- package/src/actions/remix-workspace-template.spec.ts +2 -2
- package/src/actions/remix-workspace-template.ts +5 -5
- package/src/actions/search-agent-threads.ts +1 -1
- package/src/actions/start-workspace-app-creation.ts +1 -1
- package/src/components/create-app-popover.tsx +2 -2
- package/src/components/dispatch-control-plane.tsx +70 -4
- package/src/components/workspace-app-card.spec.tsx +43 -0
- package/src/components/workspace-app-card.tsx +97 -1
- package/src/components/workspace-template-card.spec.tsx +4 -4
- package/src/components/workspace-template-card.tsx +4 -4
- package/src/lib/workspace-apps.ts +3 -0
- package/src/routes/pages/apps.tsx +84 -4
- package/src/routes/pages/thread-debug.tsx +34 -14
- package/src/server/lib/app-creation-store.ts +95 -2
- package/src/server/lib/curated-workspace-templates.ts +2 -2
- package/src/server/lib/thread-debug-store.spec.ts +82 -0
- package/src/server/lib/thread-debug-store.ts +62 -6
- package/src/server/plugins/integrations.ts +1 -1
|
@@ -68,7 +68,7 @@ describe("remix-workspace-template action", () => {
|
|
|
68
68
|
);
|
|
69
69
|
});
|
|
70
70
|
|
|
71
|
-
it("starts a hosted private
|
|
71
|
+
it("starts a hosted private app without copying data or secrets", async () => {
|
|
72
72
|
vi.stubEnv("NODE_ENV", "production");
|
|
73
73
|
mocks.startWorkspaceAppCreation.mockResolvedValue({
|
|
74
74
|
mode: "builder",
|
|
@@ -88,7 +88,7 @@ describe("remix-workspace-template action", () => {
|
|
|
88
88
|
template: "calendar",
|
|
89
89
|
description: "A private scheduling workflow.",
|
|
90
90
|
prompt: expect.stringContaining(
|
|
91
|
-
"private workspace
|
|
91
|
+
"private workspace app from a curated first-party template",
|
|
92
92
|
),
|
|
93
93
|
}),
|
|
94
94
|
);
|
|
@@ -17,11 +17,11 @@ function buildRemixPrompt(input: {
|
|
|
17
17
|
description?: string | null;
|
|
18
18
|
}): string {
|
|
19
19
|
return [
|
|
20
|
-
"Create a private workspace
|
|
20
|
+
"Create a private workspace app from a curated first-party template.",
|
|
21
21
|
`Source template: ${input.templateName} (${input.templateId}).`,
|
|
22
22
|
"Recreate the source template's product shape and capabilities as an independent workspace app.",
|
|
23
23
|
"Never copy source-app data, records, user content, secrets, credentials, tokens, API keys, or private configuration.",
|
|
24
|
-
"Use empty or synthetic seed data only, keep the
|
|
24
|
+
"Use empty or synthetic seed data only, keep the new app private to the current workspace, and do not create a public demo.",
|
|
25
25
|
`Setup note: ${input.setupNote}`,
|
|
26
26
|
input.description?.trim()
|
|
27
27
|
? `Requested customization: ${input.description.trim()}`
|
|
@@ -31,7 +31,7 @@ function buildRemixPrompt(input: {
|
|
|
31
31
|
|
|
32
32
|
export default defineAction({
|
|
33
33
|
description:
|
|
34
|
-
"Create a private, independent
|
|
34
|
+
"Create a private, independent app from one of Dispatch's curated first-party workspace templates. Valid templates are mail, calendar, analytics, slides, content, clips, brain, assets, forms, and design. The new app must not copy source-app data, secrets, credentials, or private configuration; local workspaces scaffold the template, while hosted workspaces start a Builder app-creation branch.",
|
|
35
35
|
schema: z.object({
|
|
36
36
|
templateId: z
|
|
37
37
|
.string()
|
|
@@ -56,7 +56,7 @@ export default defineAction({
|
|
|
56
56
|
.max(500)
|
|
57
57
|
.optional()
|
|
58
58
|
.nullable()
|
|
59
|
-
.describe("Optional customization or purpose for the private
|
|
59
|
+
.describe("Optional customization or purpose for the private app."),
|
|
60
60
|
}),
|
|
61
61
|
run: async (input) => {
|
|
62
62
|
const sourceTemplate = getCuratedWorkspaceTemplate(input.templateId);
|
|
@@ -83,7 +83,7 @@ export default defineAction({
|
|
|
83
83
|
action: "workspace-app.remix-requested",
|
|
84
84
|
targetType: "workspace-app",
|
|
85
85
|
targetId: appId,
|
|
86
|
-
summary: `Requested private
|
|
86
|
+
summary: `Requested private app from ${sourceTemplate.name}`,
|
|
87
87
|
metadata: {
|
|
88
88
|
sourceTemplate: sourceTemplate.template,
|
|
89
89
|
mode:
|
|
@@ -5,7 +5,7 @@ import { searchAgentThreads } from "../server/lib/thread-debug-store.js";
|
|
|
5
5
|
|
|
6
6
|
export default defineAction({
|
|
7
7
|
description:
|
|
8
|
-
"Search agent chat threads by title, preview,
|
|
8
|
+
"Search agent chat threads by title, preview, full persisted thread content, or an exact request/run ID. Non-admins are limited to their own current Dispatch DB threads.",
|
|
9
9
|
schema: z.object({
|
|
10
10
|
sourceId: z
|
|
11
11
|
.string()
|
|
@@ -6,7 +6,7 @@ import { startWorkspaceAppCreation } from "../server/lib/app-creation-store.js";
|
|
|
6
6
|
|
|
7
7
|
export default defineAction({
|
|
8
8
|
description:
|
|
9
|
-
'Start creating a new workspace app from Dispatch when the request truly needs its own app. Callers should include a concise generated description by default; Dispatch generates one from the prompt when omitted. In local dev this returns a code-agent prompt; in production it creates a Builder branch only when a Builder branch project is configured. If the result mode is "coming-soon", the work still requires a code change but no Builder Cloud Agent can run here; tell the user to edit locally or use Builder.io to edit this code in the cloud and continue customizing the app any way they like, and do not send them to Builder org/beta settings. The result must be a separate workspace app under apps/<app-id>, not a new route or file in apps/chat. If chat is used as the source template, the finished app must be branded as the requested app and must not leave visible "Chat", "Starter", "Blank app", or "New app" UI behind. If the request needs Mail, Calendar, Analytics, Brain, Assets, or another first-party app, use the existing hosted/connected app via links or A2A; do not
|
|
9
|
+
'Start creating a new workspace app from Dispatch when the request truly needs its own app. Callers should include a concise generated description by default; Dispatch generates one from the prompt when omitted. In local dev this returns a code-agent prompt; in production it creates a Builder branch only when a Builder branch project is configured. If the result mode is "coming-soon", the work still requires a code change but no Builder Cloud Agent can run here; tell the user to edit locally or use Builder.io to edit this code in the cloud and continue customizing the app any way they like, and do not send them to Builder org/beta settings. The result must be a separate workspace app under apps/<app-id>, not a new route or file in apps/chat. If chat is used as the source template, the finished app must be branded as the requested app and must not leave visible "Chat", "Starter", "Blank app", or "New app" UI behind. If the request needs Mail, Calendar, Analytics, Brain, Assets, or another first-party app, use the existing hosted/connected app via links or A2A; do not wrap or nest those apps inside the new app unless the user explicitly asks for a customized app from that template.',
|
|
10
10
|
schema: z.object({
|
|
11
11
|
prompt: z.string().min(1).describe("The user's app creation request"),
|
|
12
12
|
appId: z
|
|
@@ -117,8 +117,8 @@ function buildAppCreationPrompt(input: {
|
|
|
117
117
|
`Use relative workspace links like /${input.appId}. Do not hardcode localhost, 127.0.0.1, 8080, 8100, or any dev port; the active workspace gateway/browser origin owns the port.`,
|
|
118
118
|
`Use the framework/template UI stack: shadcn/ui components and @tabler/icons-react. Do not add lucide-react or another icon library for standard UI.`,
|
|
119
119
|
`Existing first-party apps are neighbors, not implementation details for this app. If the user's prompt mentions Mail, Calendar, Analytics, Brain, Assets, Dispatch, or other templates, treat them as existing hosted/connected apps that this app can link to or call through A2A/default connected agents. For example, Mail, Calendar, Analytics, Brain, and Assets already exist at https://mail.agent-native.com, https://calendar.agent-native.com, https://analytics.agent-native.com, https://brain.agent-native.com, and https://assets.agent-native.com.`,
|
|
120
|
-
`Do not
|
|
121
|
-
`Only create another first-party app
|
|
120
|
+
`Do not create wrapper apps or scaffold child apps/routes for Mail, Calendar, Analytics, Brain, Assets, etc. inside apps/${input.appId} just so this app can access them. If the request is a cross-app dashboard or overview, build only the new dashboard/overview app and delegate to the existing apps for domain work.`,
|
|
121
|
+
`Only create another first-party app when the user explicitly asks for a customized app from that template; otherwise keep using the hosted/shared app so improvements to the base app keep flowing to users.`,
|
|
122
122
|
`Do not satisfy this by adding a route, page, component, or file inside apps/chat or another existing app unless the user explicitly asks to modify that existing app.`,
|
|
123
123
|
input.vaultAccessMode === "all-apps"
|
|
124
124
|
? `Do not create per-app Dispatch vault grants unless the workspace switches vault access to manual or the user explicitly asks for manual grants.`
|
|
@@ -6,8 +6,13 @@ import { PromptComposer } from "@agent-native/core/client/composer";
|
|
|
6
6
|
import { useActionQuery } from "@agent-native/core/client/hooks";
|
|
7
7
|
import { isInBuilderFrame } from "@agent-native/core/client/host";
|
|
8
8
|
import { useT } from "@agent-native/core/client/i18n";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
IconArrowUpRight,
|
|
11
|
+
IconChevronDown,
|
|
12
|
+
IconClockHour4,
|
|
13
|
+
} from "@tabler/icons-react";
|
|
10
14
|
import type { ReactNode } from "react";
|
|
15
|
+
import { useState } from "react";
|
|
11
16
|
import { Link, useNavigate } from "react-router";
|
|
12
17
|
|
|
13
18
|
import { submitOverviewPrompt } from "../lib/overview-chat";
|
|
@@ -16,6 +21,11 @@ import { ActionQueryError } from "./action-query-error";
|
|
|
16
21
|
import { CreateAppPopover } from "./create-app-popover";
|
|
17
22
|
import { useSetPageTitle } from "./layout/HeaderActions";
|
|
18
23
|
import { Button } from "./ui/button";
|
|
24
|
+
import {
|
|
25
|
+
Collapsible,
|
|
26
|
+
CollapsibleContent,
|
|
27
|
+
CollapsibleTrigger,
|
|
28
|
+
} from "./ui/collapsible";
|
|
19
29
|
import { Skeleton } from "./ui/skeleton";
|
|
20
30
|
import { WorkspaceAppCard } from "./workspace-app-card";
|
|
21
31
|
|
|
@@ -135,8 +145,13 @@ function AppsPanel({
|
|
|
135
145
|
apps: WorkspaceAppSummary[];
|
|
136
146
|
isLoading: boolean;
|
|
137
147
|
}) {
|
|
148
|
+
const t = useT();
|
|
149
|
+
const [showPending, setShowPending] = useState(false);
|
|
138
150
|
const visibleApps = apps.filter((app) => !app.isDispatch && !app.archived);
|
|
139
|
-
const
|
|
151
|
+
const activeApps = visibleApps.filter((app) => app.status !== "pending");
|
|
152
|
+
const pendingApps = visibleApps.filter((app) => app.status === "pending");
|
|
153
|
+
const showSkeletons =
|
|
154
|
+
isLoading && activeApps.length === 0 && pendingApps.length === 0;
|
|
140
155
|
|
|
141
156
|
return (
|
|
142
157
|
<section className="flex flex-col gap-3">
|
|
@@ -161,15 +176,66 @@ function AppsPanel({
|
|
|
161
176
|
</div>
|
|
162
177
|
))}
|
|
163
178
|
</div>
|
|
164
|
-
) :
|
|
179
|
+
) : activeApps.length > 0 ? (
|
|
165
180
|
<div className="grid gap-3 md:grid-cols-2">
|
|
166
|
-
{
|
|
181
|
+
{activeApps.map((app) => (
|
|
167
182
|
<WorkspaceAppCard key={app.id} app={app} className="min-h-32" />
|
|
168
183
|
))}
|
|
169
184
|
</div>
|
|
170
185
|
) : (
|
|
171
186
|
<CreateAppPopover />
|
|
172
187
|
)}
|
|
188
|
+
{pendingApps.length > 0 ? (
|
|
189
|
+
<Collapsible open={showPending} onOpenChange={setShowPending}>
|
|
190
|
+
<div className="space-y-3 border-t pt-3">
|
|
191
|
+
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
192
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
193
|
+
<IconClockHour4
|
|
194
|
+
size={15}
|
|
195
|
+
className="shrink-0 text-muted-foreground"
|
|
196
|
+
/>
|
|
197
|
+
<div className="min-w-0">
|
|
198
|
+
<h3 className="text-xs font-semibold text-foreground">
|
|
199
|
+
{t("dispatch.pages.pendingApps")}
|
|
200
|
+
</h3>
|
|
201
|
+
<p className="text-xs text-muted-foreground">
|
|
202
|
+
{t("dispatch.pages.pendingAppsDescription", {
|
|
203
|
+
count: pendingApps.length,
|
|
204
|
+
})}
|
|
205
|
+
</p>
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
<CollapsibleTrigger asChild>
|
|
209
|
+
<Button
|
|
210
|
+
type="button"
|
|
211
|
+
variant="ghost"
|
|
212
|
+
size="sm"
|
|
213
|
+
className="h-7 gap-1.5 px-2 text-xs"
|
|
214
|
+
>
|
|
215
|
+
{showPending
|
|
216
|
+
? t("dispatch.pages.hidePendingApps")
|
|
217
|
+
: t("dispatch.pages.showPendingApps")}
|
|
218
|
+
<IconChevronDown
|
|
219
|
+
size={13}
|
|
220
|
+
className={showPending ? "rotate-180" : undefined}
|
|
221
|
+
/>
|
|
222
|
+
</Button>
|
|
223
|
+
</CollapsibleTrigger>
|
|
224
|
+
</div>
|
|
225
|
+
<CollapsibleContent>
|
|
226
|
+
<div className="grid gap-3 md:grid-cols-2">
|
|
227
|
+
{pendingApps.map((app) => (
|
|
228
|
+
<WorkspaceAppCard
|
|
229
|
+
key={app.id}
|
|
230
|
+
app={app}
|
|
231
|
+
className="min-h-32"
|
|
232
|
+
/>
|
|
233
|
+
))}
|
|
234
|
+
</div>
|
|
235
|
+
</CollapsibleContent>
|
|
236
|
+
</div>
|
|
237
|
+
</Collapsible>
|
|
238
|
+
) : null}
|
|
173
239
|
</section>
|
|
174
240
|
);
|
|
175
241
|
}
|
|
@@ -17,6 +17,13 @@ vi.mock("@agent-native/core/client/hooks", () => ({
|
|
|
17
17
|
}),
|
|
18
18
|
}));
|
|
19
19
|
|
|
20
|
+
vi.mock("@agent-native/core/client/i18n", () => ({
|
|
21
|
+
useFormatters: () => ({
|
|
22
|
+
formatDate: (value: string) => value,
|
|
23
|
+
}),
|
|
24
|
+
useT: () => (key: string) => key,
|
|
25
|
+
}));
|
|
26
|
+
|
|
20
27
|
describe("WorkspaceAppCard", () => {
|
|
21
28
|
let container: HTMLDivElement;
|
|
22
29
|
let root: Root;
|
|
@@ -140,4 +147,40 @@ describe("WorkspaceAppCard", () => {
|
|
|
140
147
|
expect(appLink?.getAttribute("target")).toBe("_blank");
|
|
141
148
|
expect(appLink?.getAttribute("rel")).toBe("noreferrer");
|
|
142
149
|
});
|
|
150
|
+
|
|
151
|
+
it("shows ownership metadata in the more menu", async () => {
|
|
152
|
+
await act(async () => {
|
|
153
|
+
root.render(
|
|
154
|
+
<TooltipProvider>
|
|
155
|
+
<WorkspaceAppCard
|
|
156
|
+
app={{
|
|
157
|
+
id: "analytics",
|
|
158
|
+
name: "Analytics",
|
|
159
|
+
path: "/analytics",
|
|
160
|
+
createdAt: "2026-07-28T12:00:00.000Z",
|
|
161
|
+
createdBy: "creator@example.com",
|
|
162
|
+
owner: "owner@example.com",
|
|
163
|
+
teams: ["Growth", "Operations"],
|
|
164
|
+
status: "ready",
|
|
165
|
+
}}
|
|
166
|
+
/>
|
|
167
|
+
</TooltipProvider>,
|
|
168
|
+
);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
const moreButton = container.querySelector<HTMLButtonElement>(
|
|
172
|
+
'button[aria-label="More actions for Analytics"]',
|
|
173
|
+
);
|
|
174
|
+
expect(moreButton).not.toBeNull();
|
|
175
|
+
|
|
176
|
+
await act(async () => {
|
|
177
|
+
moreButton?.dispatchEvent(
|
|
178
|
+
new MouseEvent("pointerdown", { bubbles: true, button: 0 }),
|
|
179
|
+
);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
expect(document.body.textContent).toContain("creator@example.com");
|
|
183
|
+
expect(document.body.textContent).toContain("owner@example.com");
|
|
184
|
+
expect(document.body.textContent).toContain("Growth, Operations");
|
|
185
|
+
});
|
|
143
186
|
});
|
|
@@ -2,7 +2,9 @@ import {
|
|
|
2
2
|
useActionMutation,
|
|
3
3
|
useActionQuery,
|
|
4
4
|
} from "@agent-native/core/client/hooks";
|
|
5
|
+
import { useFormatters, useT } from "@agent-native/core/client/i18n";
|
|
5
6
|
import {
|
|
7
|
+
IconCalendar,
|
|
6
8
|
IconChevronDown,
|
|
7
9
|
IconChevronRight,
|
|
8
10
|
IconClockHour4,
|
|
@@ -13,6 +15,8 @@ import {
|
|
|
13
15
|
IconFileText,
|
|
14
16
|
IconWorld,
|
|
15
17
|
IconTrash,
|
|
18
|
+
IconUser,
|
|
19
|
+
IconUsersGroup,
|
|
16
20
|
} from "@tabler/icons-react";
|
|
17
21
|
import { useEffect, useState, type FormEvent } from "react";
|
|
18
22
|
import { toast } from "sonner";
|
|
@@ -40,6 +44,8 @@ import {
|
|
|
40
44
|
DropdownMenu,
|
|
41
45
|
DropdownMenuContent,
|
|
42
46
|
DropdownMenuItem,
|
|
47
|
+
DropdownMenuLabel,
|
|
48
|
+
DropdownMenuSeparator,
|
|
43
49
|
DropdownMenuTrigger,
|
|
44
50
|
} from "./ui/dropdown-menu";
|
|
45
51
|
import { Input } from "./ui/input";
|
|
@@ -58,6 +64,8 @@ export function WorkspaceAppCard({
|
|
|
58
64
|
app: WorkspaceAppSummary;
|
|
59
65
|
className?: string;
|
|
60
66
|
}) {
|
|
67
|
+
const t = useT();
|
|
68
|
+
const { formatDate } = useFormatters();
|
|
61
69
|
const href = workspaceAppHref(app);
|
|
62
70
|
const openInNewTab = isPendingBuilderHref(app);
|
|
63
71
|
const isPending = app.status === "pending";
|
|
@@ -215,7 +223,7 @@ export function WorkspaceAppCard({
|
|
|
215
223
|
</TooltipTrigger>
|
|
216
224
|
<TooltipContent>More actions</TooltipContent>
|
|
217
225
|
</Tooltip>
|
|
218
|
-
<DropdownMenuContent align="end" className="w-
|
|
226
|
+
<DropdownMenuContent align="end" className="w-72">
|
|
219
227
|
<DropdownMenuItem
|
|
220
228
|
onSelect={(event) => {
|
|
221
229
|
event.preventDefault();
|
|
@@ -244,6 +252,20 @@ export function WorkspaceAppCard({
|
|
|
244
252
|
Hide from list
|
|
245
253
|
</DropdownMenuItem>
|
|
246
254
|
)}
|
|
255
|
+
<DropdownMenuSeparator />
|
|
256
|
+
<DropdownMenuLabel className="font-normal">
|
|
257
|
+
<WorkspaceAppMetadata
|
|
258
|
+
app={app}
|
|
259
|
+
formatDate={formatDate}
|
|
260
|
+
labels={{
|
|
261
|
+
created: t("agents.dashboardMetadataCreated"),
|
|
262
|
+
createdBy: t("agents.dashboardMetadataCreatedBy"),
|
|
263
|
+
owner: t("dispatch.pages.appMetadataOwner"),
|
|
264
|
+
teams: t("dispatch.pages.appMetadataTeams"),
|
|
265
|
+
notTracked: t("agents.notTracked"),
|
|
266
|
+
}}
|
|
267
|
+
/>
|
|
268
|
+
</DropdownMenuLabel>
|
|
247
269
|
</DropdownMenuContent>
|
|
248
270
|
</DropdownMenu>
|
|
249
271
|
</div>
|
|
@@ -293,6 +315,80 @@ export function WorkspaceAppCard({
|
|
|
293
315
|
);
|
|
294
316
|
}
|
|
295
317
|
|
|
318
|
+
function WorkspaceAppMetadata({
|
|
319
|
+
app,
|
|
320
|
+
formatDate,
|
|
321
|
+
labels,
|
|
322
|
+
}: {
|
|
323
|
+
app: WorkspaceAppSummary;
|
|
324
|
+
formatDate: ReturnType<typeof useFormatters>["formatDate"];
|
|
325
|
+
labels: {
|
|
326
|
+
created: string;
|
|
327
|
+
createdBy: string;
|
|
328
|
+
owner: string;
|
|
329
|
+
teams: string;
|
|
330
|
+
notTracked: string;
|
|
331
|
+
};
|
|
332
|
+
}) {
|
|
333
|
+
function formatMetadataDate(value: string | null | undefined): string {
|
|
334
|
+
if (!value) return labels.notTracked;
|
|
335
|
+
try {
|
|
336
|
+
return formatDate(value, {
|
|
337
|
+
year: "numeric",
|
|
338
|
+
month: "short",
|
|
339
|
+
day: "numeric",
|
|
340
|
+
hour: "numeric",
|
|
341
|
+
minute: "2-digit",
|
|
342
|
+
});
|
|
343
|
+
} catch {
|
|
344
|
+
return value;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return (
|
|
349
|
+
<div className="flex flex-col gap-1.5 text-xs text-muted-foreground">
|
|
350
|
+
<WorkspaceAppMetadataRow
|
|
351
|
+
icon={IconCalendar}
|
|
352
|
+
label={labels.created}
|
|
353
|
+
value={formatMetadataDate(app.createdAt)}
|
|
354
|
+
/>
|
|
355
|
+
<WorkspaceAppMetadataRow
|
|
356
|
+
icon={IconUser}
|
|
357
|
+
label={labels.createdBy}
|
|
358
|
+
value={app.createdBy || labels.notTracked}
|
|
359
|
+
/>
|
|
360
|
+
<WorkspaceAppMetadataRow
|
|
361
|
+
icon={IconUser}
|
|
362
|
+
label={labels.owner}
|
|
363
|
+
value={app.owner || labels.notTracked}
|
|
364
|
+
/>
|
|
365
|
+
<WorkspaceAppMetadataRow
|
|
366
|
+
icon={IconUsersGroup}
|
|
367
|
+
label={labels.teams}
|
|
368
|
+
value={app.teams?.join(", ") || labels.notTracked}
|
|
369
|
+
/>
|
|
370
|
+
</div>
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function WorkspaceAppMetadataRow({
|
|
375
|
+
icon: Icon,
|
|
376
|
+
label,
|
|
377
|
+
value,
|
|
378
|
+
}: {
|
|
379
|
+
icon: typeof IconCalendar;
|
|
380
|
+
label: string;
|
|
381
|
+
value: string;
|
|
382
|
+
}) {
|
|
383
|
+
return (
|
|
384
|
+
<div className="flex items-start gap-1.5">
|
|
385
|
+
<Icon className="mt-0.5 h-3 w-3 shrink-0" />
|
|
386
|
+
<span className="shrink-0">{label}</span>
|
|
387
|
+
<span className="min-w-0 truncate text-foreground/80">{value}</span>
|
|
388
|
+
</div>
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
|
|
296
392
|
function AppResourcesDialog({ app }: { app: WorkspaceAppSummary }) {
|
|
297
393
|
const [open, setOpen] = useState(false);
|
|
298
394
|
const [inspectedResourceId, setInspectedResourceId] = useState<string | null>(
|
|
@@ -95,7 +95,7 @@ describe("WorkspaceTemplateCard", () => {
|
|
|
95
95
|
).toBe(null);
|
|
96
96
|
});
|
|
97
97
|
|
|
98
|
-
it("
|
|
98
|
+
it("creates an app with the default app id and allows an override", async () => {
|
|
99
99
|
await act(async () => {
|
|
100
100
|
root.render(
|
|
101
101
|
<WorkspaceTemplateCard template={template} defaultAppId="pipeline" />,
|
|
@@ -103,7 +103,7 @@ describe("WorkspaceTemplateCard", () => {
|
|
|
103
103
|
});
|
|
104
104
|
|
|
105
105
|
const trigger = Array.from(container.querySelectorAll("button")).find(
|
|
106
|
-
(button) => button.textContent?.includes("
|
|
106
|
+
(button) => button.textContent?.includes("Create from template"),
|
|
107
107
|
);
|
|
108
108
|
expect(trigger).not.toBeUndefined();
|
|
109
109
|
|
|
@@ -140,7 +140,7 @@ describe("WorkspaceTemplateCard", () => {
|
|
|
140
140
|
});
|
|
141
141
|
});
|
|
142
142
|
expect(toast.success).toHaveBeenCalledWith(
|
|
143
|
-
"Template
|
|
143
|
+
"Template app creation started.",
|
|
144
144
|
);
|
|
145
145
|
});
|
|
146
146
|
|
|
@@ -167,7 +167,7 @@ describe("WorkspaceTemplateCard", () => {
|
|
|
167
167
|
expect(container.textContent).toContain("Installed");
|
|
168
168
|
expect(container.querySelector('a[href^="https://"]')).toBeNull();
|
|
169
169
|
const remixButton = Array.from(container.querySelectorAll("button")).find(
|
|
170
|
-
(button) => button.textContent?.includes("
|
|
170
|
+
(button) => button.textContent?.includes("Create from template"),
|
|
171
171
|
);
|
|
172
172
|
expect(remixButton).not.toHaveProperty("disabled", true);
|
|
173
173
|
});
|
|
@@ -78,10 +78,10 @@ const DEFAULT_LABELS: WorkspaceTemplateLabels = {
|
|
|
78
78
|
cancel: "Cancel",
|
|
79
79
|
integrationSetup: "Integration setup",
|
|
80
80
|
installed: "Installed",
|
|
81
|
-
remix: "
|
|
82
|
-
remixing: "
|
|
83
|
-
remixSuccess: "Template
|
|
84
|
-
remixError: "Could not
|
|
81
|
+
remix: "Create from template",
|
|
82
|
+
remixing: "Creating app…",
|
|
83
|
+
remixSuccess: "Template app creation started.",
|
|
84
|
+
remixError: "Could not create an app from this template",
|
|
85
85
|
appIdRequired: "App ID is required.",
|
|
86
86
|
source: "Source",
|
|
87
87
|
viewLiveApp: "View the live app",
|
|
@@ -15,6 +15,9 @@ export interface WorkspaceAppSummary {
|
|
|
15
15
|
builderUrl?: string | null;
|
|
16
16
|
branchName?: string | null;
|
|
17
17
|
createdAt?: string | null;
|
|
18
|
+
createdBy?: string | null;
|
|
19
|
+
owner?: string | null;
|
|
20
|
+
teams?: string[];
|
|
18
21
|
agentCardUrl?: string | null;
|
|
19
22
|
agentCardReachable?: boolean;
|
|
20
23
|
a2aEndpointUrl?: string | null;
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
IconApps,
|
|
6
6
|
IconArrowUpRight,
|
|
7
7
|
IconChevronDown,
|
|
8
|
+
IconClockHour4,
|
|
8
9
|
IconEyeOff,
|
|
9
10
|
IconPlus,
|
|
10
11
|
IconStack2,
|
|
@@ -46,6 +47,7 @@ interface WorkspaceInfo {
|
|
|
46
47
|
|
|
47
48
|
export default function AppsRoute() {
|
|
48
49
|
const t = useT();
|
|
50
|
+
const [showPending, setShowPending] = useState(false);
|
|
49
51
|
const [showHidden, setShowHidden] = useState(false);
|
|
50
52
|
const appsQuery = useActionQuery("list-workspace-apps", {
|
|
51
53
|
includeAgentCards: false,
|
|
@@ -68,6 +70,8 @@ export default function AppsRoute() {
|
|
|
68
70
|
(app) => !app.isDispatch,
|
|
69
71
|
);
|
|
70
72
|
const visibleApps = allApps.filter((app) => !app.archived);
|
|
73
|
+
const activeApps = visibleApps.filter((app) => app.status !== "pending");
|
|
74
|
+
const pendingApps = visibleApps.filter((app) => app.status === "pending");
|
|
71
75
|
const archivedApps = allApps.filter((app) => app.archived);
|
|
72
76
|
const otherApps = filterOtherApps(
|
|
73
77
|
(connectedAppsQuery.data || []) as ConnectedAppSummary[],
|
|
@@ -118,8 +122,13 @@ export default function AppsRoute() {
|
|
|
118
122
|
</h2>
|
|
119
123
|
<p className="mt-0.5 text-xs text-muted-foreground">
|
|
120
124
|
{t("dispatch.pages.activeCount", {
|
|
121
|
-
count:
|
|
125
|
+
count: activeApps.length,
|
|
122
126
|
})}
|
|
127
|
+
{pendingApps.length > 0
|
|
128
|
+
? ` · ${t("dispatch.pages.pendingCount", {
|
|
129
|
+
count: pendingApps.length,
|
|
130
|
+
})}`
|
|
131
|
+
: ""}
|
|
123
132
|
{archivedApps.length > 0
|
|
124
133
|
? ` · ${t("dispatch.pages.hiddenCount", {
|
|
125
134
|
count: archivedApps.length,
|
|
@@ -128,7 +137,7 @@ export default function AppsRoute() {
|
|
|
128
137
|
</p>
|
|
129
138
|
</div>
|
|
130
139
|
</div>
|
|
131
|
-
{
|
|
140
|
+
{activeApps.length > 0 || pendingApps.length > 0 ? (
|
|
132
141
|
<CreateAppPopover
|
|
133
142
|
align="end"
|
|
134
143
|
trigger={
|
|
@@ -148,17 +157,74 @@ export default function AppsRoute() {
|
|
|
148
157
|
/>
|
|
149
158
|
) : showAppSkeletons ? (
|
|
150
159
|
<AppsSkeletonGrid />
|
|
151
|
-
) :
|
|
160
|
+
) : activeApps.length > 0 ? (
|
|
152
161
|
<div className="grid auto-rows-fr gap-3 md:grid-cols-2 xl:grid-cols-3">
|
|
153
|
-
{
|
|
162
|
+
{activeApps.map((app) => (
|
|
154
163
|
<WorkspaceAppCard key={app.id} app={app} className="h-full" />
|
|
155
164
|
))}
|
|
156
165
|
</div>
|
|
166
|
+
) : pendingApps.length > 0 ? (
|
|
167
|
+
<EmptyActiveAppsState />
|
|
157
168
|
) : (
|
|
158
169
|
<EmptyAppsState />
|
|
159
170
|
)}
|
|
160
171
|
</section>
|
|
161
172
|
|
|
173
|
+
{pendingApps.length > 0 ? (
|
|
174
|
+
<Collapsible open={showPending} onOpenChange={setShowPending}>
|
|
175
|
+
<section className="space-y-3 border-t pt-4">
|
|
176
|
+
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
177
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
178
|
+
<IconClockHour4
|
|
179
|
+
size={16}
|
|
180
|
+
className="shrink-0 text-muted-foreground"
|
|
181
|
+
/>
|
|
182
|
+
<div className="min-w-0">
|
|
183
|
+
<h2 className="text-sm font-semibold text-foreground">
|
|
184
|
+
{t("dispatch.pages.pendingApps")}
|
|
185
|
+
</h2>
|
|
186
|
+
<p className="text-xs text-muted-foreground">
|
|
187
|
+
{t("dispatch.pages.pendingAppsDescription", {
|
|
188
|
+
count: pendingApps.length,
|
|
189
|
+
})}
|
|
190
|
+
</p>
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
<CollapsibleTrigger asChild>
|
|
194
|
+
<Button
|
|
195
|
+
type="button"
|
|
196
|
+
variant="outline"
|
|
197
|
+
size="sm"
|
|
198
|
+
className="gap-1.5"
|
|
199
|
+
>
|
|
200
|
+
{showPending
|
|
201
|
+
? t("dispatch.pages.hidePendingApps")
|
|
202
|
+
: t("dispatch.pages.showPendingApps")}
|
|
203
|
+
<IconChevronDown
|
|
204
|
+
size={14}
|
|
205
|
+
className={cn(
|
|
206
|
+
"transition-transform",
|
|
207
|
+
showPending && "rotate-180",
|
|
208
|
+
)}
|
|
209
|
+
/>
|
|
210
|
+
</Button>
|
|
211
|
+
</CollapsibleTrigger>
|
|
212
|
+
</div>
|
|
213
|
+
<CollapsibleContent>
|
|
214
|
+
<div className="grid auto-rows-fr gap-3 md:grid-cols-2 xl:grid-cols-3">
|
|
215
|
+
{pendingApps.map((app) => (
|
|
216
|
+
<WorkspaceAppCard
|
|
217
|
+
key={app.id}
|
|
218
|
+
app={app}
|
|
219
|
+
className="h-full"
|
|
220
|
+
/>
|
|
221
|
+
))}
|
|
222
|
+
</div>
|
|
223
|
+
</CollapsibleContent>
|
|
224
|
+
</section>
|
|
225
|
+
</Collapsible>
|
|
226
|
+
) : null}
|
|
227
|
+
|
|
162
228
|
{curatedTemplatesQuery.isError ? (
|
|
163
229
|
<section className="space-y-3 border-t pt-4">
|
|
164
230
|
<ActionQueryError
|
|
@@ -400,3 +466,17 @@ function EmptyAppsState() {
|
|
|
400
466
|
</div>
|
|
401
467
|
);
|
|
402
468
|
}
|
|
469
|
+
|
|
470
|
+
function EmptyActiveAppsState() {
|
|
471
|
+
const t = useT();
|
|
472
|
+
return (
|
|
473
|
+
<div className="rounded-lg border border-dashed bg-card px-4 py-10 text-center">
|
|
474
|
+
<p className="text-sm font-medium text-foreground">
|
|
475
|
+
{t("dispatch.pages.noActiveWorkspaceApps")}
|
|
476
|
+
</p>
|
|
477
|
+
<p className="mx-auto mt-1 max-w-md text-xs text-muted-foreground">
|
|
478
|
+
{t("dispatch.pages.noActiveWorkspaceAppsDescription")}
|
|
479
|
+
</p>
|
|
480
|
+
</div>
|
|
481
|
+
);
|
|
482
|
+
}
|