@agent-native/dispatch 0.8.5 → 0.8.7
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/ask_app.d.ts +3 -0
- package/dist/actions/ask_app.d.ts.map +1 -0
- package/dist/actions/ask_app.js +12 -0
- package/dist/actions/ask_app.js.map +1 -0
- package/dist/actions/create_embed_session.d.ts +3 -0
- package/dist/actions/create_embed_session.d.ts.map +1 -0
- package/dist/actions/create_embed_session.js +28 -0
- package/dist/actions/create_embed_session.js.map +1 -0
- package/dist/actions/index.d.ts.map +1 -1
- package/dist/actions/index.js +12 -0
- package/dist/actions/index.js.map +1 -1
- package/dist/actions/list-mcp-app-access.d.ts +3 -0
- package/dist/actions/list-mcp-app-access.d.ts.map +1 -0
- package/dist/actions/list-mcp-app-access.js +25 -0
- package/dist/actions/list-mcp-app-access.js.map +1 -0
- package/dist/actions/list_apps.d.ts +3 -0
- package/dist/actions/list_apps.d.ts.map +1 -0
- package/dist/actions/list_apps.js +26 -0
- package/dist/actions/list_apps.js.map +1 -0
- package/dist/actions/open_app.d.ts +3 -0
- package/dist/actions/open_app.d.ts.map +1 -0
- package/dist/actions/open_app.js +62 -0
- package/dist/actions/open_app.js.map +1 -0
- package/dist/actions/set-mcp-app-access.d.ts +3 -0
- package/dist/actions/set-mcp-app-access.d.ts.map +1 -0
- package/dist/actions/set-mcp-app-access.js +46 -0
- package/dist/actions/set-mcp-app-access.js.map +1 -0
- package/dist/actions/start-workspace-app-creation.js +1 -1
- package/dist/actions/start-workspace-app-creation.js.map +1 -1
- package/dist/actions/view-screen.d.ts.map +1 -1
- package/dist/actions/view-screen.js +8 -0
- package/dist/actions/view-screen.js.map +1 -1
- package/dist/components/create-app-popover.d.ts.map +1 -1
- package/dist/components/create-app-popover.js +1 -0
- package/dist/components/create-app-popover.js.map +1 -1
- package/dist/components/workspace-app-card.d.ts.map +1 -1
- package/dist/components/workspace-app-card.js +2 -1
- package/dist/components/workspace-app-card.js.map +1 -1
- package/dist/routes/pages/agents.d.ts.map +1 -1
- package/dist/routes/pages/agents.js +74 -3
- package/dist/routes/pages/agents.js.map +1 -1
- package/dist/routes/pages/apps.d.ts.map +1 -1
- package/dist/routes/pages/apps.js +23 -8
- package/dist/routes/pages/apps.js.map +1 -1
- package/dist/routes/pages/metrics.d.ts.map +1 -1
- package/dist/routes/pages/metrics.js +3 -1
- package/dist/routes/pages/metrics.js.map +1 -1
- package/dist/routes/pages/overview.d.ts.map +1 -1
- package/dist/routes/pages/overview.js +1 -3
- package/dist/routes/pages/overview.js.map +1 -1
- package/dist/server/lib/app-creation-store.d.ts.map +1 -1
- package/dist/server/lib/app-creation-store.js +104 -10
- package/dist/server/lib/app-creation-store.js.map +1 -1
- package/dist/server/lib/mcp-access-store.d.ts +16 -0
- package/dist/server/lib/mcp-access-store.d.ts.map +1 -0
- package/dist/server/lib/mcp-access-store.js +64 -0
- package/dist/server/lib/mcp-access-store.js.map +1 -0
- package/dist/server/lib/mcp-gateway.d.ts +47 -0
- package/dist/server/lib/mcp-gateway.d.ts.map +1 -0
- package/dist/server/lib/mcp-gateway.js +393 -0
- package/dist/server/lib/mcp-gateway.js.map +1 -0
- package/dist/server/lib/usage-metrics-store.d.ts +1 -0
- package/dist/server/lib/usage-metrics-store.d.ts.map +1 -1
- package/dist/server/lib/usage-metrics-store.js +1 -0
- package/dist/server/lib/usage-metrics-store.js.map +1 -1
- package/dist/server/plugins/agent-chat.d.ts.map +1 -1
- package/dist/server/plugins/agent-chat.js +1 -0
- package/dist/server/plugins/agent-chat.js.map +1 -1
- package/dist/server/plugins/integrations.d.ts.map +1 -1
- package/dist/server/plugins/integrations.js +2 -1
- package/dist/server/plugins/integrations.js.map +1 -1
- package/package.json +1 -1
- package/src/actions/ask_app.ts +13 -0
- package/src/actions/create_embed_session.ts +29 -0
- package/src/actions/index.spec.ts +6 -0
- package/src/actions/index.ts +12 -0
- package/src/actions/list-mcp-app-access.ts +26 -0
- package/src/actions/list_apps.ts +27 -0
- package/src/actions/open_app.ts +68 -0
- package/src/actions/set-mcp-app-access.ts +59 -0
- package/src/actions/start-workspace-app-creation.ts +1 -1
- package/src/actions/view-screen.ts +8 -0
- package/src/components/create-app-popover.tsx +1 -0
- package/src/components/workspace-app-card.tsx +3 -2
- package/src/routes/pages/agents.tsx +187 -5
- package/src/routes/pages/apps.tsx +209 -67
- package/src/routes/pages/metrics.tsx +4 -1
- package/src/routes/pages/overview.tsx +16 -10
- package/src/server/lib/app-creation-store.spec.ts +240 -0
- package/src/server/lib/app-creation-store.ts +130 -11
- package/src/server/lib/mcp-access-store.spec.ts +58 -0
- package/src/server/lib/mcp-access-store.ts +104 -0
- package/src/server/lib/mcp-gateway.spec.ts +295 -0
- package/src/server/lib/mcp-gateway.ts +516 -0
- package/src/server/lib/usage-metrics-store.ts +2 -0
- package/src/server/plugins/agent-chat.ts +1 -0
- package/src/server/plugins/integrations.ts +2 -1
|
@@ -74,6 +74,14 @@ export default defineAction({
|
|
|
74
74
|
if (navigation?.view === "destinations") {
|
|
75
75
|
screen.recentDestinations = overview.recentDestinations;
|
|
76
76
|
}
|
|
77
|
+
if (navigation?.view === "agents") {
|
|
78
|
+
const [connectedAgents, mcpAccess] = await Promise.all([
|
|
79
|
+
runLocalDispatchAction("list-connected-agents", {}),
|
|
80
|
+
runLocalDispatchAction("list-mcp-app-access", {}),
|
|
81
|
+
]);
|
|
82
|
+
screen.connectedAgents = connectedAgents;
|
|
83
|
+
screen.mcpAppAccess = mcpAccess;
|
|
84
|
+
}
|
|
77
85
|
if (
|
|
78
86
|
navigation?.view === "overview" ||
|
|
79
87
|
navigation?.view === "metrics" ||
|
|
@@ -111,6 +111,7 @@ function buildAppCreationPrompt(input: {
|
|
|
111
111
|
`Dispatch workspace resources with scope=all are inherited workspace context. Do not copy or sync them into the new app; every workspace app reads them at runtime and may override with app shared or personal resources.`,
|
|
112
112
|
``,
|
|
113
113
|
`Pick a starter template that fits the user's prompt — analytics, brain, calendar, content, design, dispatch, forms, mail, slides, clips, or starter when none of the others fit.`,
|
|
114
|
+
`If you use the starter template, treat it as scaffolding only: the finished app must use the requested app's real name, home screen, navigation, package metadata, and manifest, and it must not leave visible "Starter", "Blank app", or "New app" UI behind.`,
|
|
114
115
|
`Use the workspace app layout: create it under apps/${input.appId}, mount it at /${input.appId}, keep it on the shared workspace database/hosting model, and avoid table-name collisions by namespacing any new domain tables to the app.`,
|
|
115
116
|
`Important routing rule: from outside the app, link to /${input.appId}; inside apps/${input.appId}, React Router routes are app-local. Use <Link to="/review"> and navigate("/review"), not "/${input.appId}/review"; APP_BASE_PATH supplies the mounted prefix, and hardcoding it causes doubled URLs like /${input.appId}/${input.appId}/review.`,
|
|
116
117
|
`Prefer useActionQuery/useActionMutation for actions. If you must raw-fetch framework endpoints, wrap them with agentNativePath("/_agent-native/actions/<name>") so mounted apps call the right URL.`,
|
|
@@ -53,6 +53,7 @@ export function WorkspaceAppCard({
|
|
|
53
53
|
const href = workspaceAppHref(app);
|
|
54
54
|
const openInNewTab = isPendingBuilderHref(app);
|
|
55
55
|
const isPending = app.status === "pending";
|
|
56
|
+
const pendingLabel = app.statusLabel || "Builder branch";
|
|
56
57
|
const isArchived = !!app.archived;
|
|
57
58
|
const audience = app.audience ?? "internal";
|
|
58
59
|
const [editOpen, setEditOpen] = useState(false);
|
|
@@ -145,7 +146,7 @@ export function WorkspaceAppCard({
|
|
|
145
146
|
className="shrink-0 gap-1 border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-300"
|
|
146
147
|
>
|
|
147
148
|
<IconClockHour4 size={12} />
|
|
148
|
-
|
|
149
|
+
{pendingLabel}
|
|
149
150
|
</Badge>
|
|
150
151
|
) : null}
|
|
151
152
|
{isArchived ? (
|
|
@@ -166,7 +167,7 @@ export function WorkspaceAppCard({
|
|
|
166
167
|
</p>
|
|
167
168
|
{isPending && app.branchName ? (
|
|
168
169
|
<p className="mt-1 truncate text-xs text-muted-foreground">
|
|
169
|
-
|
|
170
|
+
Builder branch: {app.branchName}
|
|
170
171
|
</p>
|
|
171
172
|
) : null}
|
|
172
173
|
{app.description ? (
|
|
@@ -1,11 +1,190 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useMemo, useState } from "react";
|
|
2
|
+
import {
|
|
3
|
+
agentNativePath,
|
|
4
|
+
useActionMutation,
|
|
5
|
+
useActionQuery,
|
|
6
|
+
} from "@agent-native/core/client";
|
|
2
7
|
import { AgentsPanel, type ConnectedAgent } from "@/components/agents-panel";
|
|
3
8
|
import { DispatchShell } from "@/components/dispatch-shell";
|
|
9
|
+
import { Button } from "@/components/ui/button";
|
|
10
|
+
import { Input } from "@/components/ui/input";
|
|
11
|
+
import { Switch } from "@/components/ui/switch";
|
|
12
|
+
import { IconCheck, IconCopy, IconPlugConnected } from "@tabler/icons-react";
|
|
13
|
+
import { toast } from "sonner";
|
|
4
14
|
|
|
5
15
|
export function meta() {
|
|
6
16
|
return [{ title: "Agents — Dispatch" }];
|
|
7
17
|
}
|
|
8
18
|
|
|
19
|
+
interface McpAccessApp {
|
|
20
|
+
id: string;
|
|
21
|
+
name: string;
|
|
22
|
+
description: string;
|
|
23
|
+
url: string;
|
|
24
|
+
color: string;
|
|
25
|
+
granted: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type McpAccessMode = "all-apps" | "selected-apps";
|
|
29
|
+
|
|
30
|
+
interface McpAccessState {
|
|
31
|
+
mode: McpAccessMode;
|
|
32
|
+
selectedAppIds: string[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function dispatchMcpUrl(): string {
|
|
36
|
+
const path = agentNativePath("/_agent-native/mcp");
|
|
37
|
+
if (typeof window === "undefined") return path;
|
|
38
|
+
return new URL(path, window.location.origin).href;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function DispatchMcpAccessPanel() {
|
|
42
|
+
const { data, isLoading } = useActionQuery("list-mcp-app-access", {});
|
|
43
|
+
const [optimistic, setOptimistic] = useState<McpAccessState | null>(null);
|
|
44
|
+
const saveAccess = useActionMutation("set-mcp-app-access", {
|
|
45
|
+
onSuccess: () => {
|
|
46
|
+
setOptimistic(null);
|
|
47
|
+
toast.success("MCP app access updated");
|
|
48
|
+
},
|
|
49
|
+
onError: (error) => {
|
|
50
|
+
setOptimistic(null);
|
|
51
|
+
toast.error(error.message);
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const apps = ((data as { apps?: McpAccessApp[] } | undefined)?.apps ??
|
|
56
|
+
[]) as McpAccessApp[];
|
|
57
|
+
const access =
|
|
58
|
+
optimistic ??
|
|
59
|
+
({
|
|
60
|
+
mode: ((data as { mode?: McpAccessMode } | undefined)?.mode ??
|
|
61
|
+
"all-apps") as McpAccessMode,
|
|
62
|
+
selectedAppIds:
|
|
63
|
+
(data as { selectedAppIds?: string[] } | undefined)?.selectedAppIds ??
|
|
64
|
+
[],
|
|
65
|
+
} satisfies McpAccessState);
|
|
66
|
+
const selected = useMemo(
|
|
67
|
+
() => new Set(access.selectedAppIds),
|
|
68
|
+
[access.selectedAppIds],
|
|
69
|
+
);
|
|
70
|
+
const grantedCount =
|
|
71
|
+
access.mode === "all-apps" ? apps.length : access.selectedAppIds.length;
|
|
72
|
+
const mcpUrl = dispatchMcpUrl();
|
|
73
|
+
|
|
74
|
+
function persist(next: McpAccessState) {
|
|
75
|
+
if (next.mode === "selected-apps" && next.selectedAppIds.length === 0) {
|
|
76
|
+
toast.error("Select at least one app, or expose all apps.");
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
setOptimistic(next);
|
|
80
|
+
saveAccess.mutate(next);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function toggleApp(appId: string) {
|
|
84
|
+
const next = selected.has(appId)
|
|
85
|
+
? access.selectedAppIds.filter((id) => id !== appId)
|
|
86
|
+
: [...access.selectedAppIds, appId];
|
|
87
|
+
persist({ mode: "selected-apps", selectedAppIds: next });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function copyUrl() {
|
|
91
|
+
try {
|
|
92
|
+
await navigator.clipboard.writeText(mcpUrl);
|
|
93
|
+
toast.success("MCP URL copied");
|
|
94
|
+
} catch {
|
|
95
|
+
toast.error("Could not copy MCP URL");
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<section className="rounded-2xl border bg-card p-5">
|
|
101
|
+
<div className="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
|
|
102
|
+
<div className="min-w-0">
|
|
103
|
+
<div className="flex items-center gap-2 text-sm font-medium text-foreground">
|
|
104
|
+
<IconPlugConnected size={16} />
|
|
105
|
+
Unified MCP gateway
|
|
106
|
+
</div>
|
|
107
|
+
<div className="mt-1 max-w-2xl text-sm text-muted-foreground">
|
|
108
|
+
Connect external agents to Dispatch once, then route to granted
|
|
109
|
+
workspace apps through <code>list_apps</code>, <code>ask_app</code>,
|
|
110
|
+
and <code>open_app</code>.
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
<div className="flex items-center gap-3 rounded-xl border px-3 py-2">
|
|
114
|
+
<div>
|
|
115
|
+
<div className="text-xs font-medium text-foreground">
|
|
116
|
+
{access.mode === "all-apps" ? "All apps" : "Selected apps"}
|
|
117
|
+
</div>
|
|
118
|
+
<div className="text-xs text-muted-foreground">
|
|
119
|
+
{isLoading ? "Loading" : `${grantedCount} granted`}
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
<Switch
|
|
123
|
+
checked={access.mode === "all-apps"}
|
|
124
|
+
disabled={saveAccess.isPending || apps.length === 0}
|
|
125
|
+
onCheckedChange={(checked) =>
|
|
126
|
+
persist({
|
|
127
|
+
mode: checked ? "all-apps" : "selected-apps",
|
|
128
|
+
selectedAppIds: checked
|
|
129
|
+
? access.selectedAppIds
|
|
130
|
+
: apps.map((app) => app.id),
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
aria-label="Expose all apps through Dispatch MCP"
|
|
134
|
+
/>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
<div className="mt-4 flex flex-col gap-2 sm:flex-row">
|
|
139
|
+
<Input readOnly value={mcpUrl} className="font-mono text-xs" />
|
|
140
|
+
<Button type="button" variant="outline" onClick={copyUrl}>
|
|
141
|
+
<IconCopy size={15} />
|
|
142
|
+
Copy URL
|
|
143
|
+
</Button>
|
|
144
|
+
</div>
|
|
145
|
+
|
|
146
|
+
{access.mode === "selected-apps" ? (
|
|
147
|
+
<div className="mt-4 grid gap-2 sm:grid-cols-2 xl:grid-cols-3">
|
|
148
|
+
{apps.map((app) => {
|
|
149
|
+
const isSelected = selected.has(app.id);
|
|
150
|
+
return (
|
|
151
|
+
<button
|
|
152
|
+
key={app.id}
|
|
153
|
+
type="button"
|
|
154
|
+
disabled={
|
|
155
|
+
saveAccess.isPending &&
|
|
156
|
+
optimistic?.selectedAppIds.includes(app.id) !== isSelected
|
|
157
|
+
}
|
|
158
|
+
onClick={() => toggleApp(app.id)}
|
|
159
|
+
className="flex min-h-[76px] items-start gap-3 rounded-xl border bg-muted/20 px-3 py-3 text-left transition hover:bg-muted/40 disabled:cursor-not-allowed disabled:opacity-60"
|
|
160
|
+
aria-pressed={isSelected}
|
|
161
|
+
>
|
|
162
|
+
<span
|
|
163
|
+
className="mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-xs font-bold text-white"
|
|
164
|
+
style={{ backgroundColor: app.color }}
|
|
165
|
+
>
|
|
166
|
+
{app.name.charAt(0).toUpperCase()}
|
|
167
|
+
</span>
|
|
168
|
+
<span className="min-w-0 flex-1">
|
|
169
|
+
<span className="flex items-center gap-2 text-sm font-medium text-foreground">
|
|
170
|
+
{app.name}
|
|
171
|
+
{isSelected ? (
|
|
172
|
+
<IconCheck size={14} className="text-emerald-500" />
|
|
173
|
+
) : null}
|
|
174
|
+
</span>
|
|
175
|
+
<span className="mt-1 line-clamp-2 block text-xs text-muted-foreground">
|
|
176
|
+
{app.description || app.url}
|
|
177
|
+
</span>
|
|
178
|
+
</span>
|
|
179
|
+
</button>
|
|
180
|
+
);
|
|
181
|
+
})}
|
|
182
|
+
</div>
|
|
183
|
+
) : null}
|
|
184
|
+
</section>
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
9
188
|
export default function AgentsRoute() {
|
|
10
189
|
const { data, refetch } = useActionQuery("list-connected-agents", {});
|
|
11
190
|
|
|
@@ -14,10 +193,13 @@ export default function AgentsRoute() {
|
|
|
14
193
|
title="Agents"
|
|
15
194
|
description="Dispatch can delegate to the built-in app suite over A2A by default. Add extra agents here only if you want to route work to apps outside that built-in set."
|
|
16
195
|
>
|
|
17
|
-
<
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
196
|
+
<div className="space-y-4">
|
|
197
|
+
<DispatchMcpAccessPanel />
|
|
198
|
+
<AgentsPanel
|
|
199
|
+
agents={(data || []) as ConnectedAgent[]}
|
|
200
|
+
onRefresh={refetch}
|
|
201
|
+
/>
|
|
202
|
+
</div>
|
|
21
203
|
</DispatchShell>
|
|
22
204
|
);
|
|
23
205
|
}
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
IconBrush,
|
|
6
6
|
IconCalendarMonth,
|
|
7
7
|
IconChartBar,
|
|
8
|
+
IconChevronDown,
|
|
8
9
|
IconClipboardList,
|
|
9
10
|
IconEyeOff,
|
|
10
11
|
IconFileText,
|
|
@@ -22,6 +23,13 @@ import { CreateAppPopover } from "@/components/create-app-popover";
|
|
|
22
23
|
import { DispatchShell } from "@/components/dispatch-shell";
|
|
23
24
|
import { WorkspaceAppCard } from "@/components/workspace-app-card";
|
|
24
25
|
import { Button } from "@/components/ui/button";
|
|
26
|
+
import {
|
|
27
|
+
Collapsible,
|
|
28
|
+
CollapsibleContent,
|
|
29
|
+
CollapsibleTrigger,
|
|
30
|
+
} from "@/components/ui/collapsible";
|
|
31
|
+
import { Skeleton } from "@/components/ui/skeleton";
|
|
32
|
+
import { cn } from "@/lib/utils";
|
|
25
33
|
import type { WorkspaceAppSummary } from "@/lib/workspace-apps";
|
|
26
34
|
|
|
27
35
|
export function meta() {
|
|
@@ -58,7 +66,8 @@ const TEMPLATE_ICONS: Record<string, typeof IconMail> = {
|
|
|
58
66
|
|
|
59
67
|
export default function AppsRoute() {
|
|
60
68
|
const [showHidden, setShowHidden] = useState(false);
|
|
61
|
-
const
|
|
69
|
+
const [templatesOpen, setTemplatesOpen] = useState(false);
|
|
70
|
+
const { data: apps = [], isLoading: appsLoading } = useActionQuery(
|
|
62
71
|
"list-workspace-apps",
|
|
63
72
|
{ includeAgentCards: false, includeArchived: true },
|
|
64
73
|
{
|
|
@@ -70,7 +79,7 @@ export default function AppsRoute() {
|
|
|
70
79
|
{},
|
|
71
80
|
{ staleTime: 60_000 },
|
|
72
81
|
);
|
|
73
|
-
const { data: templates = [] } = useActionQuery(
|
|
82
|
+
const { data: templates = [], isLoading: templatesLoading } = useActionQuery(
|
|
74
83
|
"list-available-workspace-templates",
|
|
75
84
|
{},
|
|
76
85
|
{ refetchInterval: 5_000 },
|
|
@@ -84,6 +93,7 @@ export default function AppsRoute() {
|
|
|
84
93
|
const visibleApps = allApps.filter((app) => !app.archived);
|
|
85
94
|
const archivedApps = allApps.filter((app) => app.archived);
|
|
86
95
|
const typedTemplates = templates as AvailableTemplate[];
|
|
96
|
+
const showAppSkeletons = appsLoading && allApps.length === 0;
|
|
87
97
|
|
|
88
98
|
return (
|
|
89
99
|
<DispatchShell
|
|
@@ -94,82 +104,214 @@ export default function AppsRoute() {
|
|
|
94
104
|
: "Open workspace apps and start new app creation from Dispatch."
|
|
95
105
|
}
|
|
96
106
|
>
|
|
97
|
-
<div className="space-y-
|
|
107
|
+
<div className="space-y-8">
|
|
98
108
|
<section className="space-y-3">
|
|
99
|
-
<div className="flex items-
|
|
100
|
-
<div className="flex items-
|
|
101
|
-
<IconApps
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
109
|
+
<div className="flex flex-wrap items-end justify-between gap-3">
|
|
110
|
+
<div className="flex min-w-0 items-start gap-2">
|
|
111
|
+
<IconApps
|
|
112
|
+
size={16}
|
|
113
|
+
className="mt-0.5 shrink-0 text-muted-foreground"
|
|
114
|
+
/>
|
|
115
|
+
<div className="min-w-0">
|
|
116
|
+
<h2 className="truncate text-sm font-semibold text-foreground">
|
|
117
|
+
{workspaceLabel
|
|
118
|
+
? `Apps in ${workspaceLabel}`
|
|
119
|
+
: "Workspace apps"}
|
|
120
|
+
</h2>
|
|
121
|
+
<p className="mt-0.5 text-xs text-muted-foreground">
|
|
122
|
+
{visibleApps.length} active
|
|
123
|
+
{archivedApps.length > 0
|
|
124
|
+
? ` · ${archivedApps.length} hidden`
|
|
125
|
+
: ""}
|
|
126
|
+
</p>
|
|
127
|
+
</div>
|
|
107
128
|
</div>
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
<
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
|
|
120
|
-
{visibleApps.map((app) => (
|
|
121
|
-
<WorkspaceAppCard key={app.id} app={app} />
|
|
122
|
-
))}
|
|
123
|
-
|
|
124
|
-
<CreateAppPopover />
|
|
129
|
+
{visibleApps.length > 0 ? (
|
|
130
|
+
<CreateAppPopover
|
|
131
|
+
align="end"
|
|
132
|
+
trigger={
|
|
133
|
+
<Button size="sm">
|
|
134
|
+
<IconPlus size={15} className="mr-1.5" />
|
|
135
|
+
Create app
|
|
136
|
+
</Button>
|
|
137
|
+
}
|
|
138
|
+
/>
|
|
139
|
+
) : null}
|
|
125
140
|
</div>
|
|
126
|
-
</section>
|
|
127
141
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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} />
|
|
142
|
+
{showAppSkeletons ? (
|
|
143
|
+
<AppsSkeletonGrid />
|
|
144
|
+
) : visibleApps.length > 0 ? (
|
|
145
|
+
<div className="grid auto-rows-fr gap-3 md:grid-cols-2 xl:grid-cols-3">
|
|
146
|
+
{visibleApps.map((app) => (
|
|
147
|
+
<WorkspaceAppCard key={app.id} app={app} className="h-full" />
|
|
143
148
|
))}
|
|
144
149
|
</div>
|
|
145
|
-
|
|
150
|
+
) : (
|
|
151
|
+
<EmptyAppsState />
|
|
152
|
+
)}
|
|
153
|
+
</section>
|
|
154
|
+
|
|
155
|
+
{typedTemplates.length > 0 || templatesLoading ? (
|
|
156
|
+
<Collapsible open={templatesOpen} onOpenChange={setTemplatesOpen}>
|
|
157
|
+
<section className="space-y-3">
|
|
158
|
+
<div className="flex flex-wrap items-center justify-between gap-3 border-t pt-4">
|
|
159
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
160
|
+
<IconStack3
|
|
161
|
+
size={16}
|
|
162
|
+
className="shrink-0 text-muted-foreground"
|
|
163
|
+
/>
|
|
164
|
+
<div className="min-w-0">
|
|
165
|
+
<h2 className="text-sm font-semibold text-foreground">
|
|
166
|
+
Templates
|
|
167
|
+
</h2>
|
|
168
|
+
<p className="text-xs text-muted-foreground">
|
|
169
|
+
{templatesLoading
|
|
170
|
+
? "Checking available templates"
|
|
171
|
+
: `${typedTemplates.length} available to scaffold`}
|
|
172
|
+
</p>
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
<CollapsibleTrigger asChild>
|
|
176
|
+
<Button
|
|
177
|
+
type="button"
|
|
178
|
+
variant="outline"
|
|
179
|
+
size="sm"
|
|
180
|
+
className="gap-1.5"
|
|
181
|
+
>
|
|
182
|
+
{templatesOpen ? "Hide" : "Show"}
|
|
183
|
+
<IconChevronDown
|
|
184
|
+
size={14}
|
|
185
|
+
className={cn(
|
|
186
|
+
"transition-transform",
|
|
187
|
+
templatesOpen && "rotate-180",
|
|
188
|
+
)}
|
|
189
|
+
/>
|
|
190
|
+
</Button>
|
|
191
|
+
</CollapsibleTrigger>
|
|
192
|
+
</div>
|
|
193
|
+
<CollapsibleContent>
|
|
194
|
+
{templatesLoading && typedTemplates.length === 0 ? (
|
|
195
|
+
<AppsSkeletonGrid />
|
|
196
|
+
) : (
|
|
197
|
+
<div className="grid auto-rows-fr gap-3 md:grid-cols-2 xl:grid-cols-3">
|
|
198
|
+
{typedTemplates.map((template) => (
|
|
199
|
+
<AddTemplateCard
|
|
200
|
+
key={template.name}
|
|
201
|
+
template={template}
|
|
202
|
+
/>
|
|
203
|
+
))}
|
|
204
|
+
</div>
|
|
205
|
+
)}
|
|
206
|
+
</CollapsibleContent>
|
|
207
|
+
</section>
|
|
208
|
+
</Collapsible>
|
|
146
209
|
) : null}
|
|
147
210
|
|
|
148
211
|
{archivedApps.length > 0 ? (
|
|
149
|
-
<
|
|
150
|
-
<
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
212
|
+
<Collapsible open={showHidden} onOpenChange={setShowHidden}>
|
|
213
|
+
<section className="space-y-3">
|
|
214
|
+
<div className="flex flex-wrap items-center justify-between gap-3 border-t pt-4">
|
|
215
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
216
|
+
<IconEyeOff
|
|
217
|
+
size={16}
|
|
218
|
+
className="shrink-0 text-muted-foreground"
|
|
219
|
+
/>
|
|
220
|
+
<div className="min-w-0">
|
|
221
|
+
<h2 className="text-sm font-semibold text-foreground">
|
|
222
|
+
Hidden apps
|
|
223
|
+
</h2>
|
|
224
|
+
<p className="text-xs text-muted-foreground">
|
|
225
|
+
{archivedApps.length} hidden{" "}
|
|
226
|
+
{archivedApps.length === 1 ? "app" : "apps"}
|
|
227
|
+
</p>
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
<CollapsibleTrigger asChild>
|
|
231
|
+
<Button
|
|
232
|
+
type="button"
|
|
233
|
+
variant="outline"
|
|
234
|
+
size="sm"
|
|
235
|
+
className="gap-1.5"
|
|
236
|
+
>
|
|
237
|
+
{showHidden ? "Hide" : "Show"}
|
|
238
|
+
<IconChevronDown
|
|
239
|
+
size={14}
|
|
240
|
+
className={cn(
|
|
241
|
+
"transition-transform",
|
|
242
|
+
showHidden && "rotate-180",
|
|
243
|
+
)}
|
|
244
|
+
/>
|
|
245
|
+
</Button>
|
|
246
|
+
</CollapsibleTrigger>
|
|
164
247
|
</div>
|
|
165
|
-
|
|
166
|
-
|
|
248
|
+
<CollapsibleContent>
|
|
249
|
+
<div className="grid auto-rows-fr gap-3 md:grid-cols-2 xl:grid-cols-3">
|
|
250
|
+
{archivedApps.map((app) => (
|
|
251
|
+
<WorkspaceAppCard
|
|
252
|
+
key={app.id}
|
|
253
|
+
app={app}
|
|
254
|
+
className="h-full"
|
|
255
|
+
/>
|
|
256
|
+
))}
|
|
257
|
+
</div>
|
|
258
|
+
</CollapsibleContent>
|
|
259
|
+
</section>
|
|
260
|
+
</Collapsible>
|
|
167
261
|
) : null}
|
|
168
262
|
</div>
|
|
169
263
|
</DispatchShell>
|
|
170
264
|
);
|
|
171
265
|
}
|
|
172
266
|
|
|
267
|
+
function AppsSkeletonGrid() {
|
|
268
|
+
return (
|
|
269
|
+
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
|
|
270
|
+
{Array.from({ length: 3 }).map((_, index) => (
|
|
271
|
+
<div key={index} className="rounded-lg border bg-card p-4">
|
|
272
|
+
<div className="flex items-start justify-between gap-3">
|
|
273
|
+
<div className="min-w-0 flex-1 space-y-3">
|
|
274
|
+
<Skeleton className="h-4 w-32" />
|
|
275
|
+
<Skeleton className="h-3 w-24" />
|
|
276
|
+
<div className="space-y-2 pt-1">
|
|
277
|
+
<Skeleton className="h-3 w-full" />
|
|
278
|
+
<Skeleton className="h-3 w-2/3" />
|
|
279
|
+
</div>
|
|
280
|
+
</div>
|
|
281
|
+
<Skeleton className="h-7 w-7 rounded-md" />
|
|
282
|
+
</div>
|
|
283
|
+
</div>
|
|
284
|
+
))}
|
|
285
|
+
</div>
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function EmptyAppsState() {
|
|
290
|
+
return (
|
|
291
|
+
<div className="rounded-lg border border-dashed bg-card px-4 py-10 text-center">
|
|
292
|
+
<div className="mx-auto flex size-10 items-center justify-center rounded-lg bg-muted text-muted-foreground">
|
|
293
|
+
<IconApps size={18} />
|
|
294
|
+
</div>
|
|
295
|
+
<h3 className="mt-3 text-sm font-semibold text-foreground">
|
|
296
|
+
No workspace apps yet
|
|
297
|
+
</h3>
|
|
298
|
+
<p className="mx-auto mt-1 max-w-sm text-sm text-muted-foreground">
|
|
299
|
+
Create an app when a workflow needs its own focused place to live.
|
|
300
|
+
</p>
|
|
301
|
+
<div className="mt-4">
|
|
302
|
+
<CreateAppPopover
|
|
303
|
+
trigger={
|
|
304
|
+
<Button size="sm">
|
|
305
|
+
<IconPlus size={15} className="mr-1.5" />
|
|
306
|
+
Create app
|
|
307
|
+
</Button>
|
|
308
|
+
}
|
|
309
|
+
/>
|
|
310
|
+
</div>
|
|
311
|
+
</div>
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
173
315
|
function AddTemplateCard({ template }: { template: AvailableTemplate }) {
|
|
174
316
|
const Icon = TEMPLATE_ICONS[template.icon] ?? IconSparkles;
|
|
175
317
|
const scaffold = useActionMutation("scaffold-workspace-app", {
|
|
@@ -188,9 +330,9 @@ function AddTemplateCard({ template }: { template: AvailableTemplate }) {
|
|
|
188
330
|
});
|
|
189
331
|
|
|
190
332
|
return (
|
|
191
|
-
<div className="group relative flex items-
|
|
333
|
+
<div className="group relative flex h-full min-h-36 items-stretch gap-3 rounded-lg border bg-card p-4 transition hover:border-foreground/30">
|
|
192
334
|
<div
|
|
193
|
-
className="flex h-9 w-9 shrink-0 items-center justify-center rounded-md"
|
|
335
|
+
className="flex h-9 w-9 shrink-0 items-center justify-center self-start rounded-md"
|
|
194
336
|
style={{
|
|
195
337
|
backgroundColor: `rgb(${template.colorRgb} / 0.12)`,
|
|
196
338
|
color: template.color,
|
|
@@ -198,7 +340,7 @@ function AddTemplateCard({ template }: { template: AvailableTemplate }) {
|
|
|
198
340
|
>
|
|
199
341
|
<Icon size={18} />
|
|
200
342
|
</div>
|
|
201
|
-
<div className="min-w-0 flex-1">
|
|
343
|
+
<div className="flex min-w-0 flex-1 flex-col">
|
|
202
344
|
<div className="flex min-w-0 items-center gap-2">
|
|
203
345
|
<h3 className="truncate text-sm font-semibold text-foreground">
|
|
204
346
|
{template.label}
|
|
@@ -207,7 +349,7 @@ function AddTemplateCard({ template }: { template: AvailableTemplate }) {
|
|
|
207
349
|
<p className="mt-1 line-clamp-2 text-xs leading-relaxed text-muted-foreground">
|
|
208
350
|
{template.hint}
|
|
209
351
|
</p>
|
|
210
|
-
<div className="mt-3">
|
|
352
|
+
<div className="mt-auto pt-3">
|
|
211
353
|
<Button
|
|
212
354
|
size="sm"
|
|
213
355
|
variant="outline"
|
|
@@ -217,7 +359,7 @@ function AddTemplateCard({ template }: { template: AvailableTemplate }) {
|
|
|
217
359
|
{scaffold.isPending ? (
|
|
218
360
|
<>
|
|
219
361
|
<IconLoader2 size={14} className="mr-1.5 animate-spin" />
|
|
220
|
-
Adding
|
|
362
|
+
Adding...
|
|
221
363
|
</>
|
|
222
364
|
) : (
|
|
223
365
|
<>
|
|
@@ -47,6 +47,7 @@ interface AppAccessMetric {
|
|
|
47
47
|
name: string;
|
|
48
48
|
path: string;
|
|
49
49
|
status?: "ready" | "pending";
|
|
50
|
+
statusLabel?: string;
|
|
50
51
|
isDispatch: boolean;
|
|
51
52
|
accessLabel: string;
|
|
52
53
|
accessUsers: number;
|
|
@@ -440,7 +441,9 @@ function AppAccessTable({
|
|
|
440
441
|
"border-amber-500/30 bg-amber-500/10 text-amber-700 dark:text-amber-300",
|
|
441
442
|
)}
|
|
442
443
|
>
|
|
443
|
-
{row.status === "pending"
|
|
444
|
+
{row.status === "pending"
|
|
445
|
+
? row.statusLabel || "Builder branch"
|
|
446
|
+
: row.accessLabel}
|
|
444
447
|
</Badge>
|
|
445
448
|
</td>
|
|
446
449
|
<td className="px-2 py-3 text-right tabular-nums">
|
|
@@ -213,16 +213,22 @@ function WorkspaceAppsSection({
|
|
|
213
213
|
</Button>
|
|
214
214
|
</div>
|
|
215
215
|
|
|
216
|
-
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
|
|
217
|
-
{showSkeletons
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
216
|
+
<div className="grid auto-rows-fr gap-3 sm:grid-cols-2 xl:grid-cols-3">
|
|
217
|
+
{showSkeletons ? (
|
|
218
|
+
Array.from({ length: 6 }).map((_, index) => (
|
|
219
|
+
<AppCardSkeleton key={index} />
|
|
220
|
+
))
|
|
221
|
+
) : visibleApps.length > 0 ? (
|
|
222
|
+
visibleApps.map((app) => (
|
|
223
|
+
<WorkspaceAppCard
|
|
224
|
+
key={app.id}
|
|
225
|
+
app={app}
|
|
226
|
+
className="h-full min-h-32"
|
|
227
|
+
/>
|
|
228
|
+
))
|
|
229
|
+
) : (
|
|
230
|
+
<CreateAppPopover />
|
|
231
|
+
)}
|
|
226
232
|
</div>
|
|
227
233
|
</section>
|
|
228
234
|
);
|