@agent-native/dispatch 0.8.5 → 0.8.6
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 +59 -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/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/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/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 +237 -0
- package/dist/server/lib/mcp-gateway.js.map +1 -0
- 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 +61 -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/routes/pages/agents.tsx +187 -5
- package/src/routes/pages/apps.tsx +209 -67
- package/src/routes/pages/overview.tsx +16 -10
- 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.ts +333 -0
- package/src/server/plugins/agent-chat.ts +1 -0
- package/src/server/plugins/integrations.ts +2 -1
|
@@ -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
|
<>
|
|
@@ -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
|
);
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
isAppAllowedByMcpAccess,
|
|
4
|
+
normalizeMcpAppAccessSettings,
|
|
5
|
+
} from "./mcp-access-store.js";
|
|
6
|
+
|
|
7
|
+
describe("normalizeMcpAppAccessSettings", () => {
|
|
8
|
+
it("defaults to all apps", () => {
|
|
9
|
+
expect(normalizeMcpAppAccessSettings(null)).toEqual({
|
|
10
|
+
mode: "all-apps",
|
|
11
|
+
selectedAppIds: [],
|
|
12
|
+
updatedAt: undefined,
|
|
13
|
+
updatedBy: undefined,
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("normalizes selected app ids", () => {
|
|
18
|
+
expect(
|
|
19
|
+
normalizeMcpAppAccessSettings({
|
|
20
|
+
mode: "selected-apps",
|
|
21
|
+
selectedAppIds: [" Mail ", "mail", "calendar"],
|
|
22
|
+
updatedAt: "2026-05-20T12:00:00.000Z",
|
|
23
|
+
updatedBy: "admin@example.test",
|
|
24
|
+
}),
|
|
25
|
+
).toEqual({
|
|
26
|
+
mode: "selected-apps",
|
|
27
|
+
selectedAppIds: ["mail", "calendar"],
|
|
28
|
+
updatedAt: "2026-05-20T12:00:00.000Z",
|
|
29
|
+
updatedBy: "admin@example.test",
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe("isAppAllowedByMcpAccess", () => {
|
|
35
|
+
it("allows every app in all-apps mode", () => {
|
|
36
|
+
expect(
|
|
37
|
+
isAppAllowedByMcpAccess("mail", {
|
|
38
|
+
mode: "all-apps",
|
|
39
|
+
selectedAppIds: [],
|
|
40
|
+
}),
|
|
41
|
+
).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("checks selected grants in selected-apps mode", () => {
|
|
45
|
+
expect(
|
|
46
|
+
isAppAllowedByMcpAccess("mail", {
|
|
47
|
+
mode: "selected-apps",
|
|
48
|
+
selectedAppIds: ["calendar"],
|
|
49
|
+
}),
|
|
50
|
+
).toBe(false);
|
|
51
|
+
expect(
|
|
52
|
+
isAppAllowedByMcpAccess("calendar", {
|
|
53
|
+
mode: "selected-apps",
|
|
54
|
+
selectedAppIds: ["calendar"],
|
|
55
|
+
}),
|
|
56
|
+
).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
});
|