@agent-native/dispatch 0.19.2 → 0.20.1
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/provider-api-register.d.ts +5 -5
- package/dist/actions/start-workspace-app-creation.js +1 -1
- package/dist/actions/start-workspace-app-creation.js.map +1 -1
- package/dist/components/admin-navigation.d.ts +2 -1
- package/dist/components/admin-navigation.d.ts.map +1 -1
- package/dist/components/admin-navigation.js +10 -7
- package/dist/components/admin-navigation.js.map +1 -1
- package/dist/components/app-icon.d.ts +9 -0
- package/dist/components/app-icon.d.ts.map +1 -0
- package/dist/components/app-icon.js +85 -0
- package/dist/components/app-icon.js.map +1 -0
- package/dist/components/app-list-row.d.ts +12 -0
- package/dist/components/app-list-row.d.ts.map +1 -0
- package/dist/components/app-list-row.js +11 -0
- package/dist/components/app-list-row.js.map +1 -0
- package/dist/components/app-open-actions.d.ts +17 -0
- package/dist/components/app-open-actions.d.ts.map +1 -0
- package/dist/components/app-open-actions.js +20 -0
- package/dist/components/app-open-actions.js.map +1 -0
- package/dist/components/connected-app-card.d.ts +2 -1
- package/dist/components/connected-app-card.d.ts.map +1 -1
- package/dist/components/connected-app-card.js +8 -8
- package/dist/components/connected-app-card.js.map +1 -1
- package/dist/components/dispatch-control-plane.d.ts.map +1 -1
- package/dist/components/dispatch-control-plane.js +21 -14
- package/dist/components/dispatch-control-plane.js.map +1 -1
- package/dist/components/layout/Layout.d.ts.map +1 -1
- package/dist/components/layout/Layout.js +40 -19
- package/dist/components/layout/Layout.js.map +1 -1
- package/dist/components/layout/workspace-apps-rail.d.ts.map +1 -1
- package/dist/components/layout/workspace-apps-rail.js +10 -10
- package/dist/components/layout/workspace-apps-rail.js.map +1 -1
- package/dist/components/other-apps-section.d.ts +31 -0
- package/dist/components/other-apps-section.d.ts.map +1 -0
- package/dist/components/other-apps-section.js +59 -0
- package/dist/components/other-apps-section.js.map +1 -0
- package/dist/components/workspace-app-card.d.ts.map +1 -1
- package/dist/components/workspace-app-card.js +38 -28
- package/dist/components/workspace-app-card.js.map +1 -1
- package/dist/components/workspace-template-card.d.ts +8 -2
- package/dist/components/workspace-template-card.d.ts.map +1 -1
- package/dist/components/workspace-template-card.js +17 -13
- package/dist/components/workspace-template-card.js.map +1 -1
- package/dist/lib/other-apps.d.ts.map +1 -1
- package/dist/lib/other-apps.js +5 -1
- package/dist/lib/other-apps.js.map +1 -1
- package/dist/lib/workspace-apps.d.ts +1 -0
- package/dist/lib/workspace-apps.d.ts.map +1 -1
- package/dist/lib/workspace-apps.js +3 -0
- package/dist/lib/workspace-apps.js.map +1 -1
- package/dist/routes/index.d.ts.map +1 -1
- package/dist/routes/index.js +0 -2
- package/dist/routes/index.js.map +1 -1
- package/dist/routes/pages/admin._index.d.ts.map +1 -1
- package/dist/routes/pages/admin._index.js +32 -7
- package/dist/routes/pages/admin._index.js.map +1 -1
- package/dist/routes/pages/apps.$appId.d.ts.map +1 -1
- package/dist/routes/pages/apps.$appId.js +62 -13
- package/dist/routes/pages/apps.$appId.js.map +1 -1
- package/dist/routes/pages/apps.d.ts +1 -1
- package/dist/routes/pages/apps.d.ts.map +1 -1
- package/dist/routes/pages/apps.js +21 -30
- package/dist/routes/pages/apps.js.map +1 -1
- package/dist/server/plugins/agent-chat.js +6 -1
- package/dist/server/plugins/agent-chat.js.map +1 -1
- package/package.json +2 -2
- package/src/actions/start-workspace-app-creation.ts +1 -1
- package/src/components/admin-navigation.tsx +21 -16
- package/src/components/app-icon.tsx +149 -0
- package/src/components/app-list-row.tsx +40 -0
- package/src/components/app-open-actions.tsx +111 -0
- package/src/components/connected-app-card.tsx +30 -34
- package/src/components/dispatch-control-plane.spec.tsx +28 -1
- package/src/components/dispatch-control-plane.tsx +122 -48
- package/src/components/layout/Layout.spec.tsx +4 -4
- package/src/components/layout/Layout.tsx +86 -55
- package/src/components/layout/workspace-apps-rail.tsx +20 -21
- package/src/components/other-apps-section.tsx +195 -0
- package/src/components/workspace-app-card.spec.tsx +58 -32
- package/src/components/workspace-app-card.tsx +222 -158
- package/src/components/workspace-template-card.spec.tsx +45 -7
- package/src/components/workspace-template-card.tsx +74 -81
- package/src/lib/other-apps.spec.ts +31 -0
- package/src/lib/other-apps.ts +9 -1
- package/src/lib/workspace-apps.ts +4 -0
- package/src/routes/index.spec.ts +2 -1
- package/src/routes/index.ts +0 -2
- package/src/routes/pages/admin._index.tsx +85 -17
- package/src/routes/pages/apps.$appId.tsx +144 -57
- package/src/routes/pages/apps.tsx +76 -147
- package/src/server/plugins/agent-chat.spec.ts +5 -0
- package/src/server/plugins/agent-chat.ts +6 -1
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import {
|
|
2
|
+
IconActivity,
|
|
3
|
+
IconApps,
|
|
4
|
+
IconBrain,
|
|
5
|
+
IconBrush,
|
|
6
|
+
IconCalendar,
|
|
7
|
+
IconChartBar,
|
|
8
|
+
IconClipboardList,
|
|
9
|
+
IconFileText,
|
|
10
|
+
IconMail,
|
|
11
|
+
IconPhoto,
|
|
12
|
+
IconPlugConnected,
|
|
13
|
+
IconPresentation,
|
|
14
|
+
IconPuzzle,
|
|
15
|
+
IconScreenShare,
|
|
16
|
+
} from "@tabler/icons-react";
|
|
17
|
+
import type { CSSProperties } from "react";
|
|
18
|
+
|
|
19
|
+
import { cn } from "../lib/utils";
|
|
20
|
+
|
|
21
|
+
type AppIconComponent = typeof IconApps;
|
|
22
|
+
|
|
23
|
+
const FALLBACK_ICONS: AppIconComponent[] = [
|
|
24
|
+
IconApps,
|
|
25
|
+
IconPuzzle,
|
|
26
|
+
IconActivity,
|
|
27
|
+
IconPlugConnected,
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const APP_ICON_TONES = [
|
|
31
|
+
"bg-primary/10 text-primary",
|
|
32
|
+
"bg-secondary text-secondary-foreground",
|
|
33
|
+
"bg-accent text-accent-foreground",
|
|
34
|
+
"bg-muted text-foreground",
|
|
35
|
+
] as const;
|
|
36
|
+
|
|
37
|
+
const ICONS_BY_KEY: Record<string, AppIconComponent> = {
|
|
38
|
+
barchart2: IconChartBar,
|
|
39
|
+
brain: IconBrain,
|
|
40
|
+
brush: IconBrush,
|
|
41
|
+
calendardays: IconCalendar,
|
|
42
|
+
chartbar: IconChartBar,
|
|
43
|
+
clipboardlist: IconClipboardList,
|
|
44
|
+
filetext: IconFileText,
|
|
45
|
+
galleryhorizontal: IconPresentation,
|
|
46
|
+
mail: IconMail,
|
|
47
|
+
photo: IconPhoto,
|
|
48
|
+
presentation: IconPresentation,
|
|
49
|
+
screenshare: IconScreenShare,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
function appIconComponent(
|
|
53
|
+
id: string,
|
|
54
|
+
name: string,
|
|
55
|
+
iconKey?: string,
|
|
56
|
+
): AppIconComponent {
|
|
57
|
+
const normalizedIconKey = iconKey?.trim().toLowerCase();
|
|
58
|
+
if (normalizedIconKey && ICONS_BY_KEY[normalizedIconKey]) {
|
|
59
|
+
return ICONS_BY_KEY[normalizedIconKey];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const haystack = `${id} ${name}`.toLowerCase();
|
|
63
|
+
|
|
64
|
+
if (haystack.includes("mail") || haystack.includes("email")) {
|
|
65
|
+
return IconMail;
|
|
66
|
+
}
|
|
67
|
+
if (
|
|
68
|
+
haystack.includes("analytics") ||
|
|
69
|
+
haystack.includes("metric") ||
|
|
70
|
+
haystack.includes("indicator") ||
|
|
71
|
+
haystack.includes("gtm")
|
|
72
|
+
) {
|
|
73
|
+
return IconChartBar;
|
|
74
|
+
}
|
|
75
|
+
if (haystack.includes("coach") || haystack.includes("agent")) {
|
|
76
|
+
return IconBrain;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
let hash = 0;
|
|
80
|
+
for (const character of `${id}:${name}`) {
|
|
81
|
+
hash = (hash * 31 + character.charCodeAt(0)) | 0;
|
|
82
|
+
}
|
|
83
|
+
return FALLBACK_ICONS[Math.abs(hash) % FALLBACK_ICONS.length] ?? IconApps;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function safeHexColor(color: string | undefined): string | null {
|
|
87
|
+
const normalized = color?.trim();
|
|
88
|
+
return normalized && /^#[0-9a-f]{6}$/i.test(normalized) ? normalized : null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function AppIcon({
|
|
92
|
+
id,
|
|
93
|
+
name,
|
|
94
|
+
icon,
|
|
95
|
+
color,
|
|
96
|
+
className,
|
|
97
|
+
size = "md",
|
|
98
|
+
}: {
|
|
99
|
+
id: string;
|
|
100
|
+
name: string;
|
|
101
|
+
icon?: string;
|
|
102
|
+
color?: string;
|
|
103
|
+
className?: string;
|
|
104
|
+
size?: "sm" | "md";
|
|
105
|
+
}) {
|
|
106
|
+
const Icon = appIconComponent(id, name, icon);
|
|
107
|
+
const customColor = safeHexColor(color);
|
|
108
|
+
const tone =
|
|
109
|
+
APP_ICON_TONES[Math.abs(hashForApp(id, name)) % APP_ICON_TONES.length];
|
|
110
|
+
const style = customColor
|
|
111
|
+
? ({
|
|
112
|
+
"--dispatch-app-icon-color": customColor,
|
|
113
|
+
backgroundColor:
|
|
114
|
+
"color-mix(in srgb, var(--dispatch-app-icon-color) 14%, transparent)",
|
|
115
|
+
color: "var(--dispatch-app-icon-color)",
|
|
116
|
+
} as CSSProperties)
|
|
117
|
+
: ({
|
|
118
|
+
"--dispatch-app-icon-hue": `${appHue(id, name)} 72% 44%`,
|
|
119
|
+
backgroundColor: "hsl(var(--dispatch-app-icon-hue) / 0.14)",
|
|
120
|
+
color: "hsl(var(--dispatch-app-icon-hue))",
|
|
121
|
+
} as CSSProperties);
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<span
|
|
125
|
+
aria-hidden="true"
|
|
126
|
+
className={cn(
|
|
127
|
+
"flex shrink-0 items-center justify-center rounded-xl",
|
|
128
|
+
size === "sm" ? "size-8" : "size-10",
|
|
129
|
+
!customColor && tone,
|
|
130
|
+
className,
|
|
131
|
+
)}
|
|
132
|
+
style={style}
|
|
133
|
+
>
|
|
134
|
+
<Icon size={size === "sm" ? 16 : 19} stroke={1.8} />
|
|
135
|
+
</span>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function hashForApp(id: string, name: string): number {
|
|
140
|
+
let hash = 0;
|
|
141
|
+
for (const character of `${id}:${name}`) {
|
|
142
|
+
hash = (hash * 31 + character.charCodeAt(0)) | 0;
|
|
143
|
+
}
|
|
144
|
+
return hash;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function appHue(id: string, name: string): number {
|
|
148
|
+
return 20 + (Math.abs(hashForApp(id, name)) % 320);
|
|
149
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
import { cn } from "../lib/utils";
|
|
4
|
+
|
|
5
|
+
export const APP_LIST_GRID_CLASS =
|
|
6
|
+
"xl:grid xl:grid-cols-2 xl:gap-3 xl:overflow-visible xl:rounded-none xl:bg-transparent";
|
|
7
|
+
export const APP_LIST_GRID_ROW_CLASS = "xl:rounded-2xl xl:border-0 xl:bg-card";
|
|
8
|
+
|
|
9
|
+
export function AppList({
|
|
10
|
+
children,
|
|
11
|
+
className,
|
|
12
|
+
}: {
|
|
13
|
+
children: ReactNode;
|
|
14
|
+
className?: string;
|
|
15
|
+
}) {
|
|
16
|
+
return (
|
|
17
|
+
<div className={cn("overflow-hidden rounded-2xl bg-card", className)}>
|
|
18
|
+
{children}
|
|
19
|
+
</div>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function AppListRow({
|
|
24
|
+
children,
|
|
25
|
+
className,
|
|
26
|
+
}: {
|
|
27
|
+
children: ReactNode;
|
|
28
|
+
className?: string;
|
|
29
|
+
}) {
|
|
30
|
+
return (
|
|
31
|
+
<div
|
|
32
|
+
className={cn(
|
|
33
|
+
"group flex min-w-0 items-center gap-3 border-b px-4 py-3.5 last:border-b-0 transition-[background-color] hover:bg-muted/30 focus-within:bg-muted/30",
|
|
34
|
+
className,
|
|
35
|
+
)}
|
|
36
|
+
>
|
|
37
|
+
{children}
|
|
38
|
+
</div>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { IconChevronDown, IconPlus } from "@tabler/icons-react";
|
|
2
|
+
|
|
3
|
+
import { Button } from "./ui/button";
|
|
4
|
+
import {
|
|
5
|
+
DropdownMenu,
|
|
6
|
+
DropdownMenuContent,
|
|
7
|
+
DropdownMenuItem,
|
|
8
|
+
DropdownMenuTrigger,
|
|
9
|
+
} from "./ui/dropdown-menu";
|
|
10
|
+
|
|
11
|
+
export interface AppOpenActionLabels {
|
|
12
|
+
addApp: string;
|
|
13
|
+
openApp: string;
|
|
14
|
+
openInline: string;
|
|
15
|
+
openInNewTab: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const DEFAULT_LABELS: AppOpenActionLabels = {
|
|
19
|
+
addApp: "Add app",
|
|
20
|
+
openApp: "Open app",
|
|
21
|
+
openInline: "Open inline",
|
|
22
|
+
openInNewTab: "Open in new tab",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export function AppOpenActions({
|
|
26
|
+
name,
|
|
27
|
+
href,
|
|
28
|
+
target,
|
|
29
|
+
rel,
|
|
30
|
+
labels: labelOverrides,
|
|
31
|
+
onAddApp,
|
|
32
|
+
showInlineOption = false,
|
|
33
|
+
showNewTabOption = false,
|
|
34
|
+
}: {
|
|
35
|
+
name: string;
|
|
36
|
+
href: string | null;
|
|
37
|
+
target?: "_blank";
|
|
38
|
+
rel?: string;
|
|
39
|
+
labels?: Partial<AppOpenActionLabels>;
|
|
40
|
+
onAddApp?: () => void;
|
|
41
|
+
showInlineOption?: boolean;
|
|
42
|
+
showNewTabOption?: boolean;
|
|
43
|
+
}) {
|
|
44
|
+
const labels = { ...DEFAULT_LABELS, ...labelOverrides };
|
|
45
|
+
const hasMenu =
|
|
46
|
+
Boolean(onAddApp) ||
|
|
47
|
+
(Boolean(href) && (showInlineOption || showNewTabOption));
|
|
48
|
+
|
|
49
|
+
if (!href && !hasMenu) {
|
|
50
|
+
return (
|
|
51
|
+
<Button size="sm" variant="outline" disabled>
|
|
52
|
+
{labels.openApp}
|
|
53
|
+
</Button>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div className="flex shrink-0 overflow-hidden rounded-md border border-border focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2">
|
|
59
|
+
<Button
|
|
60
|
+
asChild={Boolean(href)}
|
|
61
|
+
size="sm"
|
|
62
|
+
variant="outline"
|
|
63
|
+
className="rounded-none border-0 border-e border-border"
|
|
64
|
+
disabled={!href}
|
|
65
|
+
>
|
|
66
|
+
{href ? (
|
|
67
|
+
<a href={href} target={target} rel={rel}>
|
|
68
|
+
{labels.openApp}
|
|
69
|
+
</a>
|
|
70
|
+
) : (
|
|
71
|
+
<span>{labels.openApp}</span>
|
|
72
|
+
)}
|
|
73
|
+
</Button>
|
|
74
|
+
{hasMenu ? (
|
|
75
|
+
<DropdownMenu>
|
|
76
|
+
<DropdownMenuTrigger asChild>
|
|
77
|
+
<Button
|
|
78
|
+
type="button"
|
|
79
|
+
size="sm"
|
|
80
|
+
variant="outline"
|
|
81
|
+
className="size-9 rounded-none border-0 p-0"
|
|
82
|
+
aria-label={`Open options for ${name}`}
|
|
83
|
+
>
|
|
84
|
+
<IconChevronDown size={15} />
|
|
85
|
+
</Button>
|
|
86
|
+
</DropdownMenuTrigger>
|
|
87
|
+
<DropdownMenuContent align="end" className="min-w-40">
|
|
88
|
+
{onAddApp ? (
|
|
89
|
+
<DropdownMenuItem onSelect={onAddApp}>
|
|
90
|
+
<IconPlus size={14} className="mr-2" />
|
|
91
|
+
{labels.addApp}
|
|
92
|
+
</DropdownMenuItem>
|
|
93
|
+
) : null}
|
|
94
|
+
{showInlineOption && href ? (
|
|
95
|
+
<DropdownMenuItem asChild>
|
|
96
|
+
<a href={href}>{labels.openInline}</a>
|
|
97
|
+
</DropdownMenuItem>
|
|
98
|
+
) : null}
|
|
99
|
+
{showNewTabOption && href ? (
|
|
100
|
+
<DropdownMenuItem asChild>
|
|
101
|
+
<a href={href} target="_blank" rel="noreferrer">
|
|
102
|
+
{labels.openInNewTab}
|
|
103
|
+
</a>
|
|
104
|
+
</DropdownMenuItem>
|
|
105
|
+
) : null}
|
|
106
|
+
</DropdownMenuContent>
|
|
107
|
+
</DropdownMenu>
|
|
108
|
+
) : null}
|
|
109
|
+
</div>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
@@ -1,42 +1,38 @@
|
|
|
1
|
-
import { useT } from "@agent-native/core/client/i18n";
|
|
2
1
|
import { withBuilderUtmTrackingParams } from "@agent-native/core/shared/builder-link-tracking";
|
|
3
|
-
import { IconArrowUpRight } from "@tabler/icons-react";
|
|
4
2
|
|
|
5
3
|
import type { ConnectedAppSummary } from "../lib/other-apps";
|
|
4
|
+
import { AppIcon } from "./app-icon";
|
|
5
|
+
import { AppListRow } from "./app-list-row";
|
|
6
|
+
import { AppOpenActions } from "./app-open-actions";
|
|
6
7
|
|
|
7
|
-
export function ConnectedAppCard({
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
export function ConnectedAppCard({
|
|
9
|
+
app,
|
|
10
|
+
className,
|
|
11
|
+
}: {
|
|
12
|
+
app: ConnectedAppSummary;
|
|
13
|
+
className?: string;
|
|
14
|
+
}) {
|
|
10
15
|
return (
|
|
11
|
-
<
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
className="group flex min-h-[116px] items-start gap-3 rounded-xl bg-card/40 p-4 transition-[background-color] hover:bg-accent/15 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
19
|
-
>
|
|
20
|
-
<span className="flex size-8 shrink-0 items-center justify-center rounded-md bg-muted text-xs font-semibold text-muted-foreground">
|
|
21
|
-
{app.name.charAt(0).toUpperCase()}
|
|
22
|
-
</span>
|
|
23
|
-
<span className="min-w-0 flex-1">
|
|
24
|
-
<span className="flex items-start justify-between gap-3">
|
|
25
|
-
<span className="truncate text-sm font-semibold text-foreground">
|
|
26
|
-
{app.name}
|
|
27
|
-
</span>
|
|
28
|
-
<IconArrowUpRight
|
|
29
|
-
size={15}
|
|
30
|
-
className="shrink-0 text-muted-foreground transition-transform group-hover:-translate-y-0.5 group-hover:translate-x-0.5"
|
|
31
|
-
/>
|
|
32
|
-
</span>
|
|
33
|
-
<span className="mt-2 line-clamp-2 block text-[13px] leading-5 text-muted-foreground">
|
|
16
|
+
<AppListRow className={className}>
|
|
17
|
+
<AppIcon id={app.id} name={app.name} color={app.color} size="sm" />
|
|
18
|
+
<div className="min-w-0 flex-1">
|
|
19
|
+
<div className="truncate text-sm font-semibold text-foreground">
|
|
20
|
+
{app.name}
|
|
21
|
+
</div>
|
|
22
|
+
<div className="mt-0.5 truncate text-xs text-muted-foreground">
|
|
34
23
|
{app.description || app.url}
|
|
35
|
-
</
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
24
|
+
</div>
|
|
25
|
+
</div>
|
|
26
|
+
<AppOpenActions
|
|
27
|
+
name={app.name}
|
|
28
|
+
href={withBuilderUtmTrackingParams(app.url, {
|
|
29
|
+
campaign: "product",
|
|
30
|
+
content: "dispatch_app",
|
|
31
|
+
})}
|
|
32
|
+
target="_blank"
|
|
33
|
+
rel="noopener noreferrer"
|
|
34
|
+
showNewTabOption
|
|
35
|
+
/>
|
|
36
|
+
</AppListRow>
|
|
41
37
|
);
|
|
42
38
|
}
|
|
@@ -12,6 +12,7 @@ const clientState = vi.hoisted(() => ({
|
|
|
12
12
|
promptComposerProps: null as Record<string, unknown> | null,
|
|
13
13
|
workspaceApps: [] as Array<Record<string, unknown>>,
|
|
14
14
|
connectedApps: [] as Array<Record<string, unknown>>,
|
|
15
|
+
curatedTemplates: [] as Array<Record<string, unknown>>,
|
|
15
16
|
useChatModels: vi.fn(() => ({
|
|
16
17
|
availableModels: [],
|
|
17
18
|
defaultModel: "auto",
|
|
@@ -56,7 +57,9 @@ vi.mock("@agent-native/core/client/hooks", () => ({
|
|
|
56
57
|
data:
|
|
57
58
|
name === "list-connected-agents"
|
|
58
59
|
? clientState.connectedApps
|
|
59
|
-
:
|
|
60
|
+
: name === "list-curated-workspace-templates"
|
|
61
|
+
? clientState.curatedTemplates
|
|
62
|
+
: clientState.workspaceApps,
|
|
60
63
|
isLoading: false,
|
|
61
64
|
isError: false,
|
|
62
65
|
error: null,
|
|
@@ -89,6 +92,7 @@ describe("DispatchControlPlane", () => {
|
|
|
89
92
|
clientState.promptComposerProps = null;
|
|
90
93
|
clientState.workspaceApps = [];
|
|
91
94
|
clientState.connectedApps = [];
|
|
95
|
+
clientState.curatedTemplates = [];
|
|
92
96
|
clientState.useChatModels.mockClear();
|
|
93
97
|
container = document.createElement("div");
|
|
94
98
|
document.body.appendChild(container);
|
|
@@ -195,6 +199,22 @@ describe("DispatchControlPlane", () => {
|
|
|
195
199
|
url: "https://duplicate.example.com",
|
|
196
200
|
},
|
|
197
201
|
];
|
|
202
|
+
clientState.curatedTemplates = [
|
|
203
|
+
{
|
|
204
|
+
id: "mail",
|
|
205
|
+
name: "Mail",
|
|
206
|
+
description: "Email client",
|
|
207
|
+
liveUrl: "https://mail.agent-native.com",
|
|
208
|
+
installed: false,
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
id: "analytics",
|
|
212
|
+
name: "Analytics",
|
|
213
|
+
description: "Workspace insights",
|
|
214
|
+
liveUrl: "https://analytics.agent-native.com",
|
|
215
|
+
installed: false,
|
|
216
|
+
},
|
|
217
|
+
];
|
|
198
218
|
|
|
199
219
|
await act(async () => {
|
|
200
220
|
root.render(
|
|
@@ -209,8 +229,15 @@ describe("DispatchControlPlane", () => {
|
|
|
209
229
|
expect(container.textContent).toContain("Onboarding");
|
|
210
230
|
expect(container.textContent).toContain("Mail");
|
|
211
231
|
expect(container.textContent).toContain("Clips");
|
|
232
|
+
expect(container.textContent).toContain("Analytics");
|
|
233
|
+
expect(container.textContent).toContain("Your apps");
|
|
234
|
+
expect(container.textContent).toContain("Other apps");
|
|
235
|
+
expect(container.textContent?.indexOf("Your apps")).toBeLessThan(
|
|
236
|
+
container.textContent?.indexOf("Other apps") ?? -1,
|
|
237
|
+
);
|
|
212
238
|
expect(container.textContent).not.toContain("Archived app");
|
|
213
239
|
expect(container.textContent).not.toContain("Duplicate onboarding");
|
|
240
|
+
expect(container.textContent).not.toContain("CRM");
|
|
214
241
|
expect(
|
|
215
242
|
Array.from(container.querySelectorAll("a")).filter((anchor) =>
|
|
216
243
|
anchor.getAttribute("href")?.includes("onboarding"),
|
|
@@ -10,18 +10,25 @@ import {
|
|
|
10
10
|
IconArrowUpRight,
|
|
11
11
|
IconChevronDown,
|
|
12
12
|
IconClockHour4,
|
|
13
|
+
IconApps,
|
|
13
14
|
} from "@tabler/icons-react";
|
|
14
15
|
import type { ReactNode } from "react";
|
|
15
16
|
import { useState } from "react";
|
|
16
17
|
import { Link, useNavigate } from "react-router";
|
|
17
18
|
|
|
18
|
-
import {
|
|
19
|
+
import type { ConnectedAppSummary } from "../lib/other-apps";
|
|
19
20
|
import { submitOverviewPrompt } from "../lib/overview-chat";
|
|
21
|
+
import { cn } from "../lib/utils";
|
|
20
22
|
import type { WorkspaceAppSummary } from "../lib/workspace-apps";
|
|
21
23
|
import { ActionQueryError } from "./action-query-error";
|
|
22
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
APP_LIST_GRID_CLASS,
|
|
26
|
+
AppList,
|
|
27
|
+
APP_LIST_GRID_ROW_CLASS,
|
|
28
|
+
} from "./app-list-row";
|
|
23
29
|
import { CreateAppPopover } from "./create-app-popover";
|
|
24
30
|
import { useSetPageTitle } from "./layout/HeaderActions";
|
|
31
|
+
import { OtherAppsSection } from "./other-apps-section";
|
|
25
32
|
import { Button } from "./ui/button";
|
|
26
33
|
import {
|
|
27
34
|
Collapsible,
|
|
@@ -30,6 +37,7 @@ import {
|
|
|
30
37
|
} from "./ui/collapsible";
|
|
31
38
|
import { Skeleton } from "./ui/skeleton";
|
|
32
39
|
import { WorkspaceAppCard } from "./workspace-app-card";
|
|
40
|
+
import type { CuratedWorkspaceTemplatesResult } from "./workspace-template-card";
|
|
33
41
|
|
|
34
42
|
function SectionHeader({
|
|
35
43
|
title,
|
|
@@ -48,6 +56,26 @@ function SectionHeader({
|
|
|
48
56
|
);
|
|
49
57
|
}
|
|
50
58
|
|
|
59
|
+
function AppGroupHeader({
|
|
60
|
+
title,
|
|
61
|
+
description,
|
|
62
|
+
}: {
|
|
63
|
+
title: string;
|
|
64
|
+
description: string;
|
|
65
|
+
}) {
|
|
66
|
+
return (
|
|
67
|
+
<div className="flex min-w-0 items-start gap-2">
|
|
68
|
+
<IconApps size={16} className="mt-0.5 shrink-0 text-muted-foreground" />
|
|
69
|
+
<div className="min-w-0">
|
|
70
|
+
<h3 className="truncate text-sm font-semibold text-foreground">
|
|
71
|
+
{title}
|
|
72
|
+
</h3>
|
|
73
|
+
<p className="mt-0.5 text-xs text-muted-foreground">{description}</p>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
51
79
|
function CommandPanel() {
|
|
52
80
|
const t = useT();
|
|
53
81
|
const {
|
|
@@ -143,29 +171,33 @@ function CommandPanel() {
|
|
|
143
171
|
function AppsPanel({
|
|
144
172
|
apps,
|
|
145
173
|
isLoading,
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
174
|
+
connectedApps,
|
|
175
|
+
connectedAppsError,
|
|
176
|
+
connectedAppsLoading,
|
|
177
|
+
onRetryConnectedApps,
|
|
178
|
+
curatedTemplates,
|
|
179
|
+
curatedTemplatesError,
|
|
180
|
+
curatedTemplatesLoading,
|
|
181
|
+
onRetryCuratedTemplates,
|
|
150
182
|
}: {
|
|
151
183
|
apps: WorkspaceAppSummary[];
|
|
152
184
|
isLoading: boolean;
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
185
|
+
connectedApps: ConnectedAppSummary[];
|
|
186
|
+
connectedAppsError?: Error | null;
|
|
187
|
+
connectedAppsLoading: boolean;
|
|
188
|
+
onRetryConnectedApps: () => void;
|
|
189
|
+
curatedTemplates?: CuratedWorkspaceTemplatesResult;
|
|
190
|
+
curatedTemplatesError?: Error | null;
|
|
191
|
+
curatedTemplatesLoading: boolean;
|
|
192
|
+
onRetryCuratedTemplates: () => void;
|
|
157
193
|
}) {
|
|
158
194
|
const t = useT();
|
|
159
195
|
const [showPending, setShowPending] = useState(false);
|
|
160
196
|
const visibleApps = apps.filter((app) => !app.isDispatch && !app.archived);
|
|
161
197
|
const activeApps = visibleApps.filter((app) => app.status !== "pending");
|
|
162
198
|
const pendingApps = visibleApps.filter((app) => app.status === "pending");
|
|
163
|
-
const hasActiveApps = activeApps.length > 0 || otherApps.length > 0;
|
|
164
199
|
const showSkeletons =
|
|
165
|
-
|
|
166
|
-
activeApps.length === 0 &&
|
|
167
|
-
otherApps.length === 0 &&
|
|
168
|
-
pendingApps.length === 0;
|
|
200
|
+
isLoading && activeApps.length === 0 && pendingApps.length === 0;
|
|
169
201
|
|
|
170
202
|
return (
|
|
171
203
|
<section className="flex flex-col gap-3">
|
|
@@ -181,30 +213,45 @@ function AppsPanel({
|
|
|
181
213
|
}
|
|
182
214
|
/>
|
|
183
215
|
{showSkeletons ? (
|
|
184
|
-
<
|
|
185
|
-
{Array.from({ length: 4 }).map((_, index) => (
|
|
186
|
-
<div key={index} className="rounded-xl bg-card/40 p-4">
|
|
187
|
-
<Skeleton className="h-4 w-32" />
|
|
188
|
-
<Skeleton className="mt-3 h-3 w-24" />
|
|
189
|
-
<Skeleton className="mt-3 h-3 w-full" />
|
|
190
|
-
</div>
|
|
191
|
-
))}
|
|
192
|
-
</div>
|
|
193
|
-
) : hasActiveApps ? (
|
|
194
|
-
<div className="grid gap-3 md:grid-cols-2">
|
|
195
|
-
{activeApps.map((app) => (
|
|
196
|
-
<WorkspaceAppCard key={app.id} app={app} className="min-h-32" />
|
|
197
|
-
))}
|
|
198
|
-
{otherApps.map((app) => (
|
|
199
|
-
<ConnectedAppCard key={app.id} app={app} />
|
|
200
|
-
))}
|
|
201
|
-
</div>
|
|
216
|
+
<OverviewAppsSkeleton />
|
|
202
217
|
) : (
|
|
203
|
-
|
|
218
|
+
<>
|
|
219
|
+
{activeApps.length > 0 ? (
|
|
220
|
+
<div className="space-y-3">
|
|
221
|
+
<AppGroupHeader
|
|
222
|
+
title={t("dispatch.pages.yourApps", {
|
|
223
|
+
defaultValue: "Your apps",
|
|
224
|
+
})}
|
|
225
|
+
description={t("dispatch.pages.activeCount", {
|
|
226
|
+
count: activeApps.length,
|
|
227
|
+
})}
|
|
228
|
+
/>
|
|
229
|
+
<AppList className={APP_LIST_GRID_CLASS}>
|
|
230
|
+
{activeApps.map((app) => (
|
|
231
|
+
<WorkspaceAppCard
|
|
232
|
+
key={app.id}
|
|
233
|
+
app={app}
|
|
234
|
+
className={APP_LIST_GRID_ROW_CLASS}
|
|
235
|
+
/>
|
|
236
|
+
))}
|
|
237
|
+
</AppList>
|
|
238
|
+
</div>
|
|
239
|
+
) : (
|
|
240
|
+
<CreateAppPopover />
|
|
241
|
+
)}
|
|
242
|
+
<OtherAppsSection
|
|
243
|
+
templates={curatedTemplates}
|
|
244
|
+
connectedApps={connectedApps}
|
|
245
|
+
workspaceApps={apps}
|
|
246
|
+
templatesLoading={curatedTemplatesLoading}
|
|
247
|
+
connectedAppsLoading={connectedAppsLoading}
|
|
248
|
+
templatesError={curatedTemplatesError}
|
|
249
|
+
connectedAppsError={connectedAppsError}
|
|
250
|
+
onRetryTemplates={onRetryCuratedTemplates}
|
|
251
|
+
onRetryConnectedApps={onRetryConnectedApps}
|
|
252
|
+
/>
|
|
253
|
+
</>
|
|
204
254
|
)}
|
|
205
|
-
{otherAppsError ? (
|
|
206
|
-
<ActionQueryError error={otherAppsError} onRetry={onRetryOtherApps} />
|
|
207
|
-
) : null}
|
|
208
255
|
{pendingApps.length > 0 ? (
|
|
209
256
|
<Collapsible open={showPending} onOpenChange={setShowPending}>
|
|
210
257
|
<div className="space-y-3 border-t pt-3">
|
|
@@ -243,15 +290,15 @@ function AppsPanel({
|
|
|
243
290
|
</CollapsibleTrigger>
|
|
244
291
|
</div>
|
|
245
292
|
<CollapsibleContent>
|
|
246
|
-
<
|
|
293
|
+
<AppList className={APP_LIST_GRID_CLASS}>
|
|
247
294
|
{pendingApps.map((app) => (
|
|
248
295
|
<WorkspaceAppCard
|
|
249
296
|
key={app.id}
|
|
250
297
|
app={app}
|
|
251
|
-
className=
|
|
298
|
+
className={APP_LIST_GRID_ROW_CLASS}
|
|
252
299
|
/>
|
|
253
300
|
))}
|
|
254
|
-
</
|
|
301
|
+
</AppList>
|
|
255
302
|
</CollapsibleContent>
|
|
256
303
|
</div>
|
|
257
304
|
</Collapsible>
|
|
@@ -260,6 +307,29 @@ function AppsPanel({
|
|
|
260
307
|
);
|
|
261
308
|
}
|
|
262
309
|
|
|
310
|
+
function OverviewAppsSkeleton() {
|
|
311
|
+
return (
|
|
312
|
+
<AppList className={APP_LIST_GRID_CLASS}>
|
|
313
|
+
{Array.from({ length: 4 }).map((_, index) => (
|
|
314
|
+
<div
|
|
315
|
+
key={index}
|
|
316
|
+
className={cn(
|
|
317
|
+
"flex min-w-0 items-center gap-3 border-b px-4 py-3.5 last:border-b-0",
|
|
318
|
+
APP_LIST_GRID_ROW_CLASS,
|
|
319
|
+
)}
|
|
320
|
+
>
|
|
321
|
+
<Skeleton className="size-8 shrink-0 rounded-xl" />
|
|
322
|
+
<div className="min-w-0 flex-1 space-y-2">
|
|
323
|
+
<Skeleton className="h-4 w-32" />
|
|
324
|
+
<Skeleton className="h-3 w-2/3" />
|
|
325
|
+
</div>
|
|
326
|
+
<Skeleton className="h-9 w-24 shrink-0 rounded-md" />
|
|
327
|
+
</div>
|
|
328
|
+
))}
|
|
329
|
+
</AppList>
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
263
333
|
export function DispatchControlPlane() {
|
|
264
334
|
useSetPageTitle(<span aria-hidden />);
|
|
265
335
|
const appsQuery = useActionQuery<WorkspaceAppSummary[]>(
|
|
@@ -270,11 +340,11 @@ export function DispatchControlPlane() {
|
|
|
270
340
|
"list-connected-agents",
|
|
271
341
|
{},
|
|
272
342
|
);
|
|
273
|
-
const
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
workspaceApps ?? [],
|
|
343
|
+
const curatedTemplatesQuery = useActionQuery<CuratedWorkspaceTemplatesResult>(
|
|
344
|
+
"list-curated-workspace-templates",
|
|
345
|
+
{},
|
|
277
346
|
);
|
|
347
|
+
const { data: workspaceApps = [], isLoading: appsLoading } = appsQuery;
|
|
278
348
|
|
|
279
349
|
return (
|
|
280
350
|
<div className="flex flex-col gap-8">
|
|
@@ -288,10 +358,14 @@ export function DispatchControlPlane() {
|
|
|
288
358
|
<AppsPanel
|
|
289
359
|
apps={workspaceApps ?? []}
|
|
290
360
|
isLoading={appsLoading}
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
361
|
+
connectedApps={connectedAppsQuery.data ?? []}
|
|
362
|
+
connectedAppsError={connectedAppsQuery.error}
|
|
363
|
+
connectedAppsLoading={connectedAppsQuery.isLoading}
|
|
364
|
+
onRetryConnectedApps={() => void connectedAppsQuery.refetch()}
|
|
365
|
+
curatedTemplates={curatedTemplatesQuery.data}
|
|
366
|
+
curatedTemplatesError={curatedTemplatesQuery.error}
|
|
367
|
+
curatedTemplatesLoading={curatedTemplatesQuery.isLoading}
|
|
368
|
+
onRetryCuratedTemplates={() => void curatedTemplatesQuery.refetch()}
|
|
295
369
|
/>
|
|
296
370
|
)}
|
|
297
371
|
</div>
|