@agent-native/dispatch 0.3.0 → 0.5.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 (48) hide show
  1. package/dist/actions/archive-workspace-app.d.ts +3 -0
  2. package/dist/actions/archive-workspace-app.d.ts.map +1 -0
  3. package/dist/actions/archive-workspace-app.js +15 -0
  4. package/dist/actions/archive-workspace-app.js.map +1 -0
  5. package/dist/actions/index.d.ts.map +1 -1
  6. package/dist/actions/index.js +10 -0
  7. package/dist/actions/index.js.map +1 -1
  8. package/dist/actions/list-available-workspace-templates.d.ts +3 -0
  9. package/dist/actions/list-available-workspace-templates.d.ts.map +1 -0
  10. package/dist/actions/list-available-workspace-templates.js +10 -0
  11. package/dist/actions/list-available-workspace-templates.js.map +1 -0
  12. package/dist/actions/remove-pending-workspace-app.d.ts +3 -0
  13. package/dist/actions/remove-pending-workspace-app.d.ts.map +1 -0
  14. package/dist/actions/remove-pending-workspace-app.js +15 -0
  15. package/dist/actions/remove-pending-workspace-app.js.map +1 -0
  16. package/dist/actions/scaffold-workspace-app.d.ts +3 -0
  17. package/dist/actions/scaffold-workspace-app.d.ts.map +1 -0
  18. package/dist/actions/scaffold-workspace-app.js +27 -0
  19. package/dist/actions/scaffold-workspace-app.js.map +1 -0
  20. package/dist/actions/unarchive-workspace-app.d.ts +3 -0
  21. package/dist/actions/unarchive-workspace-app.d.ts.map +1 -0
  22. package/dist/actions/unarchive-workspace-app.js +15 -0
  23. package/dist/actions/unarchive-workspace-app.js.map +1 -0
  24. package/dist/components/workspace-app-card.d.ts.map +1 -1
  25. package/dist/components/workspace-app-card.js +33 -2
  26. package/dist/components/workspace-app-card.js.map +1 -1
  27. package/dist/lib/workspace-apps.d.ts +1 -0
  28. package/dist/lib/workspace-apps.d.ts.map +1 -1
  29. package/dist/lib/workspace-apps.js.map +1 -1
  30. package/dist/routes/pages/apps.d.ts.map +1 -1
  31. package/dist/routes/pages/apps.js +41 -8
  32. package/dist/routes/pages/apps.js.map +1 -1
  33. package/dist/server/lib/app-creation-store.d.ts +40 -0
  34. package/dist/server/lib/app-creation-store.d.ts.map +1 -1
  35. package/dist/server/lib/app-creation-store.js +245 -6
  36. package/dist/server/lib/app-creation-store.js.map +1 -1
  37. package/dist/server/lib/thread-debug-store.d.ts +2 -2
  38. package/package.json +2 -2
  39. package/src/actions/archive-workspace-app.ts +16 -0
  40. package/src/actions/index.ts +10 -0
  41. package/src/actions/list-available-workspace-templates.ts +11 -0
  42. package/src/actions/remove-pending-workspace-app.ts +16 -0
  43. package/src/actions/scaffold-workspace-app.ts +34 -0
  44. package/src/actions/unarchive-workspace-app.ts +16 -0
  45. package/src/components/workspace-app-card.tsx +95 -5
  46. package/src/lib/workspace-apps.ts +1 -0
  47. package/src/routes/pages/apps.tsx +159 -6
  48. package/src/server/lib/app-creation-store.ts +312 -15
@@ -1,6 +1,21 @@
1
- import { IconArrowUpRight, IconClockHour4 } from "@tabler/icons-react";
1
+ import { useActionMutation } from "@agent-native/core/client";
2
+ import {
3
+ IconArrowUpRight,
4
+ IconClockHour4,
5
+ IconDots,
6
+ IconEye,
7
+ IconEyeOff,
8
+ IconTrash,
9
+ } from "@tabler/icons-react";
10
+ import { toast } from "sonner";
2
11
  import { AppKeysPopover } from "@/components/app-keys-popover";
3
12
  import { Badge } from "@/components/ui/badge";
13
+ import {
14
+ DropdownMenu,
15
+ DropdownMenuContent,
16
+ DropdownMenuItem,
17
+ DropdownMenuTrigger,
18
+ } from "@/components/ui/dropdown-menu";
4
19
  import { cn } from "@/lib/utils";
5
20
  import {
6
21
  isPendingBuilderHref,
@@ -17,12 +32,41 @@ export function WorkspaceAppCard({
17
32
  }) {
18
33
  const href = workspaceAppHref(app);
19
34
  const openInNewTab = isPendingBuilderHref(app);
35
+ const isPending = app.status === "pending";
36
+ const isArchived = !!app.archived;
37
+
38
+ const archive = useActionMutation("archive-workspace-app", {
39
+ onError: (err) =>
40
+ toast.error(`Could not hide ${app.name}: ${stringifyError(err)}`),
41
+ });
42
+ const unarchive = useActionMutation("unarchive-workspace-app", {
43
+ onError: (err) =>
44
+ toast.error(`Could not restore ${app.name}: ${stringifyError(err)}`),
45
+ });
46
+ const removePending = useActionMutation("remove-pending-workspace-app", {
47
+ onError: (err) =>
48
+ toast.error(`Could not remove ${app.name}: ${stringifyError(err)}`),
49
+ });
50
+
51
+ const handleArchive = () => {
52
+ archive.mutate({ appId: app.id });
53
+ toast.success(`Hid ${app.name} from the Apps list`);
54
+ };
55
+ const handleUnarchive = () => {
56
+ unarchive.mutate({ appId: app.id });
57
+ toast.success(`Restored ${app.name} to the Apps list`);
58
+ };
59
+ const handleRemovePending = () => {
60
+ removePending.mutate({ appId: app.id });
61
+ toast.success(`Removed pending ${app.name}`);
62
+ };
20
63
 
21
64
  return (
22
65
  <div
23
66
  aria-disabled={!href}
24
67
  className={cn(
25
68
  "group relative rounded-lg border bg-card p-4 transition hover:border-foreground/30 aria-disabled:opacity-60",
69
+ isArchived && "opacity-70",
26
70
  className,
27
71
  )}
28
72
  >
@@ -42,7 +86,7 @@ export function WorkspaceAppCard({
42
86
  <h3 className="truncate text-sm font-semibold text-foreground">
43
87
  {app.name}
44
88
  </h3>
45
- {app.status === "pending" ? (
89
+ {isPending ? (
46
90
  <Badge
47
91
  variant="outline"
48
92
  className="shrink-0 gap-1 border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-300"
@@ -51,11 +95,17 @@ export function WorkspaceAppCard({
51
95
  Building
52
96
  </Badge>
53
97
  ) : null}
98
+ {isArchived ? (
99
+ <Badge variant="outline" className="shrink-0 gap-1">
100
+ <IconEyeOff size={12} />
101
+ Hidden
102
+ </Badge>
103
+ ) : null}
54
104
  </div>
55
105
  <p className="mt-1 truncate font-mono text-xs text-muted-foreground">
56
106
  {app.path}
57
107
  </p>
58
- {app.status === "pending" && app.branchName ? (
108
+ {isPending && app.branchName ? (
59
109
  <p className="mt-1 truncate text-xs text-muted-foreground">
60
110
  Branch: {app.branchName}
61
111
  </p>
@@ -67,12 +117,47 @@ export function WorkspaceAppCard({
67
117
  ) : null}
68
118
  </div>
69
119
  <div className="flex shrink-0 items-center gap-1">
70
- {app.status === "ready" ? (
120
+ {!isPending && !isArchived ? (
71
121
  <div className="pointer-events-auto">
72
122
  <AppKeysPopover appId={app.id} appName={app.name} />
73
123
  </div>
74
124
  ) : null}
75
- {href ? (
125
+ <div className="pointer-events-auto">
126
+ <DropdownMenu>
127
+ <DropdownMenuTrigger asChild>
128
+ <button
129
+ type="button"
130
+ aria-label={`More actions for ${app.name}`}
131
+ className="inline-flex h-7 w-7 cursor-pointer items-center justify-center rounded-md text-muted-foreground opacity-0 transition hover:bg-accent hover:text-foreground focus-visible:opacity-100 group-hover:opacity-100 data-[state=open]:opacity-100"
132
+ onClick={(e) => e.stopPropagation()}
133
+ >
134
+ <IconDots size={15} />
135
+ </button>
136
+ </DropdownMenuTrigger>
137
+ <DropdownMenuContent align="end" className="w-44">
138
+ {isPending ? (
139
+ <DropdownMenuItem
140
+ onSelect={handleRemovePending}
141
+ className="text-red-600 focus:text-red-600 dark:text-red-400 dark:focus:text-red-400"
142
+ >
143
+ <IconTrash size={14} className="mr-2" />
144
+ Remove from list
145
+ </DropdownMenuItem>
146
+ ) : isArchived ? (
147
+ <DropdownMenuItem onSelect={handleUnarchive}>
148
+ <IconEye size={14} className="mr-2" />
149
+ Restore to list
150
+ </DropdownMenuItem>
151
+ ) : (
152
+ <DropdownMenuItem onSelect={handleArchive}>
153
+ <IconEyeOff size={14} className="mr-2" />
154
+ Hide from list
155
+ </DropdownMenuItem>
156
+ )}
157
+ </DropdownMenuContent>
158
+ </DropdownMenu>
159
+ </div>
160
+ {href && !isArchived ? (
76
161
  <IconArrowUpRight
77
162
  size={16}
78
163
  className="text-muted-foreground transition group-hover:text-foreground"
@@ -83,3 +168,8 @@ export function WorkspaceAppCard({
83
168
  </div>
84
169
  );
85
170
  }
171
+
172
+ function stringifyError(err: unknown): string {
173
+ if (err instanceof Error) return err.message;
174
+ return String(err);
175
+ }
@@ -9,6 +9,7 @@ export interface WorkspaceAppSummary {
9
9
  statusLabel?: string;
10
10
  builderUrl?: string | null;
11
11
  branchName?: string | null;
12
+ archived?: boolean;
12
13
  }
13
14
 
14
15
  export function workspaceAppHref(app: WorkspaceAppSummary): string | null {
@@ -1,5 +1,23 @@
1
- import { useActionQuery } from "@agent-native/core/client";
2
- import { IconApps, IconPlus } from "@tabler/icons-react";
1
+ import { useState } from "react";
2
+ import { useActionMutation, useActionQuery } from "@agent-native/core/client";
3
+ import {
4
+ IconApps,
5
+ IconBrush,
6
+ IconCalendarMonth,
7
+ IconChartBar,
8
+ IconClipboardList,
9
+ IconEyeOff,
10
+ IconFileText,
11
+ IconLoader2,
12
+ IconMail,
13
+ IconPlus,
14
+ IconPresentation,
15
+ IconScreenShare,
16
+ IconSparkles,
17
+ IconStack3,
18
+ IconVideo,
19
+ } from "@tabler/icons-react";
20
+ import { toast } from "sonner";
3
21
  import { CreateAppPopover } from "@/components/create-app-popover";
4
22
  import { DispatchShell } from "@/components/dispatch-shell";
5
23
  import { WorkspaceAppCard } from "@/components/workspace-app-card";
@@ -16,10 +34,33 @@ interface WorkspaceInfo {
16
34
  appCount: number;
17
35
  }
18
36
 
37
+ interface AvailableTemplate {
38
+ name: string;
39
+ label: string;
40
+ hint: string;
41
+ icon: string;
42
+ color: string;
43
+ colorRgb: string;
44
+ core: boolean;
45
+ }
46
+
47
+ const TEMPLATE_ICONS: Record<string, typeof IconMail> = {
48
+ Mail: IconMail,
49
+ CalendarMonth: IconCalendarMonth,
50
+ FileText: IconFileText,
51
+ Presentation: IconPresentation,
52
+ ScreenShare: IconScreenShare,
53
+ ChartBar: IconChartBar,
54
+ ClipboardList: IconClipboardList,
55
+ Brush: IconBrush,
56
+ Video: IconVideo,
57
+ };
58
+
19
59
  export default function AppsRoute() {
60
+ const [showHidden, setShowHidden] = useState(false);
20
61
  const { data: apps = [] } = useActionQuery(
21
62
  "list-workspace-apps",
22
- { includeAgentCards: false },
63
+ { includeAgentCards: false, includeArchived: true },
23
64
  {
24
65
  refetchInterval: 2_000,
25
66
  },
@@ -29,11 +70,20 @@ export default function AppsRoute() {
29
70
  {},
30
71
  { staleTime: 60_000 },
31
72
  );
73
+ const { data: templates = [] } = useActionQuery(
74
+ "list-available-workspace-templates",
75
+ {},
76
+ { refetchInterval: 5_000 },
77
+ );
78
+
32
79
  const ws = workspace as WorkspaceInfo | undefined;
33
80
  const workspaceLabel = ws?.displayName ?? ws?.name ?? null;
34
- const typedApps = (apps as WorkspaceAppSummary[]).filter(
81
+ const allApps = (apps as WorkspaceAppSummary[]).filter(
35
82
  (app) => !app.isDispatch,
36
83
  );
84
+ const visibleApps = allApps.filter((app) => !app.archived);
85
+ const archivedApps = allApps.filter((app) => app.archived);
86
+ const typedTemplates = templates as AvailableTemplate[];
37
87
 
38
88
  return (
39
89
  <DispatchShell
@@ -44,7 +94,7 @@ export default function AppsRoute() {
44
94
  : "Open workspace apps and start new app creation from Dispatch."
45
95
  }
46
96
  >
47
- <div className="space-y-4">
97
+ <div className="space-y-6">
48
98
  <section className="space-y-3">
49
99
  <div className="flex items-center justify-between gap-3">
50
100
  <div className="flex items-center gap-2">
@@ -67,14 +117,117 @@ export default function AppsRoute() {
67
117
  </div>
68
118
 
69
119
  <div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
70
- {typedApps.map((app) => (
120
+ {visibleApps.map((app) => (
71
121
  <WorkspaceAppCard key={app.id} app={app} />
72
122
  ))}
73
123
 
74
124
  <CreateAppPopover />
75
125
  </div>
76
126
  </section>
127
+
128
+ {typedTemplates.length > 0 ? (
129
+ <section className="space-y-3">
130
+ <div className="flex items-center gap-2">
131
+ <IconStack3 size={16} className="text-muted-foreground" />
132
+ <h2 className="text-sm font-semibold text-foreground">
133
+ Add a template
134
+ </h2>
135
+ <span className="text-xs text-muted-foreground">
136
+ Scaffold a first-party app into{" "}
137
+ <code className="font-mono text-[11px]">apps/</code>.
138
+ </span>
139
+ </div>
140
+ <div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
141
+ {typedTemplates.map((template) => (
142
+ <AddTemplateCard key={template.name} template={template} />
143
+ ))}
144
+ </div>
145
+ </section>
146
+ ) : null}
147
+
148
+ {archivedApps.length > 0 ? (
149
+ <section className="space-y-3">
150
+ <button
151
+ type="button"
152
+ onClick={() => setShowHidden((cur) => !cur)}
153
+ className="inline-flex cursor-pointer items-center gap-2 text-xs text-muted-foreground hover:text-foreground"
154
+ >
155
+ <IconEyeOff size={14} />
156
+ {showHidden ? "Hide" : "Show"} {archivedApps.length} hidden{" "}
157
+ {archivedApps.length === 1 ? "app" : "apps"}
158
+ </button>
159
+ {showHidden ? (
160
+ <div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
161
+ {archivedApps.map((app) => (
162
+ <WorkspaceAppCard key={app.id} app={app} />
163
+ ))}
164
+ </div>
165
+ ) : null}
166
+ </section>
167
+ ) : null}
77
168
  </div>
78
169
  </DispatchShell>
79
170
  );
80
171
  }
172
+
173
+ function AddTemplateCard({ template }: { template: AvailableTemplate }) {
174
+ const Icon = TEMPLATE_ICONS[template.icon] ?? IconSparkles;
175
+ const scaffold = useActionMutation("scaffold-workspace-app", {
176
+ onSuccess: (result: any) => {
177
+ toast.success(
178
+ `Scaffolded apps/${result?.appId || template.name}. The gateway will pick it up shortly.`,
179
+ );
180
+ },
181
+ onError: (err) => {
182
+ toast.error(
183
+ `Could not scaffold ${template.label}: ${
184
+ err instanceof Error ? err.message : String(err)
185
+ }`,
186
+ );
187
+ },
188
+ });
189
+
190
+ return (
191
+ <div className="group relative flex items-start gap-3 rounded-lg border bg-card p-4 transition hover:border-foreground/30">
192
+ <div
193
+ className="flex h-9 w-9 shrink-0 items-center justify-center rounded-md"
194
+ style={{
195
+ backgroundColor: `rgb(${template.colorRgb} / 0.12)`,
196
+ color: template.color,
197
+ }}
198
+ >
199
+ <Icon size={18} />
200
+ </div>
201
+ <div className="min-w-0 flex-1">
202
+ <div className="flex min-w-0 items-center gap-2">
203
+ <h3 className="truncate text-sm font-semibold text-foreground">
204
+ {template.label}
205
+ </h3>
206
+ </div>
207
+ <p className="mt-1 line-clamp-2 text-xs leading-relaxed text-muted-foreground">
208
+ {template.hint}
209
+ </p>
210
+ <div className="mt-3">
211
+ <Button
212
+ size="sm"
213
+ variant="outline"
214
+ disabled={scaffold.isPending}
215
+ onClick={() => scaffold.mutate({ template: template.name })}
216
+ >
217
+ {scaffold.isPending ? (
218
+ <>
219
+ <IconLoader2 size={14} className="mr-1.5 animate-spin" />
220
+ Adding…
221
+ </>
222
+ ) : (
223
+ <>
224
+ <IconPlus size={14} className="mr-1.5" />
225
+ Add to workspace
226
+ </>
227
+ )}
228
+ </Button>
229
+ </div>
230
+ </div>
231
+ </div>
232
+ );
233
+ }