@agent-native/dispatch 0.14.13 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/dist/actions/index.d.ts.map +1 -1
  2. package/dist/actions/index.js +4 -0
  3. package/dist/actions/index.js.map +1 -1
  4. package/dist/actions/list-curated-workspace-templates.d.ts +3 -0
  5. package/dist/actions/list-curated-workspace-templates.d.ts.map +1 -0
  6. package/dist/actions/list-curated-workspace-templates.js +10 -0
  7. package/dist/actions/list-curated-workspace-templates.js.map +1 -0
  8. package/dist/actions/remix-workspace-template.d.ts +60 -0
  9. package/dist/actions/remix-workspace-template.d.ts.map +1 -0
  10. package/dist/actions/remix-workspace-template.js +78 -0
  11. package/dist/actions/remix-workspace-template.js.map +1 -0
  12. package/dist/actions/upsert-destination.d.ts +12 -12
  13. package/dist/components/layout/Layout.d.ts.map +1 -1
  14. package/dist/components/layout/Layout.js +1 -0
  15. package/dist/components/layout/Layout.js.map +1 -1
  16. package/dist/components/workspace-template-card.d.ts +52 -0
  17. package/dist/components/workspace-template-card.d.ts.map +1 -0
  18. package/dist/components/workspace-template-card.js +104 -0
  19. package/dist/components/workspace-template-card.js.map +1 -0
  20. package/dist/hooks/use-navigation-state.d.ts.map +1 -1
  21. package/dist/hooks/use-navigation-state.js +4 -2
  22. package/dist/hooks/use-navigation-state.js.map +1 -1
  23. package/dist/lib/other-apps.d.ts +14 -0
  24. package/dist/lib/other-apps.d.ts.map +1 -0
  25. package/dist/lib/other-apps.js +30 -0
  26. package/dist/lib/other-apps.js.map +1 -0
  27. package/dist/routes/pages/apps.d.ts.map +1 -1
  28. package/dist/routes/pages/apps.js +37 -2
  29. package/dist/routes/pages/apps.js.map +1 -1
  30. package/dist/server/lib/app-creation-store.d.ts +1 -0
  31. package/dist/server/lib/app-creation-store.d.ts.map +1 -1
  32. package/dist/server/lib/app-creation-store.js +1 -1
  33. package/dist/server/lib/app-creation-store.js.map +1 -1
  34. package/dist/server/lib/curated-workspace-templates.d.ts +22 -0
  35. package/dist/server/lib/curated-workspace-templates.d.ts.map +1 -0
  36. package/dist/server/lib/curated-workspace-templates.js +137 -0
  37. package/dist/server/lib/curated-workspace-templates.js.map +1 -0
  38. package/package.json +2 -2
  39. package/src/actions/index.ts +4 -0
  40. package/src/actions/list-curated-workspace-templates.spec.ts +36 -0
  41. package/src/actions/list-curated-workspace-templates.ts +12 -0
  42. package/src/actions/remix-workspace-template.spec.ts +116 -0
  43. package/src/actions/remix-workspace-template.ts +99 -0
  44. package/src/components/layout/Layout.tsx +1 -0
  45. package/src/components/workspace-template-card.spec.tsx +174 -0
  46. package/src/components/workspace-template-card.tsx +361 -0
  47. package/src/hooks/use-navigation-state.ts +4 -1
  48. package/src/lib/other-apps.spec.ts +76 -0
  49. package/src/lib/other-apps.ts +44 -0
  50. package/src/routes/pages/apps.tsx +165 -0
  51. package/src/server/lib/app-creation-store.ts +1 -1
  52. package/src/server/lib/curated-workspace-templates.spec.ts +56 -0
  53. package/src/server/lib/curated-workspace-templates.ts +187 -0
@@ -1,9 +1,11 @@
1
1
  import { useActionQuery, useT } from "@agent-native/core/client";
2
2
  import {
3
3
  IconApps,
4
+ IconArrowUpRight,
4
5
  IconChevronDown,
5
6
  IconEyeOff,
6
7
  IconPlus,
8
+ IconStack2,
7
9
  } from "@tabler/icons-react";
8
10
  import { useState } from "react";
9
11
 
@@ -18,6 +20,15 @@ import {
18
20
  } from "../../components/ui/collapsible";
19
21
  import { Skeleton } from "../../components/ui/skeleton";
20
22
  import { WorkspaceAppCard } from "../../components/workspace-app-card";
23
+ import {
24
+ WorkspaceTemplatesSection,
25
+ type CuratedWorkspaceTemplatesResult,
26
+ type WorkspaceTemplateLabels,
27
+ } from "../../components/workspace-template-card";
28
+ import {
29
+ filterOtherApps,
30
+ type ConnectedAppSummary,
31
+ } from "../../lib/other-apps";
21
32
  import { cn } from "../../lib/utils";
22
33
  import type { WorkspaceAppSummary } from "../../lib/workspace-apps";
23
34
 
@@ -38,6 +49,11 @@ export default function AppsRoute() {
38
49
  includeAgentCards: false,
39
50
  includeArchived: true,
40
51
  });
52
+ const connectedAppsQuery = useActionQuery("list-connected-agents", {});
53
+ const curatedTemplatesQuery = useActionQuery(
54
+ "list-curated-workspace-templates",
55
+ {},
56
+ );
41
57
  const { data: apps = [], isLoading: appsLoading } = appsQuery;
42
58
  const { data: workspace } = useActionQuery(
43
59
  "get-workspace-info",
@@ -51,7 +67,25 @@ export default function AppsRoute() {
51
67
  );
52
68
  const visibleApps = allApps.filter((app) => !app.archived);
53
69
  const archivedApps = allApps.filter((app) => app.archived);
70
+ const otherApps = filterOtherApps(
71
+ (connectedAppsQuery.data || []) as ConnectedAppSummary[],
72
+ allApps,
73
+ );
54
74
  const showAppSkeletons = appsLoading && allApps.length === 0;
75
+ const templateLabels: WorkspaceTemplateLabels = {
76
+ appId: t("dispatch.pages.remixAppIdLabel"),
77
+ appIdDescription: t("dispatch.pages.remixAppIdDescription"),
78
+ cancel: t("dispatch.pages.cancel"),
79
+ integrationSetup: t("dispatch.pages.integrationSetup"),
80
+ installed: t("dispatch.pages.alreadyInWorkspace"),
81
+ remix: t("dispatch.pages.remix"),
82
+ remixing: t("dispatch.pages.remixing"),
83
+ remixSuccess: t("dispatch.pages.remixSuccess"),
84
+ remixError: t("dispatch.pages.remixError"),
85
+ appIdRequired: t("dispatch.pages.appIdRequired"),
86
+ source: t("dispatch.pages.source"),
87
+ viewLiveApp: t("dispatch.pages.viewLiveApp"),
88
+ };
55
89
 
56
90
  return (
57
91
  <DispatchShell
@@ -123,6 +157,65 @@ export default function AppsRoute() {
123
157
  )}
124
158
  </section>
125
159
 
160
+ {curatedTemplatesQuery.isError ? (
161
+ <section className="space-y-3 border-t pt-4">
162
+ <ActionQueryError
163
+ error={curatedTemplatesQuery.error}
164
+ onRetry={() => void curatedTemplatesQuery.refetch()}
165
+ />
166
+ </section>
167
+ ) : curatedTemplatesQuery.data ? (
168
+ <section className="space-y-3 border-t pt-4">
169
+ <div className="flex min-w-0 items-start gap-2">
170
+ <IconStack2
171
+ size={16}
172
+ className="mt-0.5 shrink-0 text-muted-foreground"
173
+ />
174
+ <div className="min-w-0">
175
+ <h2 className="truncate text-sm font-semibold text-foreground">
176
+ {t("dispatch.pages.curatedTemplates")}
177
+ </h2>
178
+ <p className="mt-0.5 text-xs text-muted-foreground">
179
+ {t("dispatch.pages.curatedTemplatesDescription")}
180
+ </p>
181
+ </div>
182
+ </div>
183
+ <WorkspaceTemplatesSection
184
+ templates={
185
+ curatedTemplatesQuery.data as CuratedWorkspaceTemplatesResult
186
+ }
187
+ labels={templateLabels}
188
+ onRemixSuccess={() => {
189
+ void appsQuery.refetch();
190
+ void curatedTemplatesQuery.refetch();
191
+ }}
192
+ />
193
+ </section>
194
+ ) : null}
195
+
196
+ {connectedAppsQuery.isError ? (
197
+ <section className="space-y-3 border-t pt-4">
198
+ <OtherAppsHeading count={0} />
199
+ <ActionQueryError
200
+ error={connectedAppsQuery.error}
201
+ onRetry={() => void connectedAppsQuery.refetch()}
202
+ />
203
+ </section>
204
+ ) : connectedAppsQuery.isLoading || otherApps.length > 0 ? (
205
+ <section className="space-y-3 border-t pt-4">
206
+ <OtherAppsHeading count={otherApps.length} />
207
+ {connectedAppsQuery.isLoading ? (
208
+ <OtherAppsSkeletonGrid />
209
+ ) : (
210
+ <div className="grid auto-rows-fr gap-3 md:grid-cols-2 xl:grid-cols-3">
211
+ {otherApps.map((app) => (
212
+ <OtherAppCard key={app.id} app={app} />
213
+ ))}
214
+ </div>
215
+ )}
216
+ </section>
217
+ ) : null}
218
+
126
219
  {archivedApps.length > 0 ? (
127
220
  <Collapsible open={showHidden} onOpenChange={setShowHidden}>
128
221
  <section className="space-y-3">
@@ -182,6 +275,78 @@ export default function AppsRoute() {
182
275
  );
183
276
  }
184
277
 
278
+ function OtherAppsHeading({ count }: { count: number }) {
279
+ const t = useT();
280
+ return (
281
+ <div className="flex min-w-0 items-start gap-2">
282
+ <IconStack2 size={16} className="mt-0.5 shrink-0 text-muted-foreground" />
283
+ <div className="min-w-0">
284
+ <h2 className="truncate text-sm font-semibold text-foreground">
285
+ {t("dispatch.pages.otherApps")}
286
+ </h2>
287
+ <p className="mt-0.5 text-xs text-muted-foreground">
288
+ {t("dispatch.pages.otherAppsDescription")}
289
+ {count > 0
290
+ ? ` · ${t("dispatch.pages.availableCount", { count })}`
291
+ : ""}
292
+ </p>
293
+ </div>
294
+ </div>
295
+ );
296
+ }
297
+
298
+ function OtherAppsSkeletonGrid() {
299
+ return (
300
+ <div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
301
+ {Array.from({ length: 3 }).map((_, index) => (
302
+ <div key={index} className="rounded-lg border bg-card p-4">
303
+ <div className="flex items-start gap-3">
304
+ <Skeleton className="size-8 rounded-md" />
305
+ <div className="flex-1 space-y-3">
306
+ <Skeleton className="h-4 w-32" />
307
+ <Skeleton className="h-3 w-full" />
308
+ <Skeleton className="h-3 w-2/3" />
309
+ </div>
310
+ </div>
311
+ </div>
312
+ ))}
313
+ </div>
314
+ );
315
+ }
316
+
317
+ function OtherAppCard({ app }: { app: ConnectedAppSummary }) {
318
+ const t = useT();
319
+ return (
320
+ <a
321
+ href={app.url}
322
+ target="_blank"
323
+ rel="noopener noreferrer"
324
+ className="group flex min-h-[116px] items-start gap-3 rounded-xl border border-border/60 bg-card/40 p-4 transition-[background-color,border-color] hover:border-foreground/20 hover:bg-accent/15 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
325
+ >
326
+ <span className="flex size-8 shrink-0 items-center justify-center rounded-md bg-muted text-xs font-semibold text-muted-foreground">
327
+ {app.name.charAt(0).toUpperCase()}
328
+ </span>
329
+ <span className="min-w-0 flex-1">
330
+ <span className="flex items-start justify-between gap-3">
331
+ <span className="truncate text-sm font-semibold text-foreground">
332
+ {app.name}
333
+ </span>
334
+ <IconArrowUpRight
335
+ size={15}
336
+ className="shrink-0 text-muted-foreground transition-transform group-hover:-translate-y-0.5 group-hover:translate-x-0.5"
337
+ />
338
+ </span>
339
+ <span className="mt-2 line-clamp-2 block text-[13px] leading-5 text-muted-foreground">
340
+ {app.description || app.url}
341
+ </span>
342
+ <span className="mt-3 block text-xs font-medium text-foreground">
343
+ {t("dispatch.pages.openApp")}
344
+ </span>
345
+ </span>
346
+ </a>
347
+ );
348
+ }
349
+
185
350
  function AppsSkeletonGrid() {
186
351
  return (
187
352
  <div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
@@ -1522,7 +1522,7 @@ function slugify(value: string): string {
1522
1522
  .slice(0, 64);
1523
1523
  }
1524
1524
 
1525
- function isLocalAppCreationRuntime(): boolean {
1525
+ export function isLocalAppCreationRuntime(): boolean {
1526
1526
  if (process.env.NODE_ENV === "production") return false;
1527
1527
  if (
1528
1528
  process.env.NETLIFY ||
@@ -0,0 +1,56 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+
3
+ const mocks = vi.hoisted(() => ({
4
+ listWorkspaceApps: vi.fn(),
5
+ }));
6
+
7
+ vi.mock("./app-creation-store.js", () => ({
8
+ listWorkspaceApps: mocks.listWorkspaceApps,
9
+ }));
10
+
11
+ import {
12
+ CURATED_WORKSPACE_TEMPLATES,
13
+ getCuratedWorkspaceTemplate,
14
+ listCuratedWorkspaceTemplates,
15
+ } from "./curated-workspace-templates.js";
16
+
17
+ describe("curated workspace templates", () => {
18
+ it("validates only the curated first-party template ids", () => {
19
+ expect(getCuratedWorkspaceTemplate(" MAIL ")).toEqual(
20
+ expect.objectContaining({ id: "mail", template: "mail" }),
21
+ );
22
+ expect(() => getCuratedWorkspaceTemplate("unknown")).toThrow(
23
+ 'Unknown curated workspace template "unknown".',
24
+ );
25
+ });
26
+
27
+ it("returns stable catalog metadata and current installation status", async () => {
28
+ mocks.listWorkspaceApps.mockResolvedValue([
29
+ { id: "dispatch" },
30
+ { id: "mail", archived: true },
31
+ { id: "custom-app" },
32
+ ]);
33
+
34
+ const result = await listCuratedWorkspaceTemplates();
35
+
36
+ expect(result).toHaveLength(10);
37
+ expect(result.map((template) => template.id)).toEqual(
38
+ CURATED_WORKSPACE_TEMPLATES.map((template) => template.id),
39
+ );
40
+ expect(result.find((template) => template.id === "mail")).toEqual(
41
+ expect.objectContaining({
42
+ name: "Mail",
43
+ template: "mail",
44
+ liveUrl: "https://mail.agent-native.com",
45
+ installed: true,
46
+ }),
47
+ );
48
+ expect(result.find((template) => template.id === "calendar")).toEqual(
49
+ expect.objectContaining({ installed: false }),
50
+ );
51
+ expect(mocks.listWorkspaceApps).toHaveBeenCalledWith({
52
+ includeAgentCards: false,
53
+ includeArchived: true,
54
+ });
55
+ });
56
+ });
@@ -0,0 +1,187 @@
1
+ import { listWorkspaceApps } from "./app-creation-store.js";
2
+
3
+ export interface CuratedWorkspaceTemplate {
4
+ id: string;
5
+ name: string;
6
+ description: string;
7
+ icon: string;
8
+ color: string;
9
+ template: string;
10
+ liveUrl: string;
11
+ category: string;
12
+ setupNote: string;
13
+ }
14
+
15
+ export interface CuratedWorkspaceTemplateStatus extends CuratedWorkspaceTemplate {
16
+ installed: boolean;
17
+ }
18
+
19
+ /**
20
+ * Stable first-party template metadata for the initial remix catalog.
21
+ * `liveUrl` identifies the product URL; it is not a public-demo claim.
22
+ */
23
+ export const CURATED_WORKSPACE_TEMPLATES: readonly CuratedWorkspaceTemplate[] =
24
+ [
25
+ {
26
+ id: "mail",
27
+ name: "Mail",
28
+ description:
29
+ "Agent-native email client with keyboard shortcuts and AI triage.",
30
+ icon: "Mail",
31
+ color: "#3B82F6",
32
+ template: "mail",
33
+ liveUrl: "https://mail.agent-native.com",
34
+ category: "communication",
35
+ setupNote:
36
+ "Connect the workspace email integration before working with live mail.",
37
+ },
38
+ {
39
+ id: "calendar",
40
+ name: "Calendar",
41
+ description:
42
+ "Manage events, synchronization, and public booking with an agent-native calendar.",
43
+ icon: "CalendarDays",
44
+ color: "#00B5FF",
45
+ template: "calendar",
46
+ liveUrl: "https://calendar.agent-native.com",
47
+ category: "productivity",
48
+ setupNote:
49
+ "Connect the workspace calendar integration before working with live events.",
50
+ },
51
+ {
52
+ id: "analytics",
53
+ name: "Analytics",
54
+ description:
55
+ "Connect data sources and prompt for charts, reports, and product insights.",
56
+ icon: "BarChart2",
57
+ color: "#F59E0B",
58
+ template: "analytics",
59
+ liveUrl: "https://analytics.agent-native.com",
60
+ category: "insights",
61
+ setupNote:
62
+ "Connect a data source or import a representative dataset before analysis.",
63
+ },
64
+ {
65
+ id: "slides",
66
+ name: "Slides",
67
+ description:
68
+ "Generate and edit React presentations with agent assistance.",
69
+ icon: "GalleryHorizontal",
70
+ color: "#EC4899",
71
+ template: "slides",
72
+ liveUrl: "https://slides.agent-native.com",
73
+ category: "creative",
74
+ setupNote:
75
+ "Start with a blank deck or connect the workspace presentation integration.",
76
+ },
77
+ {
78
+ id: "content",
79
+ name: "Content",
80
+ description:
81
+ "Write and organize local MDX content with agent assistance.",
82
+ icon: "FileText",
83
+ color: "#10B981",
84
+ template: "content",
85
+ liveUrl: "https://content.agent-native.com",
86
+ category: "content",
87
+ setupNote:
88
+ "Choose a content folder or create a first document for the private remix.",
89
+ },
90
+ {
91
+ id: "clips",
92
+ name: "Clips",
93
+ description:
94
+ "Record screens, capture meeting notes, and use voice dictation with AI.",
95
+ icon: "ScreenShare",
96
+ color: "#0EA5E9",
97
+ template: "clips",
98
+ liveUrl: "https://clips.agent-native.com",
99
+ category: "media",
100
+ setupNote:
101
+ "Grant screen and audio permissions only when the workspace needs recording.",
102
+ },
103
+ {
104
+ id: "brain",
105
+ name: "Brain",
106
+ description:
107
+ "Search cited company knowledge from conversations, meetings, and decisions.",
108
+ icon: "Brain",
109
+ color: "#8B5CF6",
110
+ template: "brain",
111
+ liveUrl: "https://brain.agent-native.com",
112
+ category: "knowledge",
113
+ setupNote:
114
+ "Connect approved knowledge sources before indexing workspace information.",
115
+ },
116
+ {
117
+ id: "assets",
118
+ name: "Assets",
119
+ description:
120
+ "Upload, organize, search, and generate on-brand images and videos.",
121
+ icon: "Photo",
122
+ color: "#0F766E",
123
+ template: "assets",
124
+ liveUrl: "https://assets.agent-native.com",
125
+ category: "content",
126
+ setupNote:
127
+ "Upload a representative asset or connect approved asset storage to begin.",
128
+ },
129
+ {
130
+ id: "forms",
131
+ name: "Forms",
132
+ description: "Create, edit, and manage forms with agent assistance.",
133
+ icon: "ClipboardList",
134
+ color: "#06B6D4",
135
+ template: "forms",
136
+ liveUrl: "https://forms.agent-native.com",
137
+ category: "operations",
138
+ setupNote:
139
+ "Create a form and choose its submission destination before inviting respondents.",
140
+ },
141
+ {
142
+ id: "design",
143
+ name: "Design",
144
+ description:
145
+ "Create and edit visual designs with agent-assisted exploration.",
146
+ icon: "Brush",
147
+ color: "#F472B6",
148
+ template: "design",
149
+ liveUrl: "https://design.agent-native.com",
150
+ category: "creative",
151
+ setupNote:
152
+ "Create a private project and optionally connect approved asset sources.",
153
+ },
154
+ ] as const;
155
+
156
+ const curatedTemplateById = new Map(
157
+ CURATED_WORKSPACE_TEMPLATES.map((template) => [template.id, template]),
158
+ );
159
+
160
+ export function getCuratedWorkspaceTemplate(
161
+ templateId: string,
162
+ ): CuratedWorkspaceTemplate {
163
+ const normalized = templateId.trim().toLowerCase();
164
+ const template = curatedTemplateById.get(normalized);
165
+ if (!template) {
166
+ throw new Error(`Unknown curated workspace template "${templateId}".`);
167
+ }
168
+ return template;
169
+ }
170
+
171
+ export async function listCuratedWorkspaceTemplates(): Promise<
172
+ CuratedWorkspaceTemplateStatus[]
173
+ > {
174
+ const installedIds = new Set(
175
+ (
176
+ await listWorkspaceApps({
177
+ includeAgentCards: false,
178
+ includeArchived: true,
179
+ })
180
+ ).map((app) => app.id.trim().toLowerCase()),
181
+ );
182
+
183
+ return CURATED_WORKSPACE_TEMPLATES.map((template) => ({
184
+ ...template,
185
+ installed: installedIds.has(template.id),
186
+ }));
187
+ }