@agent-native/dispatch 0.25.0 → 0.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/actions/provider-api-register.d.ts +11 -11
- package/dist/components/create-app-popover.d.ts +7 -2
- package/dist/components/create-app-popover.d.ts.map +1 -1
- package/dist/components/create-app-popover.js +4 -3
- package/dist/components/create-app-popover.js.map +1 -1
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +1 -0
- package/dist/components/index.js.map +1 -1
- package/dist/components/layout/Layout.d.ts.map +1 -1
- package/dist/components/layout/Layout.js +42 -2
- package/dist/components/layout/Layout.js.map +1 -1
- package/dist/components/simple-agents-panel.d.ts +4 -1
- package/dist/components/simple-agents-panel.d.ts.map +1 -1
- package/dist/components/simple-agents-panel.js +46 -13
- package/dist/components/simple-agents-panel.js.map +1 -1
- package/dist/components/workspace-app-host.d.ts +3 -1
- package/dist/components/workspace-app-host.d.ts.map +1 -1
- package/dist/components/workspace-app-host.js +11 -2
- package/dist/components/workspace-app-host.js.map +1 -1
- package/package.json +3 -3
- package/src/components/create-app-popover.spec.tsx +9 -1
- package/src/components/create-app-popover.tsx +9 -1
- package/src/components/index.ts +1 -0
- package/src/components/layout/Layout.app-chat.spec.ts +22 -0
- package/src/components/layout/Layout.tsx +72 -3
- package/src/components/simple-agents-panel.spec.tsx +81 -1
- package/src/components/simple-agents-panel.tsx +156 -71
- package/src/components/workspace-app-host.tsx +30 -1
|
@@ -60,6 +60,10 @@ interface CreateAppPopoverProps {
|
|
|
60
60
|
* Override the popover alignment. Defaults to "center" with a 10px offset.
|
|
61
61
|
*/
|
|
62
62
|
align?: "start" | "center" | "end";
|
|
63
|
+
/**
|
|
64
|
+
* Called after the server accepts a Builder app creation request.
|
|
65
|
+
*/
|
|
66
|
+
onCreated?: () => void;
|
|
63
67
|
}
|
|
64
68
|
|
|
65
69
|
async function fetchJson(url: string, init?: RequestInit): Promise<any> {
|
|
@@ -107,9 +111,11 @@ function isErrorFailureReason(reason: string | null): boolean {
|
|
|
107
111
|
*/
|
|
108
112
|
export function CreateAppFlow({
|
|
109
113
|
onClose,
|
|
114
|
+
onCreated,
|
|
110
115
|
className = "",
|
|
111
116
|
}: {
|
|
112
117
|
onClose?: () => void;
|
|
118
|
+
onCreated?: () => void;
|
|
113
119
|
className?: string;
|
|
114
120
|
}) {
|
|
115
121
|
const [step, setStep] = useState<"prompt" | "access">("prompt");
|
|
@@ -269,6 +275,7 @@ export function CreateAppFlow({
|
|
|
269
275
|
},
|
|
270
276
|
);
|
|
271
277
|
if (result?.mode === "builder") {
|
|
278
|
+
onCreated?.();
|
|
272
279
|
setBranchUrl(result?.url || null);
|
|
273
280
|
setStatusMessage("Builder branch created.");
|
|
274
281
|
} else if (result?.mode === "local-agent") {
|
|
@@ -634,6 +641,7 @@ export function CreateAppFlow({
|
|
|
634
641
|
export function CreateAppPopover({
|
|
635
642
|
trigger,
|
|
636
643
|
align = "center",
|
|
644
|
+
onCreated,
|
|
637
645
|
}: CreateAppPopoverProps) {
|
|
638
646
|
const [open, setOpen] = useState(false);
|
|
639
647
|
return (
|
|
@@ -656,7 +664,7 @@ export function CreateAppPopover({
|
|
|
656
664
|
sideOffset={10}
|
|
657
665
|
className="w-[calc(100vw-2rem)] rounded-xl p-3 shadow-xl sm:w-[460px]"
|
|
658
666
|
>
|
|
659
|
-
<CreateAppFlow onClose={() => setOpen(false)} />
|
|
667
|
+
<CreateAppFlow onClose={() => setOpen(false)} onCreated={onCreated} />
|
|
660
668
|
</PopoverContent>
|
|
661
669
|
</Popover>
|
|
662
670
|
);
|
package/src/components/index.ts
CHANGED
|
@@ -18,3 +18,4 @@ export { CreateAppPopover, CreateAppFlow } from "./create-app-popover.js";
|
|
|
18
18
|
export { AppKeysPopover } from "./app-keys-popover.js";
|
|
19
19
|
export { ActionQueryError } from "./action-query-error.js";
|
|
20
20
|
export { SimpleAgentsPanel } from "./simple-agents-panel.js";
|
|
21
|
+
export { WorkspaceAppCard } from "./workspace-app-card.js";
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
|
|
5
|
+
describe("Dispatch workspace app chat rail", () => {
|
|
6
|
+
it("owns the iframe chat toggle and persists app-specific threads", () => {
|
|
7
|
+
const source = readFileSync(
|
|
8
|
+
new URL("./Layout.tsx", import.meta.url),
|
|
9
|
+
"utf8",
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
expect(source).toContain('openStorageKey="dispatch-app-chat"');
|
|
13
|
+
expect(source).toContain(
|
|
14
|
+
"storageKey={`dispatch-app-chat:${workspaceAppId}`}",
|
|
15
|
+
);
|
|
16
|
+
expect(source).toContain("data-dispatch-workspace-app-frame");
|
|
17
|
+
expect(source).toContain("chatSidebar");
|
|
18
|
+
expect(source).toContain(
|
|
19
|
+
'window.dispatchEvent(new Event("agent-panel:toggle"))',
|
|
20
|
+
);
|
|
21
|
+
});
|
|
22
|
+
});
|
|
@@ -1941,6 +1941,39 @@ export function Layout({
|
|
|
1941
1941
|
setSidebarCollapsed(true);
|
|
1942
1942
|
}, [isWorkspaceAppRoute, localPathname]);
|
|
1943
1943
|
|
|
1944
|
+
useEffect(() => {
|
|
1945
|
+
if (!workspaceAppRouteActive && !chatFirstAppTakesMain) return;
|
|
1946
|
+
|
|
1947
|
+
const handleWorkspaceAppMessage = (event: MessageEvent) => {
|
|
1948
|
+
if (event.data?.type !== "agentNative.toggleSidebar") return;
|
|
1949
|
+
const frame = [
|
|
1950
|
+
...document.querySelectorAll<HTMLIFrameElement>(
|
|
1951
|
+
"[data-dispatch-workspace-app-frame]",
|
|
1952
|
+
),
|
|
1953
|
+
].find(
|
|
1954
|
+
(candidate) =>
|
|
1955
|
+
candidate
|
|
1956
|
+
.closest("[data-chat-first-surface-content]")
|
|
1957
|
+
?.getAttribute("aria-hidden") !== "true",
|
|
1958
|
+
);
|
|
1959
|
+
if (!(frame instanceof HTMLIFrameElement)) return;
|
|
1960
|
+
if (event.source !== frame.contentWindow) return;
|
|
1961
|
+
|
|
1962
|
+
const open = event.data.data?.open;
|
|
1963
|
+
if (open === true) {
|
|
1964
|
+
window.dispatchEvent(new Event("agent-panel:open"));
|
|
1965
|
+
} else if (open === false) {
|
|
1966
|
+
window.dispatchEvent(new Event("agent-panel:close"));
|
|
1967
|
+
} else {
|
|
1968
|
+
window.dispatchEvent(new Event("agent-panel:toggle"));
|
|
1969
|
+
}
|
|
1970
|
+
};
|
|
1971
|
+
|
|
1972
|
+
window.addEventListener("message", handleWorkspaceAppMessage);
|
|
1973
|
+
return () =>
|
|
1974
|
+
window.removeEventListener("message", handleWorkspaceAppMessage);
|
|
1975
|
+
}, [chatFirstAppTakesMain, workspaceAppRouteActive]);
|
|
1976
|
+
|
|
1944
1977
|
useEffect(() => {
|
|
1945
1978
|
if (typeof window === "undefined" || isWorkspaceAppHostRoute) return;
|
|
1946
1979
|
try {
|
|
@@ -2077,6 +2110,7 @@ export function Layout({
|
|
|
2077
2110
|
url: registration.url,
|
|
2078
2111
|
}}
|
|
2079
2112
|
embedPath={embedPath}
|
|
2113
|
+
chatSidebar
|
|
2080
2114
|
copy={chatFirstCopy}
|
|
2081
2115
|
/>
|
|
2082
2116
|
);
|
|
@@ -2300,6 +2334,43 @@ export function Layout({
|
|
|
2300
2334
|
renderTab={renderChatFirstSurfaceTab}
|
|
2301
2335
|
/>
|
|
2302
2336
|
) : null;
|
|
2337
|
+
const workspaceAppChatName =
|
|
2338
|
+
(workspaceAppId
|
|
2339
|
+
? chatFirstAppRegistrations.find(
|
|
2340
|
+
(app) => app.id.toLowerCase() === workspaceAppId.toLowerCase(),
|
|
2341
|
+
)?.name
|
|
2342
|
+
: null) ??
|
|
2343
|
+
workspaceAppId ??
|
|
2344
|
+
"Workspace app";
|
|
2345
|
+
const workspaceAppContent =
|
|
2346
|
+
workspaceAppRouteActive && workspaceAppId ? (
|
|
2347
|
+
<AgentSidebar
|
|
2348
|
+
position="left"
|
|
2349
|
+
defaultOpen
|
|
2350
|
+
openStorageKey="dispatch-app-chat"
|
|
2351
|
+
storageKey={`dispatch-app-chat:${workspaceAppId}`}
|
|
2352
|
+
scope={{
|
|
2353
|
+
type: "workspace-app",
|
|
2354
|
+
id: workspaceAppId,
|
|
2355
|
+
label: workspaceAppChatName,
|
|
2356
|
+
contextKey: `workspace-app:${workspaceAppId}`,
|
|
2357
|
+
}}
|
|
2358
|
+
agentChatSurface="app"
|
|
2359
|
+
showTabBar
|
|
2360
|
+
suppressInlineOpenApp
|
|
2361
|
+
dynamicSuggestions={false}
|
|
2362
|
+
suggestions={[]}
|
|
2363
|
+
emptyStateText={`Ask about ${workspaceAppChatName}`}
|
|
2364
|
+
agentPageHref={agentPageHref}
|
|
2365
|
+
onFullscreenRequest={openAskAgentFullscreen}
|
|
2366
|
+
>
|
|
2367
|
+
<WorkspaceAppKeepAlive activeAppId={workspaceAppId} />
|
|
2368
|
+
</AgentSidebar>
|
|
2369
|
+
) : (
|
|
2370
|
+
<WorkspaceAppKeepAlive
|
|
2371
|
+
activeAppId={workspaceAppRouteActive ? workspaceAppId : null}
|
|
2372
|
+
/>
|
|
2373
|
+
);
|
|
2303
2374
|
const content = isChatRoute ? (
|
|
2304
2375
|
<div
|
|
2305
2376
|
className={cn(
|
|
@@ -2438,9 +2509,7 @@ export function Layout({
|
|
|
2438
2509
|
|
|
2439
2510
|
<div className="relative min-w-0 flex-1 overflow-hidden">
|
|
2440
2511
|
{content}
|
|
2441
|
-
|
|
2442
|
-
activeAppId={workspaceAppRouteActive ? workspaceAppId : null}
|
|
2443
|
-
/>
|
|
2512
|
+
{workspaceAppContent}
|
|
2444
2513
|
</div>
|
|
2445
2514
|
</div>
|
|
2446
2515
|
</HeaderActionsProvider>
|
|
@@ -1,10 +1,46 @@
|
|
|
1
|
-
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
import React, { act } from "react";
|
|
3
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
5
|
|
|
3
6
|
import {
|
|
4
7
|
handleAgentPackMutationSuccess,
|
|
5
8
|
isPendingWorkspaceResourceApproval,
|
|
9
|
+
SimpleAgentsPanel,
|
|
6
10
|
} from "./simple-agents-panel";
|
|
7
11
|
|
|
12
|
+
const queryState = vi.hoisted(() => ({
|
|
13
|
+
data: [],
|
|
14
|
+
isError: false,
|
|
15
|
+
isLoading: false,
|
|
16
|
+
error: null,
|
|
17
|
+
refetch: vi.fn(),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
vi.mock("@agent-native/core/client/agent-chat", () => ({
|
|
21
|
+
navigateWithAgentChatViewTransition: vi.fn(),
|
|
22
|
+
sendToAgentChat: vi.fn(),
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
vi.mock("@agent-native/core/client/hooks", () => ({
|
|
26
|
+
useActionMutation: () => ({ mutate: vi.fn(), isPending: false }),
|
|
27
|
+
useActionQuery: () => queryState,
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
vi.mock("@agent-native/core/resources/metadata", () => ({
|
|
31
|
+
parseCustomAgentProfile: vi.fn(),
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
vi.mock("react-router", () => ({ useNavigate: () => vi.fn() }));
|
|
35
|
+
|
|
36
|
+
vi.mock("sonner", () => ({
|
|
37
|
+
toast: {
|
|
38
|
+
error: vi.fn(),
|
|
39
|
+
info: vi.fn(),
|
|
40
|
+
success: vi.fn(),
|
|
41
|
+
},
|
|
42
|
+
}));
|
|
43
|
+
|
|
8
44
|
describe("agent pack resource mutations", () => {
|
|
9
45
|
it("recognizes pending workspace-resource approvals", () => {
|
|
10
46
|
expect(
|
|
@@ -71,3 +107,47 @@ describe("agent pack resource mutations", () => {
|
|
|
71
107
|
expect(notifications).toEqual(["Pack file added", "refreshed"]);
|
|
72
108
|
});
|
|
73
109
|
});
|
|
110
|
+
|
|
111
|
+
describe("SimpleAgentsPanel", () => {
|
|
112
|
+
let container: HTMLDivElement;
|
|
113
|
+
let root: Root;
|
|
114
|
+
|
|
115
|
+
beforeEach(() => {
|
|
116
|
+
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
|
|
117
|
+
queryState.data = [];
|
|
118
|
+
queryState.isError = false;
|
|
119
|
+
queryState.isLoading = false;
|
|
120
|
+
queryState.error = null;
|
|
121
|
+
queryState.refetch.mockReset();
|
|
122
|
+
container = document.createElement("div");
|
|
123
|
+
document.body.appendChild(container);
|
|
124
|
+
root = createRoot(container);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
afterEach(() => {
|
|
128
|
+
act(() => root.unmount());
|
|
129
|
+
container.remove();
|
|
130
|
+
document.body
|
|
131
|
+
.querySelectorAll("[data-radix-portal]")
|
|
132
|
+
.forEach((portal) => portal.remove());
|
|
133
|
+
vi.unstubAllGlobals();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("keeps import and connect available when the workspace has no agents", async () => {
|
|
137
|
+
await act(async () => {
|
|
138
|
+
root.render(<SimpleAgentsPanel />);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const importButton = Array.from(container.querySelectorAll("button")).find(
|
|
142
|
+
(button) => button.textContent?.includes("Import or connect"),
|
|
143
|
+
);
|
|
144
|
+
expect(importButton).not.toBeUndefined();
|
|
145
|
+
|
|
146
|
+
await act(async () => {
|
|
147
|
+
importButton?.click();
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
expect(document.body.textContent).toContain("Import an agent");
|
|
151
|
+
expect(document.body.textContent).toContain("Connect endpoint");
|
|
152
|
+
});
|
|
153
|
+
});
|
|
@@ -10,6 +10,7 @@ import { parseCustomAgentProfile } from "@agent-native/core/resources/metadata";
|
|
|
10
10
|
import {
|
|
11
11
|
IconAdjustmentsHorizontal,
|
|
12
12
|
IconChevronDown,
|
|
13
|
+
IconDotsVertical,
|
|
13
14
|
IconEdit,
|
|
14
15
|
IconFileImport,
|
|
15
16
|
IconFolder,
|
|
@@ -19,7 +20,6 @@ import {
|
|
|
19
20
|
IconPlus,
|
|
20
21
|
IconTrash,
|
|
21
22
|
IconUpload,
|
|
22
|
-
IconUser,
|
|
23
23
|
} from "@tabler/icons-react";
|
|
24
24
|
import {
|
|
25
25
|
useEffect,
|
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
slugifyAgentName,
|
|
37
37
|
} from "../lib/simple-agent-profile.js";
|
|
38
38
|
import { ActionQueryError } from "./action-query-error";
|
|
39
|
+
import { AppIcon } from "./app-icon";
|
|
39
40
|
import {
|
|
40
41
|
AlertDialog,
|
|
41
42
|
AlertDialogAction,
|
|
@@ -63,6 +64,13 @@ import {
|
|
|
63
64
|
DialogTitle,
|
|
64
65
|
DialogTrigger,
|
|
65
66
|
} from "./ui/dialog";
|
|
67
|
+
import {
|
|
68
|
+
DropdownMenu,
|
|
69
|
+
DropdownMenuContent,
|
|
70
|
+
DropdownMenuItem,
|
|
71
|
+
DropdownMenuSeparator,
|
|
72
|
+
DropdownMenuTrigger,
|
|
73
|
+
} from "./ui/dropdown-menu";
|
|
66
74
|
import { Input } from "./ui/input";
|
|
67
75
|
import { Label } from "./ui/label";
|
|
68
76
|
import {
|
|
@@ -101,6 +109,25 @@ interface AgentPackFileInput {
|
|
|
101
109
|
content: string;
|
|
102
110
|
}
|
|
103
111
|
|
|
112
|
+
const AGENT_ICON_KEYS = [
|
|
113
|
+
"brain",
|
|
114
|
+
"users",
|
|
115
|
+
"filetext",
|
|
116
|
+
"chartbar",
|
|
117
|
+
"route",
|
|
118
|
+
"listcheck",
|
|
119
|
+
"code",
|
|
120
|
+
"messagecircle",
|
|
121
|
+
] as const;
|
|
122
|
+
|
|
123
|
+
function agentIconKey(resource: Pick<WorkspaceAgentResource, "id" | "name">) {
|
|
124
|
+
let hash = 0;
|
|
125
|
+
for (const character of `${resource.id}:${resource.name}`) {
|
|
126
|
+
hash = (hash * 31 + character.charCodeAt(0)) | 0;
|
|
127
|
+
}
|
|
128
|
+
return AGENT_ICON_KEYS[Math.abs(hash) % AGENT_ICON_KEYS.length];
|
|
129
|
+
}
|
|
130
|
+
|
|
104
131
|
export function isPendingWorkspaceResourceApproval(result: unknown) {
|
|
105
132
|
if (!result || typeof result !== "object") return false;
|
|
106
133
|
const mutation = result as { status?: unknown; changeType?: unknown };
|
|
@@ -133,6 +160,8 @@ interface AgentEditorProps {
|
|
|
133
160
|
resource?: WorkspaceAgentResource;
|
|
134
161
|
trigger?: ReactNode;
|
|
135
162
|
onSaved?: () => void;
|
|
163
|
+
open?: boolean;
|
|
164
|
+
onOpenChange?: (open: boolean) => void;
|
|
136
165
|
}
|
|
137
166
|
|
|
138
167
|
function profileFields(resource?: WorkspaceAgentResource) {
|
|
@@ -158,9 +187,17 @@ function profileFields(resource?: WorkspaceAgentResource) {
|
|
|
158
187
|
};
|
|
159
188
|
}
|
|
160
189
|
|
|
161
|
-
function AgentEditorDialog({
|
|
190
|
+
function AgentEditorDialog({
|
|
191
|
+
resource,
|
|
192
|
+
trigger,
|
|
193
|
+
onSaved,
|
|
194
|
+
open: controlledOpen,
|
|
195
|
+
onOpenChange: controlledOnOpenChange,
|
|
196
|
+
}: AgentEditorProps) {
|
|
162
197
|
const isEditing = Boolean(resource);
|
|
163
|
-
const [
|
|
198
|
+
const [uncontrolledOpen, setUncontrolledOpen] = useState(false);
|
|
199
|
+
const open = controlledOpen ?? uncontrolledOpen;
|
|
200
|
+
const setOpen = controlledOnOpenChange ?? setUncontrolledOpen;
|
|
164
201
|
const [name, setName] = useState("");
|
|
165
202
|
const [description, setDescription] = useState("");
|
|
166
203
|
const [instructions, setInstructions] = useState("");
|
|
@@ -231,14 +268,16 @@ function AgentEditorDialog({ resource, trigger, onSaved }: AgentEditorProps) {
|
|
|
231
268
|
|
|
232
269
|
return (
|
|
233
270
|
<Dialog open={open} onOpenChange={setOpen}>
|
|
234
|
-
|
|
235
|
-
{trigger
|
|
236
|
-
|
|
271
|
+
{trigger ? (
|
|
272
|
+
<DialogTrigger asChild>{trigger}</DialogTrigger>
|
|
273
|
+
) : controlledOpen === undefined ? (
|
|
274
|
+
<DialogTrigger asChild>
|
|
275
|
+
<Button size="sm">
|
|
237
276
|
<IconPlus size={16} />
|
|
238
277
|
Create agent
|
|
239
278
|
</Button>
|
|
240
|
-
|
|
241
|
-
|
|
279
|
+
</DialogTrigger>
|
|
280
|
+
) : null}
|
|
242
281
|
<DialogContent className="max-w-2xl">
|
|
243
282
|
<DialogHeader>
|
|
244
283
|
<DialogTitle>
|
|
@@ -355,12 +394,18 @@ function AgentPackDialog({
|
|
|
355
394
|
resource,
|
|
356
395
|
onChanged,
|
|
357
396
|
trigger,
|
|
397
|
+
open: controlledOpen,
|
|
398
|
+
onOpenChange: controlledOnOpenChange,
|
|
358
399
|
}: {
|
|
359
400
|
resource: WorkspaceAgentResource;
|
|
360
401
|
onChanged?: () => void;
|
|
361
402
|
trigger?: ReactNode;
|
|
403
|
+
open?: boolean;
|
|
404
|
+
onOpenChange?: (open: boolean) => void;
|
|
362
405
|
}) {
|
|
363
|
-
const [
|
|
406
|
+
const [uncontrolledOpen, setUncontrolledOpen] = useState(false);
|
|
407
|
+
const open = controlledOpen ?? uncontrolledOpen;
|
|
408
|
+
const setOpen = controlledOnOpenChange ?? setUncontrolledOpen;
|
|
364
409
|
const [selectedId, setSelectedId] = useState(resource.id);
|
|
365
410
|
const [content, setContent] = useState(resource.content);
|
|
366
411
|
const [addOpen, setAddOpen] = useState(false);
|
|
@@ -460,14 +505,16 @@ function AgentPackDialog({
|
|
|
460
505
|
if (!nextOpen) setAddOpen(false);
|
|
461
506
|
}}
|
|
462
507
|
>
|
|
463
|
-
|
|
464
|
-
{trigger
|
|
508
|
+
{trigger ? (
|
|
509
|
+
<DialogTrigger asChild>{trigger}</DialogTrigger>
|
|
510
|
+
) : controlledOpen === undefined ? (
|
|
511
|
+
<DialogTrigger asChild>
|
|
465
512
|
<Button variant="ghost" size="sm">
|
|
466
513
|
<IconFolder size={15} />
|
|
467
514
|
Pack
|
|
468
515
|
</Button>
|
|
469
|
-
|
|
470
|
-
|
|
516
|
+
</DialogTrigger>
|
|
517
|
+
) : null}
|
|
471
518
|
<DialogContent className="max-w-4xl">
|
|
472
519
|
<DialogHeader>
|
|
473
520
|
<DialogTitle>Agent pack</DialogTitle>
|
|
@@ -753,7 +800,7 @@ function ImportAgentDialog({ onImported }: { onImported?: () => void }) {
|
|
|
753
800
|
}}
|
|
754
801
|
>
|
|
755
802
|
<DialogTrigger asChild>
|
|
756
|
-
<Button variant="outline">
|
|
803
|
+
<Button variant="outline" size="sm">
|
|
757
804
|
<IconFileImport size={16} />
|
|
758
805
|
Import or connect
|
|
759
806
|
</Button>
|
|
@@ -957,10 +1004,17 @@ function ImportAgentDialog({ onImported }: { onImported?: () => void }) {
|
|
|
957
1004
|
function DeleteAgentButton({
|
|
958
1005
|
resource,
|
|
959
1006
|
onDeleted,
|
|
1007
|
+
open: controlledOpen,
|
|
1008
|
+
onOpenChange: controlledOnOpenChange,
|
|
960
1009
|
}: {
|
|
961
1010
|
resource: WorkspaceAgentResource;
|
|
962
1011
|
onDeleted?: () => void;
|
|
1012
|
+
open?: boolean;
|
|
1013
|
+
onOpenChange?: (open: boolean) => void;
|
|
963
1014
|
}) {
|
|
1015
|
+
const [uncontrolledOpen, setUncontrolledOpen] = useState(false);
|
|
1016
|
+
const open = controlledOpen ?? uncontrolledOpen;
|
|
1017
|
+
const setOpen = controlledOnOpenChange ?? setUncontrolledOpen;
|
|
964
1018
|
const remove = useActionMutation("delete-workspace-resource", {
|
|
965
1019
|
onSuccess: () => {
|
|
966
1020
|
toast.success("Agent removed");
|
|
@@ -970,16 +1024,7 @@ function DeleteAgentButton({
|
|
|
970
1024
|
});
|
|
971
1025
|
|
|
972
1026
|
return (
|
|
973
|
-
<AlertDialog>
|
|
974
|
-
<AlertDialogTrigger asChild>
|
|
975
|
-
<Button
|
|
976
|
-
variant="ghost"
|
|
977
|
-
size="icon"
|
|
978
|
-
aria-label={`Remove ${resource.name}`}
|
|
979
|
-
>
|
|
980
|
-
<IconTrash size={16} />
|
|
981
|
-
</Button>
|
|
982
|
-
</AlertDialogTrigger>
|
|
1027
|
+
<AlertDialog open={open} onOpenChange={setOpen}>
|
|
983
1028
|
<AlertDialogContent>
|
|
984
1029
|
<AlertDialogHeader>
|
|
985
1030
|
<AlertDialogTitle>Remove {resource.name}?</AlertDialogTitle>
|
|
@@ -1011,6 +1056,9 @@ function AgentRow({
|
|
|
1011
1056
|
onSaved?: () => void;
|
|
1012
1057
|
}) {
|
|
1013
1058
|
const navigate = useNavigate();
|
|
1059
|
+
const [packOpen, setPackOpen] = useState(false);
|
|
1060
|
+
const [editorOpen, setEditorOpen] = useState(false);
|
|
1061
|
+
const [deleteOpen, setDeleteOpen] = useState(false);
|
|
1014
1062
|
const packQuery = useActionQuery<AgentPackResponse>(
|
|
1015
1063
|
"list-agent-pack",
|
|
1016
1064
|
{ agentId: resource.id },
|
|
@@ -1072,10 +1120,13 @@ function AgentRow({
|
|
|
1072
1120
|
}
|
|
1073
1121
|
|
|
1074
1122
|
return (
|
|
1075
|
-
<div className="flex items-start gap-3 rounded-
|
|
1076
|
-
<
|
|
1077
|
-
|
|
1078
|
-
|
|
1123
|
+
<div className="flex min-w-0 items-start gap-3 rounded-2xl border bg-card px-4 py-4">
|
|
1124
|
+
<AppIcon
|
|
1125
|
+
id={resource.id}
|
|
1126
|
+
name={resource.name}
|
|
1127
|
+
icon={agentIconKey(resource)}
|
|
1128
|
+
size="sm"
|
|
1129
|
+
/>
|
|
1079
1130
|
<div className="min-w-0 flex-1">
|
|
1080
1131
|
<div className="flex flex-wrap items-center gap-2">
|
|
1081
1132
|
<span className="text-sm font-medium text-foreground">
|
|
@@ -1090,11 +1141,8 @@ function AgentRow({
|
|
|
1090
1141
|
{resource.description}
|
|
1091
1142
|
</div>
|
|
1092
1143
|
) : null}
|
|
1093
|
-
<div className="mt-1 font-mono text-[11px] text-muted-foreground/70">
|
|
1094
|
-
{resource.path}
|
|
1095
|
-
</div>
|
|
1096
1144
|
</div>
|
|
1097
|
-
<div className="flex shrink-0
|
|
1145
|
+
<div className="flex shrink-0 items-center gap-1">
|
|
1098
1146
|
<Button
|
|
1099
1147
|
variant="outline"
|
|
1100
1148
|
size="sm"
|
|
@@ -1104,43 +1152,63 @@ function AgentRow({
|
|
|
1104
1152
|
<IconMessageCircle size={15} />
|
|
1105
1153
|
Chat
|
|
1106
1154
|
</Button>
|
|
1107
|
-
<
|
|
1108
|
-
|
|
1109
|
-
size="sm"
|
|
1110
|
-
onClick={() => void buildApp()}
|
|
1111
|
-
disabled={promote.isPending}
|
|
1112
|
-
aria-label={`Build an app for ${resource.name}`}
|
|
1113
|
-
>
|
|
1114
|
-
<IconLayoutGrid size={15} />
|
|
1115
|
-
{promote.isPending ? "Starting..." : "Build app"}
|
|
1116
|
-
</Button>
|
|
1117
|
-
<AgentPackDialog
|
|
1118
|
-
resource={resource}
|
|
1119
|
-
onChanged={onSaved}
|
|
1120
|
-
trigger={
|
|
1155
|
+
<DropdownMenu>
|
|
1156
|
+
<DropdownMenuTrigger asChild>
|
|
1121
1157
|
<Button
|
|
1122
1158
|
variant="ghost"
|
|
1123
1159
|
size="icon"
|
|
1124
|
-
aria-label={`
|
|
1160
|
+
aria-label={`More actions for ${resource.name}`}
|
|
1161
|
+
title="More actions"
|
|
1125
1162
|
>
|
|
1126
|
-
<
|
|
1163
|
+
<IconDotsVertical size={16} />
|
|
1127
1164
|
</Button>
|
|
1128
|
-
|
|
1165
|
+
</DropdownMenuTrigger>
|
|
1166
|
+
<DropdownMenuContent align="end" className="w-48">
|
|
1167
|
+
<DropdownMenuItem
|
|
1168
|
+
onSelect={() => {
|
|
1169
|
+
void buildApp();
|
|
1170
|
+
}}
|
|
1171
|
+
disabled={promote.isPending}
|
|
1172
|
+
>
|
|
1173
|
+
<IconLayoutGrid className="me-2 size-4" />
|
|
1174
|
+
{promote.isPending ? "Starting..." : "Build app"}
|
|
1175
|
+
</DropdownMenuItem>
|
|
1176
|
+
<DropdownMenuItem onSelect={() => setPackOpen(true)}>
|
|
1177
|
+
<IconFolder className="me-2 size-4" />
|
|
1178
|
+
Manage files
|
|
1179
|
+
</DropdownMenuItem>
|
|
1180
|
+
<DropdownMenuItem onSelect={() => setEditorOpen(true)}>
|
|
1181
|
+
<IconEdit className="me-2 size-4" />
|
|
1182
|
+
Edit
|
|
1183
|
+
</DropdownMenuItem>
|
|
1184
|
+
<DropdownMenuSeparator />
|
|
1185
|
+
<DropdownMenuItem
|
|
1186
|
+
className="text-destructive focus:text-destructive"
|
|
1187
|
+
onSelect={() => setDeleteOpen(true)}
|
|
1188
|
+
>
|
|
1189
|
+
<IconTrash className="me-2 size-4" />
|
|
1190
|
+
Remove
|
|
1191
|
+
</DropdownMenuItem>
|
|
1192
|
+
</DropdownMenuContent>
|
|
1193
|
+
</DropdownMenu>
|
|
1194
|
+
<AgentPackDialog
|
|
1195
|
+
resource={resource}
|
|
1196
|
+
onChanged={onSaved}
|
|
1197
|
+
open={packOpen}
|
|
1198
|
+
onOpenChange={setPackOpen}
|
|
1129
1199
|
/>
|
|
1130
1200
|
<AgentEditorDialog
|
|
1131
1201
|
resource={resource}
|
|
1132
1202
|
onSaved={onSaved}
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
}
|
|
1203
|
+
open={editorOpen}
|
|
1204
|
+
onOpenChange={setEditorOpen}
|
|
1205
|
+
/>
|
|
1206
|
+
<DeleteAgentButton
|
|
1207
|
+
resource={resource}
|
|
1208
|
+
onDeleted={onDeleted}
|
|
1209
|
+
open={deleteOpen}
|
|
1210
|
+
onOpenChange={setDeleteOpen}
|
|
1142
1211
|
/>
|
|
1143
|
-
<DeleteAgentButton resource={resource} onDeleted={onDeleted} />
|
|
1144
1212
|
</div>
|
|
1145
1213
|
</div>
|
|
1146
1214
|
);
|
|
@@ -1160,12 +1228,18 @@ interface AgentAppCreationResult {
|
|
|
1160
1228
|
url?: string;
|
|
1161
1229
|
}
|
|
1162
1230
|
|
|
1163
|
-
export function SimpleAgentsPanel(
|
|
1231
|
+
export function SimpleAgentsPanel({
|
|
1232
|
+
title,
|
|
1233
|
+
}: {
|
|
1234
|
+
title?: ReactNode;
|
|
1235
|
+
} = {}) {
|
|
1164
1236
|
const query = useActionQuery<WorkspaceAgentResource[]>(
|
|
1165
1237
|
"list-workspace-resources",
|
|
1166
1238
|
{ kind: "agent" },
|
|
1167
1239
|
);
|
|
1168
1240
|
const agents = query.data ?? [];
|
|
1241
|
+
const refreshAgents = () => void query.refetch();
|
|
1242
|
+
const importAction = <ImportAgentDialog onImported={refreshAgents} />;
|
|
1169
1243
|
|
|
1170
1244
|
if (query.isError) {
|
|
1171
1245
|
return (
|
|
@@ -1178,21 +1252,32 @@ export function SimpleAgentsPanel() {
|
|
|
1178
1252
|
|
|
1179
1253
|
return (
|
|
1180
1254
|
<section className="flex flex-col gap-4">
|
|
1181
|
-
|
|
1182
|
-
<
|
|
1183
|
-
|
|
1184
|
-
|
|
1255
|
+
{title || agents.length > 0 ? (
|
|
1256
|
+
<div
|
|
1257
|
+
className={`flex flex-wrap items-center gap-3 ${title ? "justify-between" : "justify-end"}`}
|
|
1258
|
+
>
|
|
1259
|
+
{title ? (
|
|
1260
|
+
<h2 className="text-base font-medium text-foreground">{title}</h2>
|
|
1261
|
+
) : null}
|
|
1262
|
+
{agents.length > 0 ? (
|
|
1263
|
+
<div className="flex flex-wrap items-center justify-end gap-2">
|
|
1264
|
+
{importAction}
|
|
1265
|
+
<AgentEditorDialog onSaved={refreshAgents} />
|
|
1266
|
+
</div>
|
|
1267
|
+
) : null}
|
|
1268
|
+
</div>
|
|
1269
|
+
) : null}
|
|
1185
1270
|
{query.isLoading && agents.length === 0 ? (
|
|
1186
|
-
<div className="
|
|
1271
|
+
<div className="grid gap-3 md:grid-cols-2">
|
|
1187
1272
|
{[0, 1, 2].map((item) => (
|
|
1188
|
-
<div key={item} className="rounded-
|
|
1273
|
+
<div key={item} className="rounded-2xl border bg-card px-4 py-4">
|
|
1189
1274
|
<Skeleton className="h-4 w-1/3" />
|
|
1190
1275
|
<Skeleton className="mt-2 h-3 w-2/3" />
|
|
1191
1276
|
</div>
|
|
1192
1277
|
))}
|
|
1193
1278
|
</div>
|
|
1194
1279
|
) : agents.length > 0 ? (
|
|
1195
|
-
<div className="
|
|
1280
|
+
<div className="grid gap-3 md:grid-cols-2">
|
|
1196
1281
|
{agents.map((agent) => (
|
|
1197
1282
|
<AgentRow
|
|
1198
1283
|
key={agent.id}
|
|
@@ -1203,21 +1288,21 @@ export function SimpleAgentsPanel() {
|
|
|
1203
1288
|
))}
|
|
1204
1289
|
</div>
|
|
1205
1290
|
) : (
|
|
1206
|
-
<div className="rounded-
|
|
1291
|
+
<div className="rounded-2xl border border-dashed bg-card px-4 py-12 text-center">
|
|
1207
1292
|
<div className="text-sm font-medium text-foreground">
|
|
1208
1293
|
No agents yet
|
|
1209
1294
|
</div>
|
|
1210
1295
|
<div className="mt-4 flex flex-wrap justify-center gap-2">
|
|
1211
1296
|
<AgentEditorDialog
|
|
1212
|
-
onSaved={
|
|
1297
|
+
onSaved={refreshAgents}
|
|
1213
1298
|
trigger={
|
|
1214
|
-
<Button>
|
|
1299
|
+
<Button size="sm">
|
|
1215
1300
|
<IconPlus size={16} />
|
|
1216
1301
|
Create an agent
|
|
1217
1302
|
</Button>
|
|
1218
1303
|
}
|
|
1219
1304
|
/>
|
|
1220
|
-
|
|
1305
|
+
{importAction}
|
|
1221
1306
|
</div>
|
|
1222
1307
|
</div>
|
|
1223
1308
|
)}
|