@polpo-ai/dashboard 0.1.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/LICENSE +13 -0
- package/README.md +44 -0
- package/app/(dashboard)/error.tsx +47 -0
- package/app/(dashboard)/layout.tsx +41 -0
- package/app/(dashboard)/loading.tsx +30 -0
- package/app/(dashboard)/projects/[id]/agents/[name]/memory/view.tsx +36 -0
- package/app/(dashboard)/projects/[id]/agents/[name]/models-view.tsx +30 -0
- package/app/(dashboard)/projects/[id]/agents/[name]/page.tsx +51 -0
- package/app/(dashboard)/projects/[id]/agents/[name]/prompt/view.tsx +211 -0
- package/app/(dashboard)/projects/[id]/agents/[name]/skills/view.tsx +84 -0
- package/app/(dashboard)/projects/[id]/agents/[name]/tools/view.tsx +525 -0
- package/app/(dashboard)/projects/[id]/agents/[name]/vault/view.tsx +61 -0
- package/app/(dashboard)/projects/[id]/agents/page.tsx +39 -0
- package/app/(dashboard)/projects/[id]/agents/view.tsx +283 -0
- package/app/(dashboard)/projects/[id]/layout.tsx +11 -0
- package/app/(dashboard)/projects/[id]/logs/page.tsx +27 -0
- package/app/(dashboard)/projects/[id]/logs/view.tsx +184 -0
- package/app/(dashboard)/projects/[id]/memory/page.tsx +40 -0
- package/app/(dashboard)/projects/[id]/memory/view.tsx +17 -0
- package/app/(dashboard)/projects/[id]/missions/[missionId]/page.tsx +88 -0
- package/app/(dashboard)/projects/[id]/missions/[missionId]/view.tsx +569 -0
- package/app/(dashboard)/projects/[id]/missions/page.tsx +32 -0
- package/app/(dashboard)/projects/[id]/missions/view.tsx +282 -0
- package/app/(dashboard)/projects/[id]/onboarding-checklist.tsx +359 -0
- package/app/(dashboard)/projects/[id]/page.tsx +68 -0
- package/app/(dashboard)/projects/[id]/playbooks/[name]/page.tsx +5 -0
- package/app/(dashboard)/projects/[id]/playbooks/[name]/view.tsx +433 -0
- package/app/(dashboard)/projects/[id]/playbooks/page.tsx +5 -0
- package/app/(dashboard)/projects/[id]/playbooks/view.tsx +222 -0
- package/app/(dashboard)/projects/[id]/schedules/page.tsx +44 -0
- package/app/(dashboard)/projects/[id]/schedules/view.tsx +385 -0
- package/app/(dashboard)/projects/[id]/sessions/[sessionId]/page.tsx +55 -0
- package/app/(dashboard)/projects/[id]/sessions/[sessionId]/raw-view.tsx +373 -0
- package/app/(dashboard)/projects/[id]/sessions/[sessionId]/view.tsx +124 -0
- package/app/(dashboard)/projects/[id]/sessions/page.tsx +31 -0
- package/app/(dashboard)/projects/[id]/sessions/view.tsx +242 -0
- package/app/(dashboard)/projects/[id]/settings/page.tsx +52 -0
- package/app/(dashboard)/projects/[id]/skills/[name]/page.tsx +65 -0
- package/app/(dashboard)/projects/[id]/skills/[name]/view.tsx +227 -0
- package/app/(dashboard)/projects/[id]/skills/page.tsx +31 -0
- package/app/(dashboard)/projects/[id]/skills/view.tsx +243 -0
- package/app/(dashboard)/projects/[id]/storage/page.tsx +5 -0
- package/app/(dashboard)/projects/[id]/storage/view.tsx +385 -0
- package/app/(dashboard)/projects/[id]/tasks/[taskId]/activity/view.tsx +336 -0
- package/app/(dashboard)/projects/[id]/tasks/[taskId]/assessment/view.tsx +113 -0
- package/app/(dashboard)/projects/[id]/tasks/[taskId]/blueprint.ts +72 -0
- package/app/(dashboard)/projects/[id]/tasks/[taskId]/output/view.tsx +90 -0
- package/app/(dashboard)/projects/[id]/tasks/[taskId]/page.tsx +63 -0
- package/app/(dashboard)/projects/[id]/tasks/[taskId]/view.tsx +137 -0
- package/app/(dashboard)/projects/[id]/tasks/page.tsx +40 -0
- package/app/(dashboard)/projects/[id]/tasks/view.tsx +421 -0
- package/app/(dashboard)/projects/[id]/view.tsx +421 -0
- package/app/(dashboard)/projects/[id]/welcome-banner.tsx +156 -0
- package/app/(dashboard)/projects/projects-list.tsx +126 -0
- package/app/(playground)/error.tsx +42 -0
- package/app/(playground)/layout.tsx +27 -0
- package/app/(playground)/projects/[id]/playground/page.tsx +57 -0
- package/app/(playground)/projects/[id]/playground/view.tsx +167 -0
- package/app/(playground)/projects/[id]/playground-legacy/page.tsx +47 -0
- package/app/(playground)/projects/[id]/playground-legacy/skeleton.tsx +63 -0
- package/app/(playground)/projects/[id]/playground-legacy/view.tsx +237 -0
- package/app/favicon.ico +0 -0
- package/app/globals.css +385 -0
- package/app/icon.svg +6 -0
- package/app/layout.tsx +39 -0
- package/app/page.tsx +11 -0
- package/components/ai-elements/code-block.tsx +562 -0
- package/components/dashboard/agent-capabilities.tsx +326 -0
- package/components/dashboard/agent-identity-header.tsx +50 -0
- package/components/dashboard/agent-model-picker.tsx +195 -0
- package/components/dashboard/agent-model-selector.tsx +121 -0
- package/components/dashboard/agent-picker-card.tsx +46 -0
- package/components/dashboard/agent-studio.tsx +242 -0
- package/components/dashboard/agents-table.tsx +48 -0
- package/components/dashboard/breadcrumb.tsx +70 -0
- package/components/dashboard/builder-chat.tsx +418 -0
- package/components/dashboard/chat-shell.tsx +40 -0
- package/components/dashboard/client-picker.tsx +63 -0
- package/components/dashboard/command-snippet.tsx +77 -0
- package/components/dashboard/connect-dialog.tsx +680 -0
- package/components/dashboard/copy-button.tsx +45 -0
- package/components/dashboard/copy-card.tsx +48 -0
- package/components/dashboard/file-browser.tsx +161 -0
- package/components/dashboard/hint.tsx +22 -0
- package/components/dashboard/inference-mode.tsx +15 -0
- package/components/dashboard/manual-refresh-button.tsx +52 -0
- package/components/dashboard/markdown.tsx +31 -0
- package/components/dashboard/mcp-install-panel.tsx +196 -0
- package/components/dashboard/mission-graph.tsx +262 -0
- package/components/dashboard/nav-tabs.tsx +86 -0
- package/components/dashboard/polpo-chat.tsx +468 -0
- package/components/dashboard/project-copilot.tsx +253 -0
- package/components/dashboard/sdk-snippet-panel.tsx +194 -0
- package/components/dashboard/section-header.tsx +37 -0
- package/components/dashboard/settings-form.tsx +2477 -0
- package/components/dashboard/sidebar.tsx +385 -0
- package/components/dashboard/skeletons.tsx +1054 -0
- package/components/dashboard/skills-install-wizard.tsx +273 -0
- package/components/dashboard/swarm-runs.tsx +316 -0
- package/components/dashboard/tab-toggle.tsx +35 -0
- package/components/dashboard/task-studio.tsx +224 -0
- package/components/dashboard/top-header.tsx +203 -0
- package/components/dashboard/webhook-deliveries.tsx +234 -0
- package/components/icons/coding-agents.tsx +151 -0
- package/components/json-ld.tsx +8 -0
- package/components/providers.tsx +25 -0
- package/components/ui/badge.tsx +52 -0
- package/components/ui/button.tsx +60 -0
- package/components/ui/card.tsx +103 -0
- package/components/ui/chart.tsx +356 -0
- package/components/ui/command.tsx +196 -0
- package/components/ui/dialog.tsx +157 -0
- package/components/ui/input-group.tsx +158 -0
- package/components/ui/input.tsx +20 -0
- package/components/ui/multi-select.tsx +161 -0
- package/components/ui/popover.tsx +90 -0
- package/components/ui/select.tsx +201 -0
- package/components/ui/separator.tsx +25 -0
- package/components/ui/sheet.tsx +135 -0
- package/components/ui/skeleton.tsx +13 -0
- package/components/ui/tabs.tsx +56 -0
- package/components/ui/textarea.tsx +18 -0
- package/components/ui/tooltip.tsx +66 -0
- package/hooks/use-desktop-sidebar.tsx +50 -0
- package/hooks/use-mobile-sidebar.tsx +37 -0
- package/index.ts +14 -0
- package/lib/api.ts +194 -0
- package/lib/builder-context.ts +60 -0
- package/lib/data-client.ts +68 -0
- package/lib/get-query-client.ts +25 -0
- package/lib/polpo-client.tsx +49 -0
- package/lib/tool-catalog.ts +234 -0
- package/lib/use-event-catalog.ts +28 -0
- package/lib/utils.ts +6 -0
- package/package.json +99 -0
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { useRouter } from "next/navigation";
|
|
5
|
+
import {
|
|
6
|
+
Lock,
|
|
7
|
+
Loader2,
|
|
8
|
+
Terminal,
|
|
9
|
+
Globe,
|
|
10
|
+
Image as ImageIcon,
|
|
11
|
+
Video,
|
|
12
|
+
Volume2,
|
|
13
|
+
Search,
|
|
14
|
+
Mail,
|
|
15
|
+
Sheet,
|
|
16
|
+
FileText,
|
|
17
|
+
FileType,
|
|
18
|
+
Brain,
|
|
19
|
+
// per-tool icons
|
|
20
|
+
FilePlus,
|
|
21
|
+
FilePen,
|
|
22
|
+
SquareTerminal,
|
|
23
|
+
FileSearch,
|
|
24
|
+
TextSearch,
|
|
25
|
+
FolderTree,
|
|
26
|
+
Download,
|
|
27
|
+
Flag,
|
|
28
|
+
KeyRound,
|
|
29
|
+
Compass,
|
|
30
|
+
ScanText,
|
|
31
|
+
MousePointerClick,
|
|
32
|
+
Keyboard,
|
|
33
|
+
Camera,
|
|
34
|
+
Eye,
|
|
35
|
+
ListChecks,
|
|
36
|
+
MousePointer2,
|
|
37
|
+
MousePointer,
|
|
38
|
+
Clock,
|
|
39
|
+
Code,
|
|
40
|
+
X,
|
|
41
|
+
ArrowLeft,
|
|
42
|
+
ArrowRight,
|
|
43
|
+
RotateCw,
|
|
44
|
+
Columns3,
|
|
45
|
+
AudioLines,
|
|
46
|
+
Send,
|
|
47
|
+
Inbox,
|
|
48
|
+
MailOpen,
|
|
49
|
+
Paperclip,
|
|
50
|
+
ShieldCheck,
|
|
51
|
+
Hash,
|
|
52
|
+
Table,
|
|
53
|
+
Info,
|
|
54
|
+
Merge,
|
|
55
|
+
Save,
|
|
56
|
+
Plus,
|
|
57
|
+
Pencil,
|
|
58
|
+
type LucideIcon,
|
|
59
|
+
} from "lucide-react";
|
|
60
|
+
import type { AgentConfig } from "@polpo-ai/core";
|
|
61
|
+
import { SectionHeader } from "#/components/dashboard/section-header";
|
|
62
|
+
import {
|
|
63
|
+
TOOL_CATALOG,
|
|
64
|
+
CATALOG_TOOL_NAMES,
|
|
65
|
+
isToolEnabled,
|
|
66
|
+
hasGroupWildcard,
|
|
67
|
+
} from "#/lib/tool-catalog";
|
|
68
|
+
|
|
69
|
+
const API_URL = process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:4000";
|
|
70
|
+
|
|
71
|
+
const ICONS: Record<string, LucideIcon> = {
|
|
72
|
+
Terminal,
|
|
73
|
+
Globe,
|
|
74
|
+
Image: ImageIcon,
|
|
75
|
+
Video,
|
|
76
|
+
Volume2,
|
|
77
|
+
Search,
|
|
78
|
+
Mail,
|
|
79
|
+
Sheet,
|
|
80
|
+
FileText,
|
|
81
|
+
FileType,
|
|
82
|
+
Brain,
|
|
83
|
+
ShieldCheck,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Tools the runtime keeps ON only while `allowedTools` is unset — the moment
|
|
88
|
+
* an agent sets any `allowedTools`, the core filters these out unless they're
|
|
89
|
+
* explicitly listed. So any edit must first "materialise" this baseline (pin
|
|
90
|
+
* the names) to avoid silently stripping the agent's file/shell/HTTP tools.
|
|
91
|
+
*/
|
|
92
|
+
const BASELINE_TOOLS = [
|
|
93
|
+
"read", "write", "edit", "bash", "glob", "grep", "ls", "http_fetch", "http_download",
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
/** Empty allowedTools ⇒ runtime gives all baseline tools. Pin them before
|
|
97
|
+
* any edit so they survive once the list becomes non-empty. */
|
|
98
|
+
const materialize = (allowed: string[]): string[] =>
|
|
99
|
+
allowed.length === 0 ? [...BASELINE_TOOLS] : allowed;
|
|
100
|
+
|
|
101
|
+
/** Reverse tidy: if the list ends up as exactly the full baseline (nothing
|
|
102
|
+
* extra, nothing missing), collapse back to [] — the clean "defaults" state. */
|
|
103
|
+
function tidyBaseline(allowed: string[]): string[] {
|
|
104
|
+
const onlyBaseline = allowed.length > 0 && allowed.every((t) => BASELINE_TOOLS.includes(t));
|
|
105
|
+
const allOn = BASELINE_TOOLS.every((t) => allowed.includes(t));
|
|
106
|
+
return onlyBaseline && allOn ? [] : allowed;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Explicit (no-wildcard) single-tool toggle for baseline groups. */
|
|
110
|
+
function toggleToolExplicit(base: string[], name: string, enable: boolean): string[] {
|
|
111
|
+
if (enable) return base.includes(name) ? base : [...base, name];
|
|
112
|
+
return base.filter((t) => t !== name);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Explicit (no-wildcard) whole-group toggle for baseline groups. */
|
|
116
|
+
function toggleCategoryExplicit(base: string[], names: string[], enable: boolean): string[] {
|
|
117
|
+
const without = base.filter((t) => !names.includes(t));
|
|
118
|
+
return enable ? [...without, ...names] : without;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** An appropriate icon per individual tool. Falls back to the category
|
|
122
|
+
* icon when a tool isn't mapped. */
|
|
123
|
+
const TOOL_ICONS: Record<string, LucideIcon> = {
|
|
124
|
+
// core
|
|
125
|
+
read: FileText, write: FilePlus, edit: FilePen, bash: SquareTerminal,
|
|
126
|
+
glob: FileSearch, grep: TextSearch, ls: FolderTree, http_fetch: Globe,
|
|
127
|
+
http_download: Download, register_outcome: Flag, vault_get: KeyRound, vault_list: KeyRound,
|
|
128
|
+
// browser
|
|
129
|
+
browser_navigate: Compass, browser_snapshot: ScanText, browser_click: MousePointerClick,
|
|
130
|
+
browser_fill: FilePen, browser_type: Keyboard, browser_press: Keyboard,
|
|
131
|
+
browser_screenshot: Camera, browser_get: Eye, browser_select: ListChecks,
|
|
132
|
+
browser_hover: MousePointer2, browser_scroll: MousePointer, browser_wait: Clock,
|
|
133
|
+
browser_eval: Code, browser_close: X, browser_back: ArrowLeft, browser_forward: ArrowRight,
|
|
134
|
+
browser_reload: RotateCw, browser_tabs: Columns3,
|
|
135
|
+
// image / video
|
|
136
|
+
image_generate: ImageIcon, image_analyze: Eye, video_generate: Video,
|
|
137
|
+
// audio
|
|
138
|
+
audio_transcribe: AudioLines, audio_speak: Volume2,
|
|
139
|
+
// search
|
|
140
|
+
search_web: Search, search_find_similar: Search,
|
|
141
|
+
// email
|
|
142
|
+
email_send: Send, email_draft: FilePen, email_list: Inbox, email_read: MailOpen,
|
|
143
|
+
email_search: Search, email_download_attachment: Paperclip, email_verify: ShieldCheck,
|
|
144
|
+
email_count: Hash,
|
|
145
|
+
// excel
|
|
146
|
+
excel_read: Table, excel_write: Sheet, excel_query: Search, excel_info: Info,
|
|
147
|
+
// pdf / docx
|
|
148
|
+
pdf_read: FileText, pdf_create: FilePlus, pdf_merge: Merge, pdf_info: Info,
|
|
149
|
+
docx_read: FileText, docx_create: FilePlus,
|
|
150
|
+
// memory
|
|
151
|
+
memory_get: Brain, memory_save: Save, memory_append: Plus, memory_update: Pencil,
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const iconForTool = (name: string, fallback: LucideIcon): LucideIcon =>
|
|
155
|
+
TOOL_ICONS[name] ?? fallback;
|
|
156
|
+
|
|
157
|
+
/* ── allowedTools edit helpers ──────────────────────────────────── */
|
|
158
|
+
|
|
159
|
+
const wildcardOf = (groupKey: string) => `${groupKey}*`;
|
|
160
|
+
|
|
161
|
+
/** Expand a group wildcard into its explicit tool names (so a single
|
|
162
|
+
* tool can then be toggled off without disabling the whole group). */
|
|
163
|
+
function expandWildcard(allowed: string[], groupKey: string, groupTools: string[]) {
|
|
164
|
+
const wc = wildcardOf(groupKey);
|
|
165
|
+
if (!allowed.includes(wc)) return allowed;
|
|
166
|
+
const next = allowed.filter((t) => t !== wc);
|
|
167
|
+
for (const n of groupTools) if (!next.includes(n)) next.push(n);
|
|
168
|
+
return next;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function toggleTool(
|
|
172
|
+
allowed: string[],
|
|
173
|
+
groupKey: string,
|
|
174
|
+
groupTools: string[],
|
|
175
|
+
name: string,
|
|
176
|
+
enable: boolean,
|
|
177
|
+
): string[] {
|
|
178
|
+
let next = expandWildcard(allowed, groupKey, groupTools);
|
|
179
|
+
if (enable) {
|
|
180
|
+
if (!next.includes(name)) next = [...next, name];
|
|
181
|
+
} else {
|
|
182
|
+
next = next.filter((t) => t !== name);
|
|
183
|
+
}
|
|
184
|
+
// Tidy: all group tools on → collapse back to the wildcard.
|
|
185
|
+
if (groupTools.length > 1 && groupTools.every((n) => next.includes(n))) {
|
|
186
|
+
next = next.filter((t) => !groupTools.includes(t));
|
|
187
|
+
next.push(wildcardOf(groupKey));
|
|
188
|
+
}
|
|
189
|
+
return next;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function toggleCategory(
|
|
193
|
+
allowed: string[],
|
|
194
|
+
groupKey: string,
|
|
195
|
+
groupTools: string[],
|
|
196
|
+
enable: boolean,
|
|
197
|
+
): string[] {
|
|
198
|
+
const next = allowed.filter(
|
|
199
|
+
(t) => t !== wildcardOf(groupKey) && !groupTools.includes(t),
|
|
200
|
+
);
|
|
201
|
+
if (enable) next.push(wildcardOf(groupKey));
|
|
202
|
+
return next;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/* ── Sharp toggle switch ────────────────────────────────────────── */
|
|
206
|
+
|
|
207
|
+
function Switch({
|
|
208
|
+
checked,
|
|
209
|
+
onChange,
|
|
210
|
+
disabled,
|
|
211
|
+
}: {
|
|
212
|
+
checked: boolean;
|
|
213
|
+
onChange: (v: boolean) => void;
|
|
214
|
+
disabled?: boolean;
|
|
215
|
+
}) {
|
|
216
|
+
return (
|
|
217
|
+
<button
|
|
218
|
+
type="button"
|
|
219
|
+
role="switch"
|
|
220
|
+
aria-checked={checked}
|
|
221
|
+
disabled={disabled}
|
|
222
|
+
onClick={() => onChange(!checked)}
|
|
223
|
+
className={`relative inline-flex h-4 w-7 shrink-0 items-center border transition-colors ${
|
|
224
|
+
checked ? "border-emerald-500 bg-emerald-500/20" : "border-border bg-secondary"
|
|
225
|
+
} ${disabled ? "cursor-not-allowed opacity-40" : "cursor-pointer"}`}
|
|
226
|
+
>
|
|
227
|
+
<span
|
|
228
|
+
aria-hidden
|
|
229
|
+
className={`block h-3 w-3 transition-transform ${
|
|
230
|
+
checked ? "translate-x-[13px] bg-emerald-500" : "translate-x-[1px] bg-muted-foreground"
|
|
231
|
+
}`}
|
|
232
|
+
/>
|
|
233
|
+
</button>
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
interface CategoryTab {
|
|
238
|
+
key: string;
|
|
239
|
+
label: string;
|
|
240
|
+
icon: LucideIcon;
|
|
241
|
+
core?: boolean;
|
|
242
|
+
baseline?: boolean;
|
|
243
|
+
requiresKey?: boolean;
|
|
244
|
+
note?: string;
|
|
245
|
+
/** Catalog tool names of this group (for toggle math). */
|
|
246
|
+
groupTools: string[];
|
|
247
|
+
wildcard: boolean;
|
|
248
|
+
enabled: number;
|
|
249
|
+
total: number;
|
|
250
|
+
tools: { name: string; description: string; enabled: boolean }[];
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export default function AgentToolsView({
|
|
254
|
+
agent,
|
|
255
|
+
projectId,
|
|
256
|
+
}: {
|
|
257
|
+
agent: AgentConfig | null;
|
|
258
|
+
projectId: string;
|
|
259
|
+
}) {
|
|
260
|
+
const router = useRouter();
|
|
261
|
+
const [allowed, setAllowed] = useState<string[]>(agent?.allowedTools ?? []);
|
|
262
|
+
const [saving, setSaving] = useState(false);
|
|
263
|
+
|
|
264
|
+
const customTools = allowed.filter(
|
|
265
|
+
(t) => !t.endsWith("*") && !CATALOG_TOOL_NAMES.has(t),
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
// Baseline groups are all-on while allowedTools is unset (mirrors runtime).
|
|
269
|
+
const baselineDefault = allowed.length === 0;
|
|
270
|
+
|
|
271
|
+
const tabs: CategoryTab[] = TOOL_CATALOG.map((g) => {
|
|
272
|
+
const groupTools = g.tools.map((t) => t.name);
|
|
273
|
+
const tools = g.tools.map((t) => ({
|
|
274
|
+
name: t.name,
|
|
275
|
+
description: t.description,
|
|
276
|
+
enabled: g.core || (g.baseline && baselineDefault) || isToolEnabled(t.name, allowed),
|
|
277
|
+
}));
|
|
278
|
+
return {
|
|
279
|
+
key: g.key,
|
|
280
|
+
label: g.label,
|
|
281
|
+
icon: ICONS[g.icon] ?? Terminal,
|
|
282
|
+
core: g.core,
|
|
283
|
+
baseline: g.baseline,
|
|
284
|
+
requiresKey: g.requiresKey,
|
|
285
|
+
note: g.note,
|
|
286
|
+
groupTools,
|
|
287
|
+
// Baseline groups never collapse to a wildcard (their names have no
|
|
288
|
+
// shared prefix the runtime understands) — always explicit.
|
|
289
|
+
wildcard: !g.core && !g.baseline && hasGroupWildcard(g.key, allowed),
|
|
290
|
+
enabled: tools.filter((t) => t.enabled).length,
|
|
291
|
+
total: tools.length,
|
|
292
|
+
tools,
|
|
293
|
+
};
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
if (customTools.length > 0) {
|
|
297
|
+
tabs.push({
|
|
298
|
+
key: "__custom",
|
|
299
|
+
label: "Custom",
|
|
300
|
+
icon: Terminal,
|
|
301
|
+
groupTools: customTools,
|
|
302
|
+
wildcard: false,
|
|
303
|
+
enabled: customTools.length,
|
|
304
|
+
total: customTools.length,
|
|
305
|
+
note: "Tools enabled for this agent that aren't in the standard catalog.",
|
|
306
|
+
tools: customTools.map((name) => ({
|
|
307
|
+
name,
|
|
308
|
+
description: "Custom tool enabled for this agent.",
|
|
309
|
+
enabled: true,
|
|
310
|
+
})),
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const firstActive =
|
|
315
|
+
tabs.find((t) => !t.core && t.enabled > 0)?.key ?? tabs[0]?.key ?? "core";
|
|
316
|
+
const [active, setActive] = useState(firstActive);
|
|
317
|
+
|
|
318
|
+
async function persist(next: string[]) {
|
|
319
|
+
if (!agent) return;
|
|
320
|
+
const prev = allowed;
|
|
321
|
+
setAllowed(next); // optimistic
|
|
322
|
+
setSaving(true);
|
|
323
|
+
try {
|
|
324
|
+
const res = await fetch(
|
|
325
|
+
`${API_URL}/v1/projects/${projectId}/data/v1/agents/${encodeURIComponent(agent.name)}`,
|
|
326
|
+
{
|
|
327
|
+
method: "PATCH",
|
|
328
|
+
credentials: "include",
|
|
329
|
+
headers: { "Content-Type": "application/json" },
|
|
330
|
+
body: JSON.stringify({ allowedTools: next }),
|
|
331
|
+
},
|
|
332
|
+
);
|
|
333
|
+
if (!res.ok) {
|
|
334
|
+
setAllowed(prev); // rollback
|
|
335
|
+
} else {
|
|
336
|
+
router.refresh();
|
|
337
|
+
}
|
|
338
|
+
} catch {
|
|
339
|
+
setAllowed(prev);
|
|
340
|
+
}
|
|
341
|
+
setSaving(false);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** Run a toggle against the materialised baseline, then tidy + persist.
|
|
345
|
+
* Every edit goes through here so enabling an extended tool never silently
|
|
346
|
+
* strips the file/shell/HTTP baseline. */
|
|
347
|
+
const applyEdit = (fn: (base: string[]) => string[]) =>
|
|
348
|
+
persist(tidyBaseline(fn(materialize(allowed))));
|
|
349
|
+
|
|
350
|
+
if (!agent) {
|
|
351
|
+
return (
|
|
352
|
+
<div
|
|
353
|
+
data-testid="agent-not-found"
|
|
354
|
+
className="border border-border p-8 text-center text-sm text-muted-foreground"
|
|
355
|
+
>
|
|
356
|
+
Agent not found.
|
|
357
|
+
</div>
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const activeTab = tabs.find((t) => t.key === active) ?? tabs[0];
|
|
362
|
+
|
|
363
|
+
return (
|
|
364
|
+
<div>
|
|
365
|
+
<SectionHeader
|
|
366
|
+
description={
|
|
367
|
+
<span className="inline-flex items-center gap-1.5">
|
|
368
|
+
Toggle the tools this agent can call. Vault access is always on; file,
|
|
369
|
+
shell and HTTP tools are on until you customise the selection.
|
|
370
|
+
{saving && <Loader2 className="h-3 w-3 animate-spin" />}
|
|
371
|
+
</span>
|
|
372
|
+
}
|
|
373
|
+
/>
|
|
374
|
+
|
|
375
|
+
<div className="mt-4 flex flex-col gap-6 md:flex-row md:gap-8">
|
|
376
|
+
{/* Vertical category nav — sticky; only the tool list (right) scrolls. */}
|
|
377
|
+
<nav className="flex shrink-0 flex-col gap-0.5 md:w-48 md:self-start md:sticky md:top-0">
|
|
378
|
+
{tabs.map((t) => {
|
|
379
|
+
const Icon = t.icon;
|
|
380
|
+
const isActive = t.key === active;
|
|
381
|
+
const hot = t.enabled > 0;
|
|
382
|
+
return (
|
|
383
|
+
<button
|
|
384
|
+
key={t.key}
|
|
385
|
+
type="button"
|
|
386
|
+
onClick={() => setActive(t.key)}
|
|
387
|
+
className={`flex items-center gap-2 border-l-2 px-3 py-1.5 text-sm transition-colors ${
|
|
388
|
+
isActive
|
|
389
|
+
? "border-foreground bg-foreground/5 font-medium text-foreground"
|
|
390
|
+
: "border-transparent text-muted-foreground hover:border-border hover:text-foreground"
|
|
391
|
+
}`}
|
|
392
|
+
>
|
|
393
|
+
<Icon
|
|
394
|
+
className={`h-3.5 w-3.5 shrink-0 ${isActive || hot ? "" : "text-muted-foreground/50"}`}
|
|
395
|
+
strokeWidth={1.5}
|
|
396
|
+
/>
|
|
397
|
+
<span className="flex-1 text-left">{t.label}</span>
|
|
398
|
+
<span
|
|
399
|
+
className={`font-mono text-[10px] font-bold tabular-nums ${
|
|
400
|
+
hot ? "text-emerald-500" : "text-muted-foreground/40"
|
|
401
|
+
}`}
|
|
402
|
+
>
|
|
403
|
+
{t.core ? t.total : `${t.enabled}/${t.total}`}
|
|
404
|
+
</span>
|
|
405
|
+
</button>
|
|
406
|
+
);
|
|
407
|
+
})}
|
|
408
|
+
</nav>
|
|
409
|
+
|
|
410
|
+
{/* Active category — flat (no card background) */}
|
|
411
|
+
<div className="min-w-0 flex-1">
|
|
412
|
+
{activeTab && (
|
|
413
|
+
<div>
|
|
414
|
+
{/* Category status + master toggle */}
|
|
415
|
+
<div className="flex flex-wrap items-center gap-2 border-b border-border/60 pb-2.5">
|
|
416
|
+
{activeTab.core ? (
|
|
417
|
+
<span className="font-mono text-[9px] font-bold uppercase tracking-[0.16em] text-muted-foreground">
|
|
418
|
+
always on
|
|
419
|
+
</span>
|
|
420
|
+
) : (
|
|
421
|
+
<>
|
|
422
|
+
<Switch
|
|
423
|
+
checked={activeTab.enabled > 0}
|
|
424
|
+
disabled={activeTab.key === "__custom"}
|
|
425
|
+
onChange={(v) =>
|
|
426
|
+
applyEdit((base) =>
|
|
427
|
+
activeTab.baseline
|
|
428
|
+
? toggleCategoryExplicit(base, activeTab.groupTools, v)
|
|
429
|
+
: toggleCategory(base, activeTab.key, activeTab.groupTools, v),
|
|
430
|
+
)
|
|
431
|
+
}
|
|
432
|
+
/>
|
|
433
|
+
<span
|
|
434
|
+
className={`font-mono text-[10px] font-bold uppercase tracking-[0.12em] ${
|
|
435
|
+
activeTab.enabled > 0 ? "text-emerald-500" : "text-muted-foreground/40"
|
|
436
|
+
}`}
|
|
437
|
+
>
|
|
438
|
+
{activeTab.enabled === 0
|
|
439
|
+
? "All off"
|
|
440
|
+
: activeTab.enabled === activeTab.total
|
|
441
|
+
? "All enabled"
|
|
442
|
+
: `${activeTab.enabled} of ${activeTab.total} on`}
|
|
443
|
+
</span>
|
|
444
|
+
</>
|
|
445
|
+
)}
|
|
446
|
+
{activeTab.requiresKey && (
|
|
447
|
+
<span
|
|
448
|
+
className="ml-auto inline-flex items-center gap-1 font-mono text-[9px] font-medium uppercase tracking-[0.12em] text-muted-foreground/60"
|
|
449
|
+
title="Needs an external API key / credential"
|
|
450
|
+
>
|
|
451
|
+
<Lock className="h-3 w-3" strokeWidth={2} />
|
|
452
|
+
key
|
|
453
|
+
</span>
|
|
454
|
+
)}
|
|
455
|
+
</div>
|
|
456
|
+
|
|
457
|
+
{activeTab.note && (
|
|
458
|
+
<p className="border-b border-border/60 py-2 text-[11px] leading-relaxed text-muted-foreground/70">
|
|
459
|
+
{activeTab.note}
|
|
460
|
+
</p>
|
|
461
|
+
)}
|
|
462
|
+
|
|
463
|
+
<div>
|
|
464
|
+
{activeTab.tools.map((t) => {
|
|
465
|
+
const ToolIcon = iconForTool(t.name, activeTab.icon);
|
|
466
|
+
const isCore = !!activeTab.core;
|
|
467
|
+
const on = isCore || t.enabled;
|
|
468
|
+
return (
|
|
469
|
+
<div
|
|
470
|
+
key={t.name}
|
|
471
|
+
className="flex items-center gap-3 border-b border-border/60 py-2.5 last:border-0"
|
|
472
|
+
>
|
|
473
|
+
<ToolIcon
|
|
474
|
+
className={`h-4 w-4 shrink-0 ${on ? "text-foreground" : "text-muted-foreground/40"}`}
|
|
475
|
+
strokeWidth={1.5}
|
|
476
|
+
/>
|
|
477
|
+
<div className="min-w-0 flex-1">
|
|
478
|
+
<span
|
|
479
|
+
className={`font-mono text-xs ${on ? "text-foreground" : "text-muted-foreground/60"}`}
|
|
480
|
+
>
|
|
481
|
+
{t.name}
|
|
482
|
+
</span>
|
|
483
|
+
<p
|
|
484
|
+
className="truncate text-[11px] text-muted-foreground/70"
|
|
485
|
+
title={t.description}
|
|
486
|
+
>
|
|
487
|
+
{t.description}
|
|
488
|
+
</p>
|
|
489
|
+
</div>
|
|
490
|
+
{/* Switch always on the right. Core tools show it
|
|
491
|
+
checked + disabled (can't be turned off). */}
|
|
492
|
+
<Switch
|
|
493
|
+
checked={on}
|
|
494
|
+
disabled={isCore || activeTab.key === "__custom"}
|
|
495
|
+
onChange={(v) =>
|
|
496
|
+
applyEdit((base) =>
|
|
497
|
+
activeTab.baseline
|
|
498
|
+
? toggleToolExplicit(base, t.name, v)
|
|
499
|
+
: toggleTool(base, activeTab.key, activeTab.groupTools, t.name, v),
|
|
500
|
+
)
|
|
501
|
+
}
|
|
502
|
+
/>
|
|
503
|
+
</div>
|
|
504
|
+
);
|
|
505
|
+
})}
|
|
506
|
+
</div>
|
|
507
|
+
</div>
|
|
508
|
+
)}
|
|
509
|
+
</div>
|
|
510
|
+
</div>
|
|
511
|
+
|
|
512
|
+
<p className="mt-4 text-[11px] leading-relaxed text-muted-foreground/60">
|
|
513
|
+
Some categories need an external key (vault).{" "}
|
|
514
|
+
<a
|
|
515
|
+
href="https://docs.polpo.sh/docs/agents/tools"
|
|
516
|
+
target="_blank"
|
|
517
|
+
rel="noopener noreferrer"
|
|
518
|
+
className="underline decoration-dotted underline-offset-2 transition-colors hover:text-foreground"
|
|
519
|
+
>
|
|
520
|
+
Tool docs →
|
|
521
|
+
</a>
|
|
522
|
+
</p>
|
|
523
|
+
</div>
|
|
524
|
+
);
|
|
525
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Hint } from "#/components/dashboard/hint";
|
|
2
|
+
|
|
3
|
+
interface VaultEntry {
|
|
4
|
+
service: string;
|
|
5
|
+
type: string;
|
|
6
|
+
label: string | null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const typeLabel: Record<string, string> = {
|
|
10
|
+
smtp: "SMTP",
|
|
11
|
+
imap: "IMAP",
|
|
12
|
+
oauth: "OAuth",
|
|
13
|
+
api_key: "API Key",
|
|
14
|
+
login: "Login",
|
|
15
|
+
custom: "Custom",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export default function AgentVaultView({ entries }: { entries: VaultEntry[] }) {
|
|
19
|
+
return (
|
|
20
|
+
<div>
|
|
21
|
+
<div className="mb-4">
|
|
22
|
+
<Hint>
|
|
23
|
+
Credentials this agent can access at runtime via{" "}
|
|
24
|
+
<span className="font-mono">vault_get</span>. AES-256-GCM encrypted at rest —
|
|
25
|
+
never exposed in the dashboard.
|
|
26
|
+
</Hint>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
{entries.length > 0 ? (
|
|
30
|
+
<div data-testid="vault-table" className="border border-border overflow-hidden overflow-x-auto">
|
|
31
|
+
<table className="w-full text-sm min-w-[400px]">
|
|
32
|
+
<thead>
|
|
33
|
+
<tr className="border-b border-border bg-secondary/50">
|
|
34
|
+
<th className="px-4 py-2.5 text-left text-xs font-medium text-muted-foreground">Service</th>
|
|
35
|
+
<th className="px-4 py-2.5 text-left text-xs font-medium text-muted-foreground">Type</th>
|
|
36
|
+
<th className="px-4 py-2.5 text-left text-xs font-medium text-muted-foreground">Label</th>
|
|
37
|
+
</tr>
|
|
38
|
+
</thead>
|
|
39
|
+
<tbody>
|
|
40
|
+
{entries.map((entry) => (
|
|
41
|
+
<tr key={entry.service} className="border-b border-border last:border-0">
|
|
42
|
+
<td className="px-4 py-3 font-mono text-xs font-medium">{entry.service}</td>
|
|
43
|
+
<td className="px-4 py-3">
|
|
44
|
+
<span className="rounded bg-secondary px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wider text-muted-foreground">
|
|
45
|
+
{typeLabel[entry.type] ?? entry.type}
|
|
46
|
+
</span>
|
|
47
|
+
</td>
|
|
48
|
+
<td className="px-4 py-3 text-muted-foreground">{entry.label}</td>
|
|
49
|
+
</tr>
|
|
50
|
+
))}
|
|
51
|
+
</tbody>
|
|
52
|
+
</table>
|
|
53
|
+
</div>
|
|
54
|
+
) : (
|
|
55
|
+
<div data-testid="vault-empty" className="border border-border p-8 text-center text-sm text-muted-foreground">
|
|
56
|
+
No credentials stored for this agent.
|
|
57
|
+
</div>
|
|
58
|
+
)}
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Suspense } from "react";
|
|
2
|
+
import { dataApi } from "#/lib/api";
|
|
3
|
+
import type { AgentConfig } from "@polpo-ai/core";
|
|
4
|
+
import { AgentsListSkeleton } from "#/components/dashboard/skeletons";
|
|
5
|
+
import AgentsView, { type Team } from "./view";
|
|
6
|
+
|
|
7
|
+
async function AgentsData({ id }: { id: string }) {
|
|
8
|
+
let agents: AgentConfig[] = [];
|
|
9
|
+
let teams: Team[] = [];
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
const [agentsRes, teamsRes] = await Promise.all([
|
|
13
|
+
dataApi<{ ok: boolean; data: AgentConfig[] }>(id, "/v1/agents").catch(
|
|
14
|
+
() => ({ ok: false, data: [] as AgentConfig[] }),
|
|
15
|
+
),
|
|
16
|
+
dataApi<{ ok: boolean; data: Team[] }>(id, "/v1/agents/teams").catch(
|
|
17
|
+
() => ({ ok: false, data: [] as Team[] }),
|
|
18
|
+
),
|
|
19
|
+
]);
|
|
20
|
+
agents = agentsRes.data ?? [];
|
|
21
|
+
teams = teamsRes.data ?? [];
|
|
22
|
+
} catch {}
|
|
23
|
+
|
|
24
|
+
return <AgentsView initialAgents={agents} initialTeams={teams} />;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export default async function AgentsPage({
|
|
28
|
+
params,
|
|
29
|
+
}: {
|
|
30
|
+
params: Promise<{ id: string }>;
|
|
31
|
+
}) {
|
|
32
|
+
const { id } = await params;
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<Suspense fallback={<AgentsListSkeleton />}>
|
|
36
|
+
<AgentsData id={id} />
|
|
37
|
+
</Suspense>
|
|
38
|
+
);
|
|
39
|
+
}
|