@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.
Files changed (68) hide show
  1. package/dist/actions/get-agent-thread-debug.d.ts +7 -1
  2. package/dist/actions/get-agent-thread-debug.js +16 -3
  3. package/dist/actions/get-agent-thread-debug.js.map +1 -1
  4. package/dist/actions/list-connected-agents.js +7 -3
  5. package/dist/actions/list-connected-agents.js.map +1 -1
  6. package/dist/actions/list-curated-workspace-templates.js +1 -1
  7. package/dist/actions/list-curated-workspace-templates.js.map +1 -1
  8. package/dist/actions/provider-api-register.d.ts +14 -14
  9. package/dist/actions/remix-workspace-template.js +5 -5
  10. package/dist/actions/remix-workspace-template.js.map +1 -1
  11. package/dist/actions/search-agent-threads.js +1 -1
  12. package/dist/actions/search-agent-threads.js.map +1 -1
  13. package/dist/actions/start-workspace-app-creation.js +1 -1
  14. package/dist/actions/start-workspace-app-creation.js.map +1 -1
  15. package/dist/components/create-app-popover.js +2 -2
  16. package/dist/components/create-app-popover.js.map +1 -1
  17. package/dist/components/dispatch-control-plane.d.ts.map +1 -1
  18. package/dist/components/dispatch-control-plane.js +13 -3
  19. package/dist/components/dispatch-control-plane.js.map +1 -1
  20. package/dist/components/workspace-app-card.d.ts.map +1 -1
  21. package/dist/components/workspace-app-card.js +35 -4
  22. package/dist/components/workspace-app-card.js.map +1 -1
  23. package/dist/components/workspace-template-card.js +4 -4
  24. package/dist/components/workspace-template-card.js.map +1 -1
  25. package/dist/lib/workspace-apps.d.ts +3 -0
  26. package/dist/lib/workspace-apps.d.ts.map +1 -1
  27. package/dist/lib/workspace-apps.js.map +1 -1
  28. package/dist/routes/pages/apps.d.ts.map +1 -1
  29. package/dist/routes/pages/apps.js +19 -4
  30. package/dist/routes/pages/apps.js.map +1 -1
  31. package/dist/routes/pages/thread-debug.d.ts.map +1 -1
  32. package/dist/routes/pages/thread-debug.js +25 -11
  33. package/dist/routes/pages/thread-debug.js.map +1 -1
  34. package/dist/server/lib/app-creation-store.d.ts +3 -0
  35. package/dist/server/lib/app-creation-store.d.ts.map +1 -1
  36. package/dist/server/lib/app-creation-store.js +54 -2
  37. package/dist/server/lib/app-creation-store.js.map +1 -1
  38. package/dist/server/lib/curated-workspace-templates.d.ts +1 -1
  39. package/dist/server/lib/curated-workspace-templates.js +2 -2
  40. package/dist/server/lib/curated-workspace-templates.js.map +1 -1
  41. package/dist/server/lib/thread-debug-store.d.ts +7 -1
  42. package/dist/server/lib/thread-debug-store.d.ts.map +1 -1
  43. package/dist/server/lib/thread-debug-store.js +43 -5
  44. package/dist/server/lib/thread-debug-store.js.map +1 -1
  45. package/dist/server/plugins/integrations.js +1 -1
  46. package/dist/server/plugins/integrations.js.map +1 -1
  47. package/package.json +3 -3
  48. package/src/actions/get-agent-thread-debug.ts +30 -15
  49. package/src/actions/list-connected-agents.ts +7 -2
  50. package/src/actions/list-curated-workspace-templates.ts +1 -1
  51. package/src/actions/remix-workspace-template.spec.ts +2 -2
  52. package/src/actions/remix-workspace-template.ts +5 -5
  53. package/src/actions/search-agent-threads.ts +1 -1
  54. package/src/actions/start-workspace-app-creation.ts +1 -1
  55. package/src/components/create-app-popover.tsx +2 -2
  56. package/src/components/dispatch-control-plane.tsx +70 -4
  57. package/src/components/workspace-app-card.spec.tsx +43 -0
  58. package/src/components/workspace-app-card.tsx +97 -1
  59. package/src/components/workspace-template-card.spec.tsx +4 -4
  60. package/src/components/workspace-template-card.tsx +4 -4
  61. package/src/lib/workspace-apps.ts +3 -0
  62. package/src/routes/pages/apps.tsx +84 -4
  63. package/src/routes/pages/thread-debug.tsx +34 -14
  64. package/src/server/lib/app-creation-store.ts +95 -2
  65. package/src/server/lib/curated-workspace-templates.ts +2 -2
  66. package/src/server/lib/thread-debug-store.spec.ts +82 -0
  67. package/src/server/lib/thread-debug-store.ts +62 -6
  68. 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 remix without copying data or secrets", async () => {
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 remix of a curated first-party template",
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 remix of a curated first-party template.",
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 remix private to the current workspace, and do not create a public demo.",
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 remix of one of Dispatch's curated first-party workspace templates. Valid templates are mail, calendar, analytics, slides, content, clips, brain, assets, forms, and design. The remix 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.",
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 remix."),
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 remix of ${sourceTemplate.name}`,
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, or full persisted thread content. Non-admins are limited to their own current Dispatch DB threads.",
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 clone, wrap, or nest those templates inside the new app unless the user explicitly asks for a customized copy.',
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 clone first-party templates, 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 copy when the user explicitly asks for a customized fork/copy of that app; otherwise keep using the hosted/shared app so improvements to the base template keep flowing to users.`,
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 { IconArrowUpRight } from "@tabler/icons-react";
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 showSkeletons = isLoading && visibleApps.length === 0;
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
- ) : visibleApps.length > 0 ? (
179
+ ) : activeApps.length > 0 ? (
165
180
  <div className="grid gap-3 md:grid-cols-2">
166
- {visibleApps.map((app) => (
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-44">
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("remixes with the default app id and allows an override", async () => {
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("Remix into workspace"),
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 remixed into your workspace.",
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("Remix into workspace"),
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: "Remix into workspace",
82
- remixing: "Remixing…",
83
- remixSuccess: "Template remixed into your workspace.",
84
- remixError: "Could not remix this template",
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: visibleApps.length,
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
- {visibleApps.length > 0 ? (
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
- ) : visibleApps.length > 0 ? (
160
+ ) : activeApps.length > 0 ? (
152
161
  <div className="grid auto-rows-fr gap-3 md:grid-cols-2 xl:grid-cols-3">
153
- {visibleApps.map((app) => (
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
+ }