@agent-native/dispatch 0.15.25 → 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/list-connected-agents.js +7 -3
- package/dist/actions/list-connected-agents.js.map +1 -1
- package/dist/actions/provider-api-register.d.ts +4 -4
- 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/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/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 +52 -0
- package/dist/server/lib/app-creation-store.js.map +1 -1
- package/package.json +3 -3
- package/src/actions/list-connected-agents.ts +7 -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/lib/workspace-apps.ts +3 -0
- package/src/routes/pages/apps.tsx +84 -4
- package/src/server/lib/app-creation-store.ts +93 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agent-native/dispatch",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.26",
|
|
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.10.
|
|
87
|
+
"@agent-native/toolkit": "0.10.9"
|
|
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.127.1"
|
|
102
102
|
},
|
|
103
103
|
"peerDependencies": {
|
|
104
104
|
"@agent-native/core": ">=0.8.0",
|
|
@@ -12,6 +12,7 @@ import { getRequestUserEmail } from "@agent-native/core/server";
|
|
|
12
12
|
import {
|
|
13
13
|
discoverAgents,
|
|
14
14
|
getBuiltinAgents,
|
|
15
|
+
normalizeAgentId,
|
|
15
16
|
shouldIncludeRemoteAgentManifest,
|
|
16
17
|
} from "@agent-native/core/server/agent-discovery";
|
|
17
18
|
import { z } from "zod";
|
|
@@ -58,8 +59,12 @@ export default defineAction({
|
|
|
58
59
|
const manifest = parseRemoteAgentManifest(full.content, resource.path);
|
|
59
60
|
if (!manifest) continue;
|
|
60
61
|
if (!shouldIncludeRemoteAgentManifest(manifest, "dispatch")) continue;
|
|
61
|
-
|
|
62
|
-
|
|
62
|
+
// discoverAgents keys agents by the normalized id (image/images/asset
|
|
63
|
+
// all collapse to assets). Keying this map by the raw manifest id makes
|
|
64
|
+
// the id lookups below miss, so the same agent lands in the list twice.
|
|
65
|
+
const manifestId = normalizeAgentId(manifest.id);
|
|
66
|
+
if (builtinIds.has(manifestId)) continue;
|
|
67
|
+
customById.set(manifestId, {
|
|
63
68
|
resourceId: resource.id,
|
|
64
69
|
path: resource.path,
|
|
65
70
|
scope: resource.owner === SHARED_OWNER ? "shared" : "personal",
|
|
@@ -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>(
|
|
@@ -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
|
+
}
|
|
@@ -66,6 +66,9 @@ export interface WorkspaceAppSummary {
|
|
|
66
66
|
builderUrl?: string | null;
|
|
67
67
|
branchName?: string | null;
|
|
68
68
|
createdAt?: string | null;
|
|
69
|
+
createdBy?: string | null;
|
|
70
|
+
owner?: string | null;
|
|
71
|
+
teams?: string[];
|
|
69
72
|
agentCardUrl?: string | null;
|
|
70
73
|
agentCardReachable?: boolean;
|
|
71
74
|
a2aEndpointUrl?: string | null;
|
|
@@ -125,6 +128,9 @@ interface PendingWorkspaceApp {
|
|
|
125
128
|
contextId: string | null;
|
|
126
129
|
contextLabel: string | null;
|
|
127
130
|
audience?: WorkspaceAppAudience;
|
|
131
|
+
createdBy?: string | null;
|
|
132
|
+
owner?: string | null;
|
|
133
|
+
teams?: string[];
|
|
128
134
|
createdAt: string;
|
|
129
135
|
updatedAt: string;
|
|
130
136
|
expiresAt: string | null;
|
|
@@ -387,6 +393,12 @@ function applyWorkspaceAppMetadataOverride(
|
|
|
387
393
|
...app,
|
|
388
394
|
...(shouldApplyName ? { name } : {}),
|
|
389
395
|
...(shouldApplyDescription ? { description } : {}),
|
|
396
|
+
...(app.status === "pending" && !app.createdBy && override.updatedBy
|
|
397
|
+
? { createdBy: override.updatedBy }
|
|
398
|
+
: {}),
|
|
399
|
+
...(app.status === "pending" && !app.owner && override.updatedBy
|
|
400
|
+
? { owner: override.updatedBy }
|
|
401
|
+
: {}),
|
|
390
402
|
};
|
|
391
403
|
}
|
|
392
404
|
|
|
@@ -448,6 +460,75 @@ function normalizeWorkspaceAppPathList(value: unknown): string[] {
|
|
|
448
460
|
return Array.from(new Set(paths));
|
|
449
461
|
}
|
|
450
462
|
|
|
463
|
+
function normalizeWorkspaceAppTeams(value: unknown): string[] {
|
|
464
|
+
const rawTeams = Array.isArray(value)
|
|
465
|
+
? value
|
|
466
|
+
: typeof value === "string"
|
|
467
|
+
? value.split(",")
|
|
468
|
+
: [];
|
|
469
|
+
return Array.from(
|
|
470
|
+
new Set(
|
|
471
|
+
rawTeams
|
|
472
|
+
.map((team) => (typeof team === "string" ? team.trim() : ""))
|
|
473
|
+
.filter(Boolean),
|
|
474
|
+
),
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function workspaceAppMetadataFromRecord(
|
|
479
|
+
record: Record<string, any>,
|
|
480
|
+
): Pick<WorkspaceAppSummary, "createdAt" | "createdBy" | "owner" | "teams"> {
|
|
481
|
+
const config = record["agent-native"] ?? record.agentNative;
|
|
482
|
+
const workspaceConfig =
|
|
483
|
+
config && typeof config === "object" && !Array.isArray(config)
|
|
484
|
+
? (config.workspaceApp ?? config.workspace ?? config)
|
|
485
|
+
: {};
|
|
486
|
+
const metadata =
|
|
487
|
+
record.metadata &&
|
|
488
|
+
typeof record.metadata === "object" &&
|
|
489
|
+
!Array.isArray(record.metadata)
|
|
490
|
+
? record.metadata
|
|
491
|
+
: {};
|
|
492
|
+
const pick = (...values: unknown[]) =>
|
|
493
|
+
values.map(cleanOptionalText).find((value) => value !== undefined);
|
|
494
|
+
|
|
495
|
+
const teams = normalizeWorkspaceAppTeams(
|
|
496
|
+
record.teams ??
|
|
497
|
+
record.team ??
|
|
498
|
+
metadata.teams ??
|
|
499
|
+
metadata.team ??
|
|
500
|
+
workspaceConfig.teams ??
|
|
501
|
+
workspaceConfig.team,
|
|
502
|
+
);
|
|
503
|
+
return {
|
|
504
|
+
createdAt: pick(
|
|
505
|
+
record.createdAt,
|
|
506
|
+
record.created_at,
|
|
507
|
+
metadata.createdAt,
|
|
508
|
+
workspaceConfig.createdAt,
|
|
509
|
+
),
|
|
510
|
+
createdBy: pick(
|
|
511
|
+
record.createdBy,
|
|
512
|
+
record.createdByEmail,
|
|
513
|
+
record.created_by,
|
|
514
|
+
metadata.createdBy,
|
|
515
|
+
metadata.createdByEmail,
|
|
516
|
+
workspaceConfig.createdBy,
|
|
517
|
+
workspaceConfig.createdByEmail,
|
|
518
|
+
),
|
|
519
|
+
owner: pick(
|
|
520
|
+
record.owner,
|
|
521
|
+
record.ownerEmail,
|
|
522
|
+
record.owner_email,
|
|
523
|
+
metadata.owner,
|
|
524
|
+
metadata.ownerEmail,
|
|
525
|
+
workspaceConfig.owner,
|
|
526
|
+
workspaceConfig.ownerEmail,
|
|
527
|
+
),
|
|
528
|
+
teams,
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
|
|
451
532
|
function workspaceAppAudienceFromPackageJson(
|
|
452
533
|
pkg: unknown,
|
|
453
534
|
): WorkspaceAppAudience | undefined {
|
|
@@ -513,6 +594,7 @@ function parseWorkspaceAppsManifest(parsed: any): WorkspaceAppSummary[] | null {
|
|
|
513
594
|
const id = typeof entry.id === "string" ? entry.id.trim() : "";
|
|
514
595
|
const pathValue = typeof entry.path === "string" ? entry.path.trim() : "";
|
|
515
596
|
if (!id || !pathValue.startsWith("/")) return null;
|
|
597
|
+
const metadata = workspaceAppMetadataFromRecord(entry);
|
|
516
598
|
return {
|
|
517
599
|
id,
|
|
518
600
|
name:
|
|
@@ -534,6 +616,7 @@ function parseWorkspaceAppsManifest(parsed: any): WorkspaceAppSummary[] | null {
|
|
|
534
616
|
publicPaths: normalizeWorkspaceAppPathList(entry.publicPaths),
|
|
535
617
|
protectedPaths: normalizeWorkspaceAppPathList(entry.protectedPaths),
|
|
536
618
|
status: "ready",
|
|
619
|
+
...metadata,
|
|
537
620
|
} satisfies WorkspaceAppSummary;
|
|
538
621
|
})
|
|
539
622
|
.filter(
|
|
@@ -645,6 +728,9 @@ function parsePendingWorkspaceApps(value: unknown): PendingWorkspaceApp[] {
|
|
|
645
728
|
: null,
|
|
646
729
|
contextId: cleanOptionalString(record.contextId),
|
|
647
730
|
contextLabel: cleanOptionalString(record.contextLabel),
|
|
731
|
+
createdBy: cleanOptionalString(record.createdBy),
|
|
732
|
+
owner: cleanOptionalString(record.owner ?? record.ownerEmail),
|
|
733
|
+
teams: normalizeWorkspaceAppTeams(record.teams ?? record.team),
|
|
648
734
|
...(record.audience === undefined
|
|
649
735
|
? {}
|
|
650
736
|
: { audience: normalizeWorkspaceAppAudience(record.audience) }),
|
|
@@ -755,6 +841,9 @@ function pendingAppToSummary(app: PendingWorkspaceApp): WorkspaceAppSummary {
|
|
|
755
841
|
builderUrl: app.builderUrl,
|
|
756
842
|
branchName: app.branchName,
|
|
757
843
|
createdAt: app.createdAt,
|
|
844
|
+
createdBy: app.createdBy,
|
|
845
|
+
owner: app.owner,
|
|
846
|
+
teams: app.teams,
|
|
758
847
|
};
|
|
759
848
|
}
|
|
760
849
|
|
|
@@ -918,6 +1007,8 @@ async function recordPendingWorkspaceApp(input: {
|
|
|
918
1007
|
projectId: input.projectId,
|
|
919
1008
|
contextId: context?.id ?? null,
|
|
920
1009
|
contextLabel: context?.label ?? null,
|
|
1010
|
+
createdBy: currentOwnerEmail(),
|
|
1011
|
+
owner: currentOwnerEmail(),
|
|
921
1012
|
createdAt: existing?.createdAt || now,
|
|
922
1013
|
updatedAt: now,
|
|
923
1014
|
expiresAt: pendingWorkspaceAppExpiresAt(existing?.createdAt || now),
|
|
@@ -1038,6 +1129,7 @@ function readWorkspaceAppsFromFilesystem(
|
|
|
1038
1129
|
const pkg = readJson(path.join(appDir, "package.json"));
|
|
1039
1130
|
if (!pkg) return null;
|
|
1040
1131
|
const routeAccess = workspaceAppRouteAccessFromPackageJson(pkg);
|
|
1132
|
+
const metadata = workspaceAppMetadataFromRecord(pkg);
|
|
1041
1133
|
return {
|
|
1042
1134
|
id: entry.name,
|
|
1043
1135
|
name: pkg.displayName || titleCase(entry.name),
|
|
@@ -1051,6 +1143,7 @@ function readWorkspaceAppsFromFilesystem(
|
|
|
1051
1143
|
publicPaths: routeAccess.publicPaths,
|
|
1052
1144
|
protectedPaths: routeAccess.protectedPaths,
|
|
1053
1145
|
status: "ready",
|
|
1146
|
+
...metadata,
|
|
1054
1147
|
} satisfies WorkspaceAppSummary;
|
|
1055
1148
|
})
|
|
1056
1149
|
.filter((app): app is WorkspaceAppSummary => !!app)
|