@agent-native/dispatch 0.2.2 → 0.2.4
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/README.md +4 -4
- package/dist/components/agents-panel.d.ts.map +1 -1
- package/dist/components/agents-panel.js +68 -4
- package/dist/components/agents-panel.js.map +1 -1
- package/dist/components/layout/Header.js +1 -1
- package/dist/components/layout/Header.js.map +1 -1
- package/dist/components/layout/Layout.d.ts.map +1 -1
- package/dist/components/layout/Layout.js +2 -3
- package/dist/components/layout/Layout.js.map +1 -1
- package/dist/components/ui/sidebar.d.ts.map +1 -1
- package/dist/components/ui/sidebar.js +1 -1
- package/dist/components/ui/sidebar.js.map +1 -1
- package/dist/lib/overview-chat.d.ts +2 -0
- package/dist/lib/overview-chat.d.ts.map +1 -0
- package/dist/lib/overview-chat.js +20 -0
- package/dist/lib/overview-chat.js.map +1 -0
- package/dist/routes/pages/overview.d.ts.map +1 -1
- package/dist/routes/pages/overview.js +4 -8
- package/dist/routes/pages/overview.js.map +1 -1
- package/package.json +4 -3
- package/src/components/agents-panel.tsx +117 -22
- package/src/components/layout/Header.tsx +1 -1
- package/src/components/layout/Layout.tsx +3 -11
- package/src/components/ui/sidebar.tsx +22 -18
- package/src/lib/overview-chat.spec.ts +48 -0
- package/src/lib/overview-chat.ts +24 -0
- package/src/routes/pages/overview.tsx +3 -8
- package/src/styles/dispatch-css.spec.ts +55 -0
- package/src/styles/dispatch.css +9 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useRef, useState } from "react";
|
|
1
|
+
import { useRef, useState, type FormEvent } from "react";
|
|
2
2
|
import { Button } from "@/components/ui/button";
|
|
3
3
|
import { Input } from "@/components/ui/input";
|
|
4
4
|
import {
|
|
@@ -27,6 +27,48 @@ export interface ConnectedAgent {
|
|
|
27
27
|
scope?: "shared" | "personal";
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
+
type AgentFormErrors = Partial<Record<"name" | "url" | "form", string>>;
|
|
31
|
+
|
|
32
|
+
function slugifyAgentName(value: string): string {
|
|
33
|
+
return value
|
|
34
|
+
.trim()
|
|
35
|
+
.toLowerCase()
|
|
36
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
37
|
+
.replace(/^-+|-+$/g, "");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function validateAgentForm(name: string, url: string): AgentFormErrors {
|
|
41
|
+
const errors: AgentFormErrors = {};
|
|
42
|
+
const trimmedName = name.trim();
|
|
43
|
+
const trimmedUrl = url.trim();
|
|
44
|
+
|
|
45
|
+
if (!trimmedName) {
|
|
46
|
+
errors.name = "Agent name is required.";
|
|
47
|
+
} else if (!slugifyAgentName(trimmedName)) {
|
|
48
|
+
errors.name = "Agent name must include at least one letter or number.";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!trimmedUrl) {
|
|
52
|
+
errors.url = "Agent endpoint URL is required.";
|
|
53
|
+
} else {
|
|
54
|
+
try {
|
|
55
|
+
const parsed = new URL(trimmedUrl);
|
|
56
|
+
if (parsed.protocol !== "https:" && parsed.protocol !== "http:") {
|
|
57
|
+
errors.url = "Use an http:// or https:// endpoint URL.";
|
|
58
|
+
} else if (!parsed.hostname) {
|
|
59
|
+
errors.url = "Enter a complete endpoint URL with a host.";
|
|
60
|
+
} else if (parsed.username || parsed.password) {
|
|
61
|
+
errors.url = "Do not include credentials in the endpoint URL.";
|
|
62
|
+
}
|
|
63
|
+
} catch {
|
|
64
|
+
errors.url =
|
|
65
|
+
"Enter a valid endpoint URL, such as https://app.example.com.";
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return errors;
|
|
70
|
+
}
|
|
71
|
+
|
|
30
72
|
export function AgentsPanel({
|
|
31
73
|
agents,
|
|
32
74
|
onRefresh,
|
|
@@ -38,6 +80,7 @@ export function AgentsPanel({
|
|
|
38
80
|
const [url, setUrl] = useState("");
|
|
39
81
|
const [description, setDescription] = useState("");
|
|
40
82
|
const [saving, setSaving] = useState(false);
|
|
83
|
+
const [errors, setErrors] = useState<AgentFormErrors>({});
|
|
41
84
|
const nameRef = useRef<HTMLInputElement>(null);
|
|
42
85
|
|
|
43
86
|
const customAgents = agents.filter((agent) => agent.source === "custom");
|
|
@@ -46,12 +89,17 @@ export function AgentsPanel({
|
|
|
46
89
|
);
|
|
47
90
|
const builtinAgents = agents.filter((agent) => agent.source === "builtin");
|
|
48
91
|
|
|
49
|
-
const handleAdd = async () => {
|
|
92
|
+
const handleAdd = async (event?: FormEvent<HTMLFormElement>) => {
|
|
93
|
+
event?.preventDefault();
|
|
50
94
|
const trimmedName = name.trim();
|
|
51
95
|
const trimmedUrl = url.trim();
|
|
52
|
-
|
|
96
|
+
const nextErrors = validateAgentForm(trimmedName, trimmedUrl);
|
|
97
|
+
if (Object.keys(nextErrors).length > 0) {
|
|
98
|
+
setErrors(nextErrors);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
53
101
|
|
|
54
|
-
const id = trimmedName
|
|
102
|
+
const id = slugifyAgentName(trimmedName);
|
|
55
103
|
const agentJson = JSON.stringify(
|
|
56
104
|
{
|
|
57
105
|
id,
|
|
@@ -79,9 +127,21 @@ export function AgentsPanel({
|
|
|
79
127
|
setName("");
|
|
80
128
|
setUrl("");
|
|
81
129
|
setDescription("");
|
|
130
|
+
setErrors({});
|
|
82
131
|
onRefresh();
|
|
83
132
|
nameRef.current?.focus();
|
|
133
|
+
} else {
|
|
134
|
+
setErrors({
|
|
135
|
+
form: `Could not add agent. Request failed with ${res.status}.`,
|
|
136
|
+
});
|
|
84
137
|
}
|
|
138
|
+
} catch (error) {
|
|
139
|
+
setErrors({
|
|
140
|
+
form:
|
|
141
|
+
error instanceof Error
|
|
142
|
+
? error.message
|
|
143
|
+
: "Could not add agent. Please try again.",
|
|
144
|
+
});
|
|
85
145
|
} finally {
|
|
86
146
|
setSaving(false);
|
|
87
147
|
}
|
|
@@ -230,31 +290,66 @@ export function AgentsPanel({
|
|
|
230
290
|
<p className="mt-1 text-xs leading-relaxed text-muted-foreground">
|
|
231
291
|
Add another A2A-compatible app by saving its agent endpoint here.
|
|
232
292
|
</p>
|
|
233
|
-
<
|
|
234
|
-
<
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
293
|
+
<form className="mt-4 space-y-3" onSubmit={handleAdd} noValidate>
|
|
294
|
+
<div className="space-y-1.5">
|
|
295
|
+
<Input
|
|
296
|
+
ref={nameRef}
|
|
297
|
+
value={name}
|
|
298
|
+
onChange={(event) => {
|
|
299
|
+
setName(event.target.value);
|
|
300
|
+
setErrors((current) => ({ ...current, name: undefined }));
|
|
301
|
+
}}
|
|
302
|
+
placeholder="Name"
|
|
303
|
+
aria-invalid={Boolean(errors.name)}
|
|
304
|
+
aria-describedby={
|
|
305
|
+
errors.name ? "external-agent-name-error" : undefined
|
|
306
|
+
}
|
|
307
|
+
/>
|
|
308
|
+
{errors.name ? (
|
|
309
|
+
<p
|
|
310
|
+
id="external-agent-name-error"
|
|
311
|
+
className="text-xs font-medium text-destructive"
|
|
312
|
+
>
|
|
313
|
+
{errors.name}
|
|
314
|
+
</p>
|
|
315
|
+
) : null}
|
|
316
|
+
</div>
|
|
317
|
+
<div className="space-y-1.5">
|
|
318
|
+
<Input
|
|
319
|
+
value={url}
|
|
320
|
+
onChange={(event) => {
|
|
321
|
+
setUrl(event.target.value);
|
|
322
|
+
setErrors((current) => ({ ...current, url: undefined }));
|
|
323
|
+
}}
|
|
324
|
+
placeholder="https://app.example.com"
|
|
325
|
+
aria-invalid={Boolean(errors.url)}
|
|
326
|
+
aria-describedby={
|
|
327
|
+
errors.url ? "external-agent-url-error" : undefined
|
|
328
|
+
}
|
|
329
|
+
/>
|
|
330
|
+
{errors.url ? (
|
|
331
|
+
<p
|
|
332
|
+
id="external-agent-url-error"
|
|
333
|
+
className="text-xs font-medium text-destructive"
|
|
334
|
+
>
|
|
335
|
+
{errors.url}
|
|
336
|
+
</p>
|
|
337
|
+
) : null}
|
|
338
|
+
</div>
|
|
245
339
|
<Input
|
|
246
340
|
value={description}
|
|
247
341
|
onChange={(event) => setDescription(event.target.value)}
|
|
248
342
|
placeholder="Description (optional)"
|
|
249
343
|
/>
|
|
250
|
-
|
|
251
|
-
className="
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
344
|
+
{errors.form ? (
|
|
345
|
+
<p className="text-xs font-medium text-destructive">
|
|
346
|
+
{errors.form}
|
|
347
|
+
</p>
|
|
348
|
+
) : null}
|
|
349
|
+
<Button type="submit" className="w-full" disabled={saving}>
|
|
255
350
|
{saving ? "Saving..." : "Add agent"}
|
|
256
351
|
</Button>
|
|
257
|
-
</
|
|
352
|
+
</form>
|
|
258
353
|
</div>
|
|
259
354
|
</div>
|
|
260
355
|
</section>
|
|
@@ -327,8 +327,6 @@ export function Layout({
|
|
|
327
327
|
}) {
|
|
328
328
|
const location = useLocation();
|
|
329
329
|
const [mobileOpen, setMobileOpen] = useState(false);
|
|
330
|
-
const hasEmbeddedAgentChat =
|
|
331
|
-
location.pathname === "/" || location.pathname === "/overview";
|
|
332
330
|
|
|
333
331
|
if (CHROMELESS_PATHS.some((path) => location.pathname === path)) {
|
|
334
332
|
return <>{children}</>;
|
|
@@ -337,12 +335,7 @@ export function Layout({
|
|
|
337
335
|
const showHeader = !pageOwnsToolbar(location.pathname);
|
|
338
336
|
const appContent = (
|
|
339
337
|
<div className="flex h-full flex-1 flex-col overflow-hidden">
|
|
340
|
-
{showHeader ? (
|
|
341
|
-
<Header
|
|
342
|
-
onOpenMobile={() => setMobileOpen(true)}
|
|
343
|
-
showAgentToggle={!hasEmbeddedAgentChat}
|
|
344
|
-
/>
|
|
345
|
-
) : null}
|
|
338
|
+
{showHeader ? <Header onOpenMobile={() => setMobileOpen(true)} /> : null}
|
|
346
339
|
<InvitationBanner />
|
|
347
340
|
<main className="flex-1 overflow-y-auto">
|
|
348
341
|
{showHeader ? (
|
|
@@ -359,7 +352,7 @@ export function Layout({
|
|
|
359
352
|
return (
|
|
360
353
|
<HeaderActionsProvider>
|
|
361
354
|
<div className="flex h-screen w-full overflow-hidden bg-background">
|
|
362
|
-
<aside className="hidden
|
|
355
|
+
<aside className="hidden lg:flex w-64 shrink-0 flex-col border-r bg-sidebar text-sidebar-foreground">
|
|
363
356
|
<NavContent extensions={extensions} />
|
|
364
357
|
</aside>
|
|
365
358
|
|
|
@@ -383,8 +376,7 @@ export function Layout({
|
|
|
383
376
|
|
|
384
377
|
{/*
|
|
385
378
|
* Always mount AgentSidebar so home composer's sendToAgentChat
|
|
386
|
-
* fallback can pop it via agent-panel:open.
|
|
387
|
-
* hidden on overview because the home composer is the primary input.
|
|
379
|
+
* fallback can pop it via agent-panel:open.
|
|
388
380
|
*/}
|
|
389
381
|
<AgentSidebar
|
|
390
382
|
position="right"
|
|
@@ -307,24 +307,28 @@ const SidebarRail = React.forwardRef<
|
|
|
307
307
|
const { toggleSidebar } = useSidebar();
|
|
308
308
|
|
|
309
309
|
return (
|
|
310
|
-
<
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
310
|
+
<Tooltip>
|
|
311
|
+
<TooltipTrigger asChild>
|
|
312
|
+
<button
|
|
313
|
+
ref={ref}
|
|
314
|
+
data-sidebar="rail"
|
|
315
|
+
aria-label="Toggle Sidebar"
|
|
316
|
+
tabIndex={-1}
|
|
317
|
+
onClick={toggleSidebar}
|
|
318
|
+
className={cn(
|
|
319
|
+
"absolute inset-y-0 z-20 hidden w-4 -translate-x-1/2 transition-all ease-linear after:absolute after:inset-y-0 after:left-1/2 after:w-[2px] hover:after:bg-sidebar-border group-data-[side=left]:-right-4 group-data-[side=right]:left-0 sm:flex",
|
|
320
|
+
"[[data-side=left]_&]:cursor-w-resize [[data-side=right]_&]:cursor-e-resize",
|
|
321
|
+
"[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
|
|
322
|
+
"group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full group-data-[collapsible=offcanvas]:hover:bg-sidebar",
|
|
323
|
+
"[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
|
|
324
|
+
"[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
|
|
325
|
+
className,
|
|
326
|
+
)}
|
|
327
|
+
{...props}
|
|
328
|
+
/>
|
|
329
|
+
</TooltipTrigger>
|
|
330
|
+
<TooltipContent>Toggle Sidebar</TooltipContent>
|
|
331
|
+
</Tooltip>
|
|
328
332
|
);
|
|
329
333
|
});
|
|
330
334
|
SidebarRail.displayName = "SidebarRail";
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
const frameState = vi.hoisted(() => ({ inBuilderFrame: false }));
|
|
4
|
+
const sendToAgentChatMock = vi.hoisted(() => vi.fn(() => "chat-tab"));
|
|
5
|
+
|
|
6
|
+
vi.mock("@agent-native/core/client", () => ({
|
|
7
|
+
isInBuilderFrame: () => frameState.inBuilderFrame,
|
|
8
|
+
sendToAgentChat: sendToAgentChatMock,
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
const { submitOverviewPrompt } = await import("./overview-chat.js");
|
|
12
|
+
|
|
13
|
+
describe("submitOverviewPrompt", () => {
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
frameState.inBuilderFrame = false;
|
|
16
|
+
sendToAgentChatMock.mockClear();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("sends overview prompts to a new local agent tab outside Builder", () => {
|
|
20
|
+
const tabId = submitOverviewPrompt(" build a metrics app ", "auto");
|
|
21
|
+
|
|
22
|
+
expect(tabId).toBe("chat-tab");
|
|
23
|
+
expect(sendToAgentChatMock).toHaveBeenCalledWith({
|
|
24
|
+
message: "build a metrics app",
|
|
25
|
+
submit: true,
|
|
26
|
+
newTab: true,
|
|
27
|
+
model: "auto",
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("routes overview prompts to Builder chat inside Builder", () => {
|
|
32
|
+
frameState.inBuilderFrame = true;
|
|
33
|
+
|
|
34
|
+
const tabId = submitOverviewPrompt("ship the onboarding flow", "auto");
|
|
35
|
+
|
|
36
|
+
expect(tabId).toBe("chat-tab");
|
|
37
|
+
expect(sendToAgentChatMock).toHaveBeenCalledWith({
|
|
38
|
+
message: "ship the onboarding flow",
|
|
39
|
+
submit: true,
|
|
40
|
+
type: "code",
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("ignores empty prompts", () => {
|
|
45
|
+
expect(submitOverviewPrompt(" ", "auto")).toBeNull();
|
|
46
|
+
expect(sendToAgentChatMock).not.toHaveBeenCalled();
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { isInBuilderFrame, sendToAgentChat } from "@agent-native/core/client";
|
|
2
|
+
|
|
3
|
+
export function submitOverviewPrompt(
|
|
4
|
+
message: string,
|
|
5
|
+
selectedModel?: string | null,
|
|
6
|
+
): string | null {
|
|
7
|
+
const trimmed = message.trim();
|
|
8
|
+
if (!trimmed) return null;
|
|
9
|
+
|
|
10
|
+
if (isInBuilderFrame()) {
|
|
11
|
+
return sendToAgentChat({
|
|
12
|
+
message: trimmed,
|
|
13
|
+
submit: true,
|
|
14
|
+
type: "code",
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return sendToAgentChat({
|
|
19
|
+
message: trimmed,
|
|
20
|
+
submit: true,
|
|
21
|
+
newTab: true,
|
|
22
|
+
model: selectedModel || undefined,
|
|
23
|
+
});
|
|
24
|
+
}
|
|
@@ -2,7 +2,6 @@ import { useEffect, useMemo, useState } from "react";
|
|
|
2
2
|
import { Link } from "react-router";
|
|
3
3
|
import {
|
|
4
4
|
PromptComposer,
|
|
5
|
-
sendToAgentChat,
|
|
6
5
|
useActionQuery,
|
|
7
6
|
useChatModels,
|
|
8
7
|
agentNativePath,
|
|
@@ -34,6 +33,7 @@ import {
|
|
|
34
33
|
TooltipContent,
|
|
35
34
|
TooltipTrigger,
|
|
36
35
|
} from "@/components/ui/tooltip";
|
|
36
|
+
import { submitOverviewPrompt } from "@/lib/overview-chat";
|
|
37
37
|
|
|
38
38
|
interface IntegrationStatus {
|
|
39
39
|
platform: string;
|
|
@@ -99,17 +99,12 @@ function HomeChatPanel() {
|
|
|
99
99
|
const { selectedModel } = useChatModels();
|
|
100
100
|
|
|
101
101
|
const send = (message: string) => {
|
|
102
|
-
|
|
103
|
-
message,
|
|
104
|
-
submit: true,
|
|
105
|
-
newTab: true,
|
|
106
|
-
model: selectedModel || undefined,
|
|
107
|
-
});
|
|
102
|
+
submitOverviewPrompt(message, selectedModel);
|
|
108
103
|
};
|
|
109
104
|
|
|
110
105
|
return (
|
|
111
106
|
<section className="px-2 py-6 sm:py-10">
|
|
112
|
-
<div className="mx-auto w-full max-w-2xl space-y-
|
|
107
|
+
<div className="mx-auto w-full max-w-2xl space-y-8">
|
|
113
108
|
<h1 className="text-center text-2xl font-semibold tracking-tight text-foreground sm:text-3xl">
|
|
114
109
|
What should we do next?
|
|
115
110
|
</h1>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { describe, expect, it } from "vitest";
|
|
5
|
+
|
|
6
|
+
const packageRoot = path.resolve(
|
|
7
|
+
path.dirname(fileURLToPath(import.meta.url)),
|
|
8
|
+
"../..",
|
|
9
|
+
);
|
|
10
|
+
const repoRoot = path.resolve(packageRoot, "../..");
|
|
11
|
+
|
|
12
|
+
describe("dispatch Tailwind styles", () => {
|
|
13
|
+
it("exports package source directives for consuming apps", () => {
|
|
14
|
+
const pkg = JSON.parse(
|
|
15
|
+
fs.readFileSync(path.join(packageRoot, "package.json"), "utf-8"),
|
|
16
|
+
) as { exports?: Record<string, string> };
|
|
17
|
+
const stylesheet = fs.readFileSync(
|
|
18
|
+
path.join(packageRoot, "src/styles/dispatch.css"),
|
|
19
|
+
"utf-8",
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
expect(pkg.exports?.["./styles/dispatch.css"]).toBe(
|
|
23
|
+
"./src/styles/dispatch.css",
|
|
24
|
+
);
|
|
25
|
+
expect(stylesheet).toContain(
|
|
26
|
+
'@source "../components/**/*.{js,mjs,ts,tsx}"',
|
|
27
|
+
);
|
|
28
|
+
expect(stylesheet).toContain('@source "../routes/**/*.{js,mjs,ts,tsx}"');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("imports package source directives from the Dispatch template", () => {
|
|
32
|
+
const globalCss = fs.readFileSync(
|
|
33
|
+
path.join(repoRoot, "templates/dispatch/app/global.css"),
|
|
34
|
+
"utf-8",
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
expect(globalCss).toContain(
|
|
38
|
+
'@import "@agent-native/dispatch/styles/dispatch.css";',
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe("dispatch route shells", () => {
|
|
44
|
+
it("re-exports the index route redirects from the Dispatch template", () => {
|
|
45
|
+
const indexRoute = fs.readFileSync(
|
|
46
|
+
path.join(repoRoot, "templates/dispatch/app/routes/_index.tsx"),
|
|
47
|
+
"utf-8",
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
expect(indexRoute).toContain("loader");
|
|
51
|
+
expect(indexRoute).toContain("clientLoader");
|
|
52
|
+
expect(indexRoute).toContain("HydrateFallback");
|
|
53
|
+
expect(indexRoute).toContain("@agent-native/dispatch/routes/pages/_index");
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Tailwind v4 does not scan package sources in node_modules unless the
|
|
3
|
+
* consuming app opts in. Import this stylesheet from a Dispatch app's global
|
|
4
|
+
* CSS so Tailwind includes the utilities used by packaged Dispatch routes and
|
|
5
|
+
* components.
|
|
6
|
+
*/
|
|
7
|
+
@source "../components/**/*.{js,mjs,ts,tsx}";
|
|
8
|
+
@source "../hooks/**/*.{js,mjs,ts,tsx}";
|
|
9
|
+
@source "../routes/**/*.{js,mjs,ts,tsx}";
|