@agentprojectcontext/apx 1.68.0 → 1.69.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/package.json +1 -1
- package/src/host/daemon/api/agents.js +37 -1
- package/src/interfaces/web/dist/assets/index-_2zKBH4O.js +803 -0
- package/src/interfaces/web/dist/assets/index-_2zKBH4O.js.map +1 -0
- package/src/interfaces/web/dist/assets/index-xQYf6_ab.css +1 -0
- package/src/interfaces/web/dist/index.html +2 -2
- package/src/interfaces/web/src/i18n/en.ts +4 -0
- package/src/interfaces/web/src/i18n/es.ts +4 -0
- package/src/interfaces/web/src/lib/api/agents.ts +2 -1
- package/src/interfaces/web/src/screens/project/AgentBrainGraph.tsx +169 -47
- package/src/interfaces/web/src/screens/project/AgentDetailScreen.tsx +108 -34
- package/src/interfaces/web/src/screens/project/AgentsTab.tsx +72 -16
- package/src/interfaces/web/src/screens/project/Overview.tsx +90 -1
- package/src/interfaces/web/src/types/daemon.ts +10 -0
- package/src/interfaces/web/dist/assets/index-D4BmWoDM.css +0 -1
- package/src/interfaces/web/dist/assets/index-vwd6yQVw.js +0 -803
- package/src/interfaces/web/dist/assets/index-vwd6yQVw.js.map +0 -1
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { useMemo, useState } from "react";
|
|
2
2
|
import { useNavigate } from "react-router-dom";
|
|
3
3
|
import useSWR from "swr";
|
|
4
|
-
import { Bot, Crown, Eye, GitBranch, List, Plus, Send, Sparkles, Upload, Wrench } from "lucide-react";
|
|
4
|
+
import { Activity, Bot, Crown, Eye, GitBranch, Heart, List, MessagesSquare, Plus, Send, Sparkles, Upload, Wrench, Zap } from "lucide-react";
|
|
5
5
|
import { Agents } from "../../lib/api";
|
|
6
|
-
import type { AgentEntry } from "../../types/daemon";
|
|
6
|
+
import type { AgentEntry, AgentStats } from "../../types/daemon";
|
|
7
7
|
import { Section } from "../../components/Section";
|
|
8
8
|
import { Badge, Button, Dialog, Empty, Field, Input, Loading, Switch, Textarea } from "../../components/ui";
|
|
9
|
+
import { Tip } from "../../components/ui/tip";
|
|
9
10
|
import { UiSelect } from "../../components/UiSelect";
|
|
10
11
|
import { useToast } from "../../components/Toast";
|
|
11
12
|
import { EmojiInput, AutonomyPicker, AreaRoleFields } from "../../components/agents/AgentFormFields";
|
|
@@ -23,6 +24,44 @@ function agentVisual(a: AgentEntry) {
|
|
|
23
24
|
: { gradient: "from-slate-600 to-gray-600", Icon: Bot };
|
|
24
25
|
}
|
|
25
26
|
|
|
27
|
+
// Icon + count summary (threads / records / tasks / heartbeats) mirroring the
|
|
28
|
+
// agent Explorer, with an i18n tooltip on each so the icons are self-describing.
|
|
29
|
+
const STAT_META: { key: keyof AgentStats; icon: typeof Bot; i18n: Parameters<typeof t>[0] }[] = [
|
|
30
|
+
{ key: "threads", icon: MessagesSquare, i18n: "agents_ui.stat_threads" },
|
|
31
|
+
{ key: "records", icon: Activity, i18n: "agents_ui.stat_records" },
|
|
32
|
+
{ key: "tasks", icon: Zap, i18n: "agents_ui.stat_tasks" },
|
|
33
|
+
{ key: "heartbeats", icon: Heart, i18n: "agents_ui.stat_heartbeats" },
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
function AgentStatRow({ stats, className }: { stats?: AgentStats; className?: string }) {
|
|
37
|
+
if (!stats) return null;
|
|
38
|
+
return (
|
|
39
|
+
<div className={cn("flex items-center gap-3 text-[11px] text-muted-fg", className)}>
|
|
40
|
+
{STAT_META.map(({ key, icon: I, i18n }) => (
|
|
41
|
+
<Tip key={key} content={t(i18n)}>
|
|
42
|
+
<span className="inline-flex items-center gap-1 tabular-nums">
|
|
43
|
+
<I size={12} /> {stats[key]}
|
|
44
|
+
</span>
|
|
45
|
+
</Tip>
|
|
46
|
+
))}
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Group agents by their area (category). Named areas sort alphabetically;
|
|
52
|
+
// uncategorized agents fall to the end.
|
|
53
|
+
function groupByArea(agents: AgentEntry[]): { area: string | null; agents: AgentEntry[] }[] {
|
|
54
|
+
const map = new Map<string | null, AgentEntry[]>();
|
|
55
|
+
for (const a of agents) {
|
|
56
|
+
const k = a.area || null;
|
|
57
|
+
if (!map.has(k)) map.set(k, []);
|
|
58
|
+
map.get(k)!.push(a);
|
|
59
|
+
}
|
|
60
|
+
return [...map.entries()]
|
|
61
|
+
.sort(([a], [b]) => (a === null ? 1 : b === null ? -1 : a.localeCompare(b)))
|
|
62
|
+
.map(([area, agents]) => ({ area, agents }));
|
|
63
|
+
}
|
|
64
|
+
|
|
26
65
|
// Build parent→children map with panda's single-orchestrator fallback: if there
|
|
27
66
|
// is exactly one master and an agent has no explicit parent, treat it as a child.
|
|
28
67
|
function buildTree(agents: AgentEntry[]) {
|
|
@@ -50,7 +89,7 @@ function buildTree(agents: AgentEntry[]) {
|
|
|
50
89
|
export function AgentsTab({ pid }: { pid: string }) {
|
|
51
90
|
const navigate = useNavigate();
|
|
52
91
|
const toast = useToast();
|
|
53
|
-
const list = useSWR(`/projects/${pid}/agents`, () => Agents.list(pid));
|
|
92
|
+
const list = useSWR(`/projects/${pid}/agents?stats=1`, () => Agents.list(pid, { stats: true }));
|
|
54
93
|
const [view, setView] = useState<"hierarchy" | "list">("hierarchy");
|
|
55
94
|
const [creating, setCreating] = useState(false);
|
|
56
95
|
|
|
@@ -174,26 +213,41 @@ function HierarchyView({
|
|
|
174
213
|
<div className="space-y-8">
|
|
175
214
|
{roots.map((root) => {
|
|
176
215
|
const kids = childrenByParent.get(root.slug) || [];
|
|
216
|
+
const groups = groupByArea(kids);
|
|
217
|
+
const categorized = groups.some((g) => g.area);
|
|
177
218
|
return (
|
|
178
219
|
<div key={root.slug} className="flex flex-col items-center">
|
|
179
220
|
<AgentCard agent={root} onOpen={onOpen} onChat={onChat} wide />
|
|
180
221
|
{kids.length > 0 && (
|
|
181
222
|
<>
|
|
182
223
|
<div className="h-5 w-px bg-border" />
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
<AgentCard key={g.slug} agent={g} onOpen={onOpen} onChat={onChat} compact />
|
|
193
|
-
))}
|
|
194
|
-
</div>
|
|
195
|
-
</>
|
|
224
|
+
{/* When children carry a category (area), lay them out grouped
|
|
225
|
+
under a heading per category; otherwise keep the flat row. */}
|
|
226
|
+
<div className="flex flex-col gap-6 border-t border-border pt-5">
|
|
227
|
+
{groups.map((g) => (
|
|
228
|
+
<div key={g.area ?? "__none"} className="flex flex-col items-center gap-3">
|
|
229
|
+
{categorized && (
|
|
230
|
+
<span className="rounded-full border border-border bg-muted/40 px-2.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-muted-fg">
|
|
231
|
+
{g.area || t("agents_ui.uncategorized")} · {g.agents.length}
|
|
232
|
+
</span>
|
|
196
233
|
)}
|
|
234
|
+
<div className="flex flex-wrap items-start justify-center gap-4">
|
|
235
|
+
{g.agents.map((k) => (
|
|
236
|
+
<div key={k.slug} className="flex flex-col items-center">
|
|
237
|
+
<AgentCard agent={k} onOpen={onOpen} onChat={onChat} />
|
|
238
|
+
{(childrenByParent.get(k.slug) || []).length > 0 && (
|
|
239
|
+
<>
|
|
240
|
+
<div className="h-4 w-px bg-border" />
|
|
241
|
+
<div className="flex flex-wrap justify-center gap-3 border-t border-border pt-4">
|
|
242
|
+
{(childrenByParent.get(k.slug) || []).map((gc) => (
|
|
243
|
+
<AgentCard key={gc.slug} agent={gc} onOpen={onOpen} onChat={onChat} compact />
|
|
244
|
+
))}
|
|
245
|
+
</div>
|
|
246
|
+
</>
|
|
247
|
+
)}
|
|
248
|
+
</div>
|
|
249
|
+
))}
|
|
250
|
+
</div>
|
|
197
251
|
</div>
|
|
198
252
|
))}
|
|
199
253
|
</div>
|
|
@@ -236,6 +290,7 @@ function AgentCard({
|
|
|
236
290
|
{agent.role && <Badge>{agent.role}</Badge>}
|
|
237
291
|
{agent.model && !compact && <Badge tone="info">{agent.model}</Badge>}
|
|
238
292
|
</div>
|
|
293
|
+
<AgentStatRow stats={agent.stats} className="mt-2" />
|
|
239
294
|
<div className="mt-2 flex items-center gap-3 border-t border-border pt-2 text-xs text-muted-fg" onClick={(e) => e.stopPropagation()}>
|
|
240
295
|
<button onClick={() => onOpen(agent.slug)} className="flex items-center gap-1 hover:text-foreground"><Eye size={12} /> {t("project.agents.view")}</button>
|
|
241
296
|
<button onClick={() => onChat(agent.slug)} className="flex items-center gap-1 text-emerald-500 hover:text-emerald-400"><Send size={12} /> {t("project.agents.chat")}</button>
|
|
@@ -269,6 +324,7 @@ function ListView({ agents, onOpen, onChat }: { agents: AgentEntry[]; onOpen: (s
|
|
|
269
324
|
{a.tools?.map((tl) => <span key={tl} className="inline-flex items-center gap-0.5 rounded bg-muted px-1 py-0.5 text-[9px] text-muted-fg"><Wrench size={9} /> {tl}</span>)}
|
|
270
325
|
</div>
|
|
271
326
|
</div>
|
|
327
|
+
<AgentStatRow stats={a.stats} className="hidden shrink-0 sm:flex" />
|
|
272
328
|
<div className="flex shrink-0 items-center gap-3 text-xs text-muted-fg" onClick={(e) => e.stopPropagation()}>
|
|
273
329
|
<button onClick={() => onOpen(a.slug)} className="flex items-center gap-1 hover:text-foreground"><Eye size={12} /> {t("project.agents.view")}</button>
|
|
274
330
|
<button onClick={() => onChat(a.slug)} className="flex items-center gap-1 text-emerald-500 hover:text-emerald-400"><Send size={12} /> {t("project.agents.chat")}</button>
|
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
1
2
|
import useSWR from "swr";
|
|
2
3
|
import { NavLink, useNavigate } from "react-router-dom";
|
|
3
|
-
import { Bot, FileCode2, Heart, MessagesSquare, Puzzle, Zap, Crown, Activity } from "lucide-react";
|
|
4
|
+
import { Bot, Briefcase, FileCode2, Heart, MessagesSquare, Puzzle, Zap, Crown, Activity } from "lucide-react";
|
|
4
5
|
import { Agents, Artifacts, Mcps, Routines, Tasks } from "../../lib/api";
|
|
5
6
|
import { Section } from "../../components/Section";
|
|
6
7
|
import { StatusIcon, StatusBadge, effectiveStatus, statusLabel, TASK_STATUS_ORDER } from "../../components/tasks/taskStatus";
|
|
8
|
+
import { BrainGraph, type BrainNode, type BrainEdge } from "./AgentBrainGraph";
|
|
7
9
|
import { cn } from "../../lib/cn";
|
|
8
10
|
import { t } from "../../i18n";
|
|
9
11
|
import type { AgentEntry } from "../../types/daemon";
|
|
@@ -22,6 +24,10 @@ export function Overview({ pid }: { pid: string }) {
|
|
|
22
24
|
const agentList = agents.data ?? [];
|
|
23
25
|
const orchestrators = agentList.filter((a) => a.is_master || a.type === "orchestrator");
|
|
24
26
|
const specialists = agentList.filter((a) => !(a.is_master || a.type === "orchestrator"));
|
|
27
|
+
// When agents carry an area, the project reads as a company: group the roster
|
|
28
|
+
// by area instead of the flat orchestrator/specialist split.
|
|
29
|
+
const areaGroups = groupByArea(agentList);
|
|
30
|
+
const hasAreas = areaGroups.some((g) => g.area);
|
|
25
31
|
const activeRoutines = (routines.data ?? []).filter((r) => r.enabled).length;
|
|
26
32
|
const openTasks = [...(tasks.data ?? [])].sort((a, b) => (b.created_at || "").localeCompare(a.created_at || "")).slice(0, 6);
|
|
27
33
|
|
|
@@ -53,11 +59,32 @@ export function Overview({ pid }: { pid: string }) {
|
|
|
53
59
|
</div>
|
|
54
60
|
)}
|
|
55
61
|
|
|
62
|
+
{/* Team brain — the whole agent map: orchestrators at the core, their
|
|
63
|
+
specialists clustered around them as satellites, all connected. */}
|
|
64
|
+
{agentList.length > 0 && (
|
|
65
|
+
<Section title={t("project.overview.brain_title")} description={t("project.overview.brain_desc")} className="!p-4">
|
|
66
|
+
<TeamBrain pid={pid} agents={agentList} navigate={navigate} />
|
|
67
|
+
</Section>
|
|
68
|
+
)}
|
|
69
|
+
|
|
56
70
|
<div className="grid gap-4 lg:grid-cols-2">
|
|
57
71
|
{/* Agent roster */}
|
|
58
72
|
<Section title={t("project.overview.roster")} className="!p-4">
|
|
59
73
|
{agentList.length === 0 ? (
|
|
60
74
|
<p className="text-sm text-muted-fg">{t("project.overview.no_agents")}</p>
|
|
75
|
+
) : hasAreas ? (
|
|
76
|
+
<div className="space-y-3">
|
|
77
|
+
{areaGroups.map((g) => (
|
|
78
|
+
<RosterRow
|
|
79
|
+
key={g.area ?? "__none"}
|
|
80
|
+
label={g.area || t("agents_ui.uncategorized")}
|
|
81
|
+
icon={Briefcase}
|
|
82
|
+
agents={g.agents}
|
|
83
|
+
pid={pid}
|
|
84
|
+
navigate={navigate}
|
|
85
|
+
/>
|
|
86
|
+
))}
|
|
87
|
+
</div>
|
|
61
88
|
) : (
|
|
62
89
|
<div className="space-y-3">
|
|
63
90
|
{orchestrators.length > 0 && (
|
|
@@ -106,6 +133,67 @@ export function Overview({ pid }: { pid: string }) {
|
|
|
106
133
|
);
|
|
107
134
|
}
|
|
108
135
|
|
|
136
|
+
// Group agents by area (category); named areas first (alphabetical), the
|
|
137
|
+
// uncategorized bucket last.
|
|
138
|
+
function groupByArea(agents: AgentEntry[]): { area: string | null; agents: AgentEntry[] }[] {
|
|
139
|
+
const map = new Map<string | null, AgentEntry[]>();
|
|
140
|
+
for (const a of agents) {
|
|
141
|
+
const k = a.area || null;
|
|
142
|
+
if (!map.has(k)) map.set(k, []);
|
|
143
|
+
map.get(k)!.push(a);
|
|
144
|
+
}
|
|
145
|
+
return [...map.entries()]
|
|
146
|
+
.sort(([a], [b]) => (a === null ? 1 : b === null ? -1 : a.localeCompare(b)))
|
|
147
|
+
.map(([area, agents]) => ({ area, agents }));
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Whole-project agent map. The core is the team; orchestrators hang off it as
|
|
151
|
+
// hubs, and each specialist connects to its parent orchestrator (or the core if
|
|
152
|
+
// unparented) — so teams read as satellite clusters. Click a node to open it.
|
|
153
|
+
function TeamBrain({
|
|
154
|
+
pid, agents, navigate,
|
|
155
|
+
}: {
|
|
156
|
+
pid: string; agents: AgentEntry[]; navigate: (to: string) => void;
|
|
157
|
+
}) {
|
|
158
|
+
const { nodes, edges } = useMemo(() => {
|
|
159
|
+
const nodes: BrainNode[] = [];
|
|
160
|
+
const edges: BrainEdge[] = [];
|
|
161
|
+
const ROOT = "__root";
|
|
162
|
+
nodes.push({ id: ROOT, label: t("project.overview.brain_core"), kind: "agent", role: "core", emoji: "🧠" });
|
|
163
|
+
|
|
164
|
+
const slugs = new Set(agents.map((a) => a.slug));
|
|
165
|
+
const hasKids = (slug: string) => agents.some((x) => x.parent === slug);
|
|
166
|
+
|
|
167
|
+
for (const a of agents) {
|
|
168
|
+
const isOrch = !!a.is_master || a.type === "orchestrator";
|
|
169
|
+
nodes.push({
|
|
170
|
+
id: a.slug,
|
|
171
|
+
label: a.slug,
|
|
172
|
+
slug: a.slug,
|
|
173
|
+
kind: isOrch ? "agent" : "agentlink",
|
|
174
|
+
role: isOrch || hasKids(a.slug) ? "hub" : "leaf",
|
|
175
|
+
emoji: a.emoji || undefined,
|
|
176
|
+
relation: a.role || (isOrch ? t("project.agents.orchestrator") : t("project.overview.specialists")),
|
|
177
|
+
detail: a.description || undefined,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
for (const a of agents) {
|
|
181
|
+
const parent = a.parent && slugs.has(a.parent) ? a.parent : ROOT;
|
|
182
|
+
edges.push({ source: parent, target: a.slug });
|
|
183
|
+
}
|
|
184
|
+
return { nodes, edges };
|
|
185
|
+
}, [agents]);
|
|
186
|
+
|
|
187
|
+
return (
|
|
188
|
+
<BrainGraph
|
|
189
|
+
nodes={nodes}
|
|
190
|
+
edges={edges}
|
|
191
|
+
height={520}
|
|
192
|
+
onNodeClick={(n) => { if (n.slug) navigate(`/p/${pid}/agents/${n.slug}`); }}
|
|
193
|
+
/>
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
109
197
|
function RosterRow({
|
|
110
198
|
label, icon: Icon, agents, pid, navigate,
|
|
111
199
|
}: {
|
|
@@ -128,6 +216,7 @@ function RosterRow({
|
|
|
128
216
|
>
|
|
129
217
|
<span className="text-sm leading-none">{a.emoji || "🤖"}</span>
|
|
130
218
|
<span className="truncate">{a.slug}</span>
|
|
219
|
+
{a.role && <span className="truncate text-[10px] text-muted-fg">· {a.role}</span>}
|
|
131
220
|
</button>
|
|
132
221
|
))}
|
|
133
222
|
</div>
|
|
@@ -35,6 +35,16 @@ export interface AgentEntry {
|
|
|
35
35
|
autonomy?: AgentAutonomy | null;
|
|
36
36
|
skills: string[];
|
|
37
37
|
tools: string[];
|
|
38
|
+
// Optional per-agent activity summary; only present when the list is
|
|
39
|
+
// requested with `?stats=1` (see AgentsTab).
|
|
40
|
+
stats?: AgentStats;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface AgentStats {
|
|
44
|
+
threads: number;
|
|
45
|
+
records: number;
|
|
46
|
+
tasks: number;
|
|
47
|
+
heartbeats: number;
|
|
38
48
|
}
|
|
39
49
|
|
|
40
50
|
export interface AgentDetail extends AgentEntry {
|