clawport-ui 0.4.7 → 0.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components/AgentNode.tsx +133 -56
- package/components/GridView.tsx +17 -55
- package/components/OrgMap.tsx +291 -68
- package/lib/agents-registry.ts +101 -3
- package/lib/teams.ts +44 -0
- package/package.json +2 -1
package/components/AgentNode.tsx
CHANGED
|
@@ -10,6 +10,8 @@ export function AgentNode({ data, selected }: NodeProps) {
|
|
|
10
10
|
const hasCrons = agent.crons && agent.crons.length > 0
|
|
11
11
|
const hasErrors = hasCrons && agent.crons.some((c: CronJob) => c.status === "error")
|
|
12
12
|
const cronCount = hasCrons ? agent.crons.length : 0
|
|
13
|
+
const toolCount = agent.tools?.length ?? 0
|
|
14
|
+
const reportCount = agent.directReports?.length ?? 0
|
|
13
15
|
|
|
14
16
|
return (
|
|
15
17
|
<div
|
|
@@ -25,14 +27,13 @@ export function AgentNode({ data, selected }: NodeProps) {
|
|
|
25
27
|
borderBottom: `1px solid ${selected ? "var(--accent)" : "var(--separator)"}`,
|
|
26
28
|
borderLeft: `1px solid ${selected ? "var(--accent)" : "var(--separator)"}`,
|
|
27
29
|
padding: "var(--space-3) var(--space-4)",
|
|
28
|
-
|
|
29
|
-
maxWidth: 220,
|
|
30
|
+
width: 260,
|
|
30
31
|
cursor: "pointer",
|
|
31
32
|
position: "relative",
|
|
32
33
|
boxShadow: selected ? "0 0 0 1px var(--accent), var(--shadow-card)" : "var(--shadow-card)",
|
|
33
34
|
}}
|
|
34
35
|
>
|
|
35
|
-
{/* Emoji + Name row */}
|
|
36
|
+
{/* Emoji + Name + Title row */}
|
|
36
37
|
<div
|
|
37
38
|
style={{
|
|
38
39
|
display: "flex",
|
|
@@ -42,75 +43,119 @@ export function AgentNode({ data, selected }: NodeProps) {
|
|
|
42
43
|
}}
|
|
43
44
|
>
|
|
44
45
|
<AgentAvatar agent={agent} size={30} borderRadius={8} />
|
|
45
|
-
<div
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
46
|
+
<div style={{ flex: 1, minWidth: 0 }}>
|
|
47
|
+
<div
|
|
48
|
+
style={{
|
|
49
|
+
fontSize: "var(--text-body)",
|
|
50
|
+
fontWeight: "var(--weight-semibold)",
|
|
51
|
+
color: "var(--text-primary)",
|
|
52
|
+
whiteSpace: "nowrap",
|
|
53
|
+
overflow: "hidden",
|
|
54
|
+
textOverflow: "ellipsis",
|
|
55
|
+
lineHeight: "var(--leading-tight)",
|
|
56
|
+
}}
|
|
57
|
+
>
|
|
58
|
+
{agent.name}
|
|
59
|
+
</div>
|
|
60
|
+
<div
|
|
61
|
+
style={{
|
|
62
|
+
fontSize: "var(--text-caption2)",
|
|
63
|
+
color: agent.color,
|
|
64
|
+
opacity: 0.85,
|
|
65
|
+
whiteSpace: "nowrap",
|
|
66
|
+
overflow: "hidden",
|
|
67
|
+
textOverflow: "ellipsis",
|
|
68
|
+
marginTop: 1,
|
|
69
|
+
}}
|
|
70
|
+
>
|
|
71
|
+
{agent.title}
|
|
72
|
+
</div>
|
|
56
73
|
</div>
|
|
57
74
|
</div>
|
|
58
75
|
|
|
59
|
-
{/*
|
|
60
|
-
<div
|
|
61
|
-
style={{
|
|
62
|
-
fontSize: "var(--text-caption1)",
|
|
63
|
-
color: "var(--text-secondary)",
|
|
64
|
-
whiteSpace: "nowrap",
|
|
65
|
-
overflow: "hidden",
|
|
66
|
-
textOverflow: "ellipsis",
|
|
67
|
-
marginTop: 1,
|
|
68
|
-
}}
|
|
69
|
-
>
|
|
70
|
-
{agent.title}
|
|
71
|
-
</div>
|
|
72
|
-
|
|
73
|
-
{/* Description snippet */}
|
|
76
|
+
{/* Description — allow 2 lines */}
|
|
74
77
|
{agent.description && (
|
|
75
78
|
<div
|
|
76
79
|
style={{
|
|
77
80
|
fontSize: "var(--text-caption2)",
|
|
81
|
+
lineHeight: 1.4,
|
|
78
82
|
color: "var(--text-tertiary)",
|
|
79
|
-
|
|
83
|
+
marginTop: "var(--space-1)",
|
|
84
|
+
display: "-webkit-box",
|
|
85
|
+
WebkitLineClamp: 2,
|
|
86
|
+
WebkitBoxOrient: "vertical",
|
|
80
87
|
overflow: "hidden",
|
|
81
|
-
textOverflow: "ellipsis",
|
|
82
|
-
marginTop: 2,
|
|
83
88
|
}}
|
|
84
89
|
>
|
|
85
90
|
{agent.description}
|
|
86
91
|
</div>
|
|
87
92
|
)}
|
|
88
93
|
|
|
89
|
-
{/*
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
<div
|
|
102
|
-
className={hasErrors ? "animate-error-pulse" : ""}
|
|
94
|
+
{/* Stats row */}
|
|
95
|
+
<div
|
|
96
|
+
style={{
|
|
97
|
+
display: "flex",
|
|
98
|
+
alignItems: "center",
|
|
99
|
+
gap: "var(--space-2)",
|
|
100
|
+
marginTop: "var(--space-2)",
|
|
101
|
+
flexWrap: "wrap",
|
|
102
|
+
}}
|
|
103
|
+
>
|
|
104
|
+
{toolCount > 0 && (
|
|
105
|
+
<span
|
|
103
106
|
style={{
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
background:
|
|
108
|
-
|
|
107
|
+
fontSize: "var(--text-caption2)",
|
|
108
|
+
fontWeight: "var(--weight-medium)",
|
|
109
|
+
color: "var(--text-quaternary)",
|
|
110
|
+
background: "var(--fill-quaternary)",
|
|
111
|
+
padding: "1px 7px",
|
|
112
|
+
borderRadius: 10,
|
|
109
113
|
}}
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
+
>
|
|
115
|
+
{toolCount} tools
|
|
116
|
+
</span>
|
|
117
|
+
)}
|
|
118
|
+
{reportCount > 0 && (
|
|
119
|
+
<span
|
|
120
|
+
style={{
|
|
121
|
+
fontSize: "var(--text-caption2)",
|
|
122
|
+
fontWeight: "var(--weight-medium)",
|
|
123
|
+
color: "var(--text-quaternary)",
|
|
124
|
+
background: "var(--fill-quaternary)",
|
|
125
|
+
padding: "1px 7px",
|
|
126
|
+
borderRadius: 10,
|
|
127
|
+
}}
|
|
128
|
+
>
|
|
129
|
+
{reportCount} reports
|
|
130
|
+
</span>
|
|
131
|
+
)}
|
|
132
|
+
{hasCrons && (
|
|
133
|
+
<span
|
|
134
|
+
style={{
|
|
135
|
+
display: "inline-flex",
|
|
136
|
+
alignItems: "center",
|
|
137
|
+
gap: 4,
|
|
138
|
+
fontSize: "var(--text-caption2)",
|
|
139
|
+
fontWeight: "var(--weight-medium)",
|
|
140
|
+
color: hasErrors ? "var(--system-red)" : "var(--system-green)",
|
|
141
|
+
background: hasErrors ? "var(--system-red)10" : "var(--system-green)10",
|
|
142
|
+
padding: "1px 7px",
|
|
143
|
+
borderRadius: 10,
|
|
144
|
+
}}
|
|
145
|
+
>
|
|
146
|
+
<span
|
|
147
|
+
className={hasErrors ? "animate-error-pulse" : ""}
|
|
148
|
+
style={{
|
|
149
|
+
width: 5,
|
|
150
|
+
height: 5,
|
|
151
|
+
borderRadius: "50%",
|
|
152
|
+
background: hasErrors ? "var(--system-red)" : "var(--system-green)",
|
|
153
|
+
}}
|
|
154
|
+
/>
|
|
155
|
+
{cronCount} cron{cronCount !== 1 ? "s" : ""}
|
|
156
|
+
</span>
|
|
157
|
+
)}
|
|
158
|
+
</div>
|
|
114
159
|
|
|
115
160
|
{/* Handles - invisible */}
|
|
116
161
|
<Handle type="target" position={Position.Top} style={{ opacity: 0 }} />
|
|
@@ -119,4 +164,36 @@ export function AgentNode({ data, selected }: NodeProps) {
|
|
|
119
164
|
)
|
|
120
165
|
}
|
|
121
166
|
|
|
122
|
-
|
|
167
|
+
function TeamGroupNode({ data }: NodeProps) {
|
|
168
|
+
const { label, color } = data as { label: string; color?: string } & Record<string, unknown>
|
|
169
|
+
return (
|
|
170
|
+
<div
|
|
171
|
+
style={{
|
|
172
|
+
width: "100%",
|
|
173
|
+
height: "100%",
|
|
174
|
+
position: "relative",
|
|
175
|
+
}}
|
|
176
|
+
>
|
|
177
|
+
<div
|
|
178
|
+
style={{
|
|
179
|
+
position: "absolute",
|
|
180
|
+
top: 10,
|
|
181
|
+
left: 0,
|
|
182
|
+
right: 0,
|
|
183
|
+
textAlign: "center",
|
|
184
|
+
fontSize: "var(--text-caption2)",
|
|
185
|
+
fontWeight: "var(--weight-semibold)",
|
|
186
|
+
letterSpacing: "var(--tracking-wide)",
|
|
187
|
+
textTransform: "uppercase",
|
|
188
|
+
color: color ?? "var(--text-tertiary)",
|
|
189
|
+
userSelect: "none",
|
|
190
|
+
pointerEvents: "none",
|
|
191
|
+
}}
|
|
192
|
+
>
|
|
193
|
+
{label}
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export const nodeTypes = { agentNode: AgentNode, teamGroup: TeamGroupNode }
|
package/components/GridView.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client"
|
|
2
2
|
|
|
3
3
|
import type { Agent, CronJob } from "@/lib/types"
|
|
4
|
+
import { buildTeams } from "@/lib/teams"
|
|
4
5
|
import { AgentAvatar } from "@/components/AgentAvatar"
|
|
5
6
|
|
|
6
7
|
interface GridViewProps {
|
|
@@ -16,45 +17,6 @@ function worstStatus(statuses: CronJob["status"][]): CronJob["status"] {
|
|
|
16
17
|
return "idle"
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
interface Team {
|
|
20
|
-
manager: Agent
|
|
21
|
-
members: Agent[]
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function buildTeams(agents: Agent[]): { jarvis: Agent | null; teams: Team[]; soloOps: Agent[] } {
|
|
25
|
-
const jarvis = agents.find((a) => a.reportsTo === null) ?? null
|
|
26
|
-
if (!jarvis) return { jarvis: null, teams: [], soloOps: [] }
|
|
27
|
-
|
|
28
|
-
const byId = new Map(agents.map((a) => [a.id, a]))
|
|
29
|
-
const teamManagers: Agent[] = []
|
|
30
|
-
const soloOps: Agent[] = []
|
|
31
|
-
|
|
32
|
-
for (const rid of jarvis.directReports) {
|
|
33
|
-
const r = byId.get(rid)
|
|
34
|
-
if (!r) continue
|
|
35
|
-
if (r.directReports.length > 0) {
|
|
36
|
-
teamManagers.push(r)
|
|
37
|
-
} else {
|
|
38
|
-
soloOps.push(r)
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const teams: Team[] = teamManagers.map((mgr) => {
|
|
43
|
-
const members: Agent[] = []
|
|
44
|
-
const queue = [...mgr.directReports]
|
|
45
|
-
while (queue.length > 0) {
|
|
46
|
-
const id = queue.shift()!
|
|
47
|
-
const a = byId.get(id)
|
|
48
|
-
if (a) {
|
|
49
|
-
members.push(a)
|
|
50
|
-
queue.push(...a.directReports)
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
return { manager: mgr, members }
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
return { jarvis, teams, soloOps }
|
|
57
|
-
}
|
|
58
20
|
|
|
59
21
|
function AgentCard({
|
|
60
22
|
agent,
|
|
@@ -269,7 +231,7 @@ function TeamSection({
|
|
|
269
231
|
}
|
|
270
232
|
|
|
271
233
|
export function GridView({ agents, crons, selectedId, onSelect }: GridViewProps) {
|
|
272
|
-
const {
|
|
234
|
+
const { root, teams, soloOps } = buildTeams(agents)
|
|
273
235
|
|
|
274
236
|
const totalCrons = crons.length
|
|
275
237
|
const healthyCrons = crons.filter((c) => c.status === "ok").length
|
|
@@ -286,10 +248,10 @@ export function GridView({ agents, crons, selectedId, onSelect }: GridViewProps)
|
|
|
286
248
|
}}
|
|
287
249
|
>
|
|
288
250
|
{/* Jarvis hero banner */}
|
|
289
|
-
{
|
|
251
|
+
{root && (
|
|
290
252
|
<button
|
|
291
253
|
className="hover-lift focus-ring"
|
|
292
|
-
onClick={() => onSelect(
|
|
254
|
+
onClick={() => onSelect(root)}
|
|
293
255
|
style={{
|
|
294
256
|
display: "flex",
|
|
295
257
|
alignItems: "center",
|
|
@@ -297,28 +259,28 @@ export function GridView({ agents, crons, selectedId, onSelect }: GridViewProps)
|
|
|
297
259
|
width: "100%",
|
|
298
260
|
padding: "var(--space-5) var(--space-6)",
|
|
299
261
|
borderRadius: "var(--radius-xl)",
|
|
300
|
-
background: `linear-gradient(135deg, var(--material-regular) 0%, ${
|
|
301
|
-
border: selectedId ===
|
|
302
|
-
? `1.5px solid ${
|
|
262
|
+
background: `linear-gradient(135deg, var(--material-regular) 0%, ${root.color}08 100%)`,
|
|
263
|
+
border: selectedId === root.id
|
|
264
|
+
? `1.5px solid ${root.color}`
|
|
303
265
|
: "1px solid var(--separator)",
|
|
304
266
|
cursor: "pointer",
|
|
305
267
|
textAlign: "left",
|
|
306
268
|
marginBottom: "var(--space-6)",
|
|
307
269
|
transition: "all 150ms var(--ease-spring)",
|
|
308
|
-
boxShadow: selectedId ===
|
|
309
|
-
? `0 0 0 1px ${
|
|
270
|
+
boxShadow: selectedId === root.id
|
|
271
|
+
? `0 0 0 1px ${root.color}40, 0 8px 32px ${root.color}12`
|
|
310
272
|
: "var(--shadow-card)",
|
|
311
273
|
position: "relative",
|
|
312
274
|
overflow: "hidden",
|
|
313
275
|
}}
|
|
314
276
|
>
|
|
315
277
|
<AgentAvatar
|
|
316
|
-
agent={
|
|
278
|
+
agent={root}
|
|
317
279
|
size={64}
|
|
318
280
|
borderRadius={18}
|
|
319
281
|
style={{
|
|
320
|
-
border: `1.5px solid ${
|
|
321
|
-
boxShadow: `0 4px 20px ${
|
|
282
|
+
border: `1.5px solid ${root.color}50`,
|
|
283
|
+
boxShadow: `0 4px 20px ${root.color}20`,
|
|
322
284
|
}}
|
|
323
285
|
/>
|
|
324
286
|
<div style={{ flex: 1, minWidth: 0 }}>
|
|
@@ -331,19 +293,19 @@ export function GridView({ agents, crons, selectedId, onSelect }: GridViewProps)
|
|
|
331
293
|
lineHeight: "var(--leading-tight)",
|
|
332
294
|
}}
|
|
333
295
|
>
|
|
334
|
-
{
|
|
296
|
+
{root.name}
|
|
335
297
|
</div>
|
|
336
298
|
<div
|
|
337
299
|
style={{
|
|
338
300
|
fontSize: "var(--text-subheadline)",
|
|
339
|
-
color:
|
|
301
|
+
color: root.color,
|
|
340
302
|
opacity: 0.85,
|
|
341
303
|
marginTop: 2,
|
|
342
304
|
}}
|
|
343
305
|
>
|
|
344
|
-
{
|
|
306
|
+
{root.title}
|
|
345
307
|
</div>
|
|
346
|
-
{
|
|
308
|
+
{root.description && (
|
|
347
309
|
<div
|
|
348
310
|
style={{
|
|
349
311
|
fontSize: "var(--text-caption1)",
|
|
@@ -351,7 +313,7 @@ export function GridView({ agents, crons, selectedId, onSelect }: GridViewProps)
|
|
|
351
313
|
marginTop: "var(--space-1)",
|
|
352
314
|
}}
|
|
353
315
|
>
|
|
354
|
-
{
|
|
316
|
+
{root.description}
|
|
355
317
|
</div>
|
|
356
318
|
)}
|
|
357
319
|
</div>
|
package/components/OrgMap.tsx
CHANGED
|
@@ -2,14 +2,17 @@
|
|
|
2
2
|
import {
|
|
3
3
|
ReactFlow,
|
|
4
4
|
Controls,
|
|
5
|
+
Panel,
|
|
5
6
|
useNodesState,
|
|
6
7
|
useEdgesState,
|
|
7
8
|
type Node,
|
|
8
9
|
type Edge,
|
|
9
10
|
ConnectionLineType,
|
|
10
11
|
} from "@xyflow/react"
|
|
11
|
-
import { useCallback, useEffect } from "react"
|
|
12
|
+
import { useCallback, useEffect, useState } from "react"
|
|
13
|
+
import dagre from "@dagrejs/dagre"
|
|
12
14
|
import type { Agent, CronJob } from "@/lib/types"
|
|
15
|
+
import { buildTeams } from "@/lib/teams"
|
|
13
16
|
import { nodeTypes } from "@/components/AgentNode"
|
|
14
17
|
|
|
15
18
|
interface OrgMapProps {
|
|
@@ -19,79 +22,40 @@ interface OrgMapProps {
|
|
|
19
22
|
onNodeClick: (agent: Agent) => void
|
|
20
23
|
}
|
|
21
24
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
type MapLayout = "teams" | "hierarchy"
|
|
26
|
+
|
|
27
|
+
const NODE_W = 260
|
|
28
|
+
const NODE_H = 110
|
|
29
|
+
|
|
30
|
+
// ── Shared helpers ─────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
function mergeAgentsWithCrons(agents: Agent[], crons: CronJob[]) {
|
|
28
33
|
const withCrons = agents.map((a) => ({
|
|
29
34
|
...a,
|
|
30
35
|
crons: crons.filter((c) => c.agentId === a.id),
|
|
31
36
|
}))
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
// BFS to determine levels
|
|
35
|
-
const levels: string[][] = []
|
|
36
|
-
const visited = new Set<string>()
|
|
37
|
-
const root = agents.find((a) => a.reportsTo === null)
|
|
38
|
-
if (!root) return { nodes: [], edges: [] }
|
|
39
|
-
|
|
40
|
-
let queue = [root.id]
|
|
41
|
-
while (queue.length > 0) {
|
|
42
|
-
levels.push([...queue])
|
|
43
|
-
queue.forEach((id) => visited.add(id))
|
|
44
|
-
const nextQueue: string[] = []
|
|
45
|
-
for (const id of queue) {
|
|
46
|
-
const agent = agentMap.get(id)
|
|
47
|
-
if (!agent) continue
|
|
48
|
-
for (const childId of agent.directReports) {
|
|
49
|
-
if (!visited.has(childId)) nextQueue.push(childId)
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
queue = nextQueue
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Pick up disconnected agents
|
|
56
|
-
const disconnected = agents.filter((a) => !visited.has(a.id))
|
|
57
|
-
if (disconnected.length > 0) levels.push(disconnected.map((a) => a.id))
|
|
58
|
-
|
|
59
|
-
const LEVEL_HEIGHT = 200
|
|
60
|
-
const nodes: Node[] = []
|
|
61
|
-
|
|
62
|
-
for (let level = 0; level < levels.length; level++) {
|
|
63
|
-
const ids = levels[level]
|
|
64
|
-
const spacing = Math.max(200, Math.min(260, 1600 / Math.max(ids.length, 1)))
|
|
65
|
-
const totalWidth = ids.length * spacing
|
|
66
|
-
const startX = 600 - totalWidth / 2 + spacing / 2
|
|
67
|
-
|
|
68
|
-
ids.forEach((id, i) => {
|
|
69
|
-
const agent = agentMapWithCrons.get(id)
|
|
70
|
-
if (!agent) return
|
|
71
|
-
nodes.push({
|
|
72
|
-
id,
|
|
73
|
-
type: "agentNode",
|
|
74
|
-
data: agent as unknown as Record<string, unknown>,
|
|
75
|
-
position: { x: startX + i * spacing - spacing / 2, y: level * LEVEL_HEIGHT + 20 },
|
|
76
|
-
selected: id === selectedId,
|
|
77
|
-
})
|
|
78
|
-
})
|
|
79
|
-
}
|
|
37
|
+
return new Map(withCrons.map((a) => [a.id, a]))
|
|
38
|
+
}
|
|
80
39
|
|
|
81
|
-
|
|
40
|
+
function buildEdges(
|
|
41
|
+
agents: Agent[],
|
|
42
|
+
selectedId: string | null,
|
|
43
|
+
): Edge[] {
|
|
44
|
+
const agentMap = new Map(agents.map((a) => [a.id, a]))
|
|
82
45
|
const selectedAgentIds = new Set<string>()
|
|
83
46
|
if (selectedId) {
|
|
84
47
|
selectedAgentIds.add(selectedId)
|
|
85
|
-
const
|
|
86
|
-
if (
|
|
87
|
-
if (
|
|
88
|
-
|
|
48
|
+
const sel = agentMap.get(selectedId)
|
|
49
|
+
if (sel) {
|
|
50
|
+
if (sel.reportsTo) selectedAgentIds.add(sel.reportsTo)
|
|
51
|
+
sel.directReports.forEach((id) => selectedAgentIds.add(id))
|
|
89
52
|
}
|
|
90
53
|
}
|
|
91
54
|
|
|
92
55
|
const edges: Edge[] = []
|
|
93
56
|
for (const agent of agents) {
|
|
94
57
|
for (const childId of agent.directReports) {
|
|
58
|
+
if (!agentMap.has(childId)) continue
|
|
95
59
|
const isHighlighted =
|
|
96
60
|
selectedId && selectedAgentIds.has(agent.id) && selectedAgentIds.has(childId)
|
|
97
61
|
|
|
@@ -101,29 +65,240 @@ function buildLayout(
|
|
|
101
65
|
target: childId,
|
|
102
66
|
type: "smoothstep",
|
|
103
67
|
style: {
|
|
104
|
-
stroke: isHighlighted ? "var(--accent)" : "var(--
|
|
105
|
-
strokeWidth: isHighlighted ? 2 : 1.5,
|
|
106
|
-
opacity: isHighlighted ? 1 : 0.
|
|
107
|
-
strokeDasharray: isHighlighted ? undefined : "6 4",
|
|
68
|
+
stroke: isHighlighted ? "var(--accent)" : "var(--text-quaternary)",
|
|
69
|
+
strokeWidth: isHighlighted ? 2.5 : 1.5,
|
|
70
|
+
opacity: isHighlighted ? 1 : 0.7,
|
|
108
71
|
},
|
|
109
72
|
animated: !!isHighlighted,
|
|
110
73
|
})
|
|
111
74
|
}
|
|
112
75
|
}
|
|
76
|
+
return edges
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ── Dagre helper ───────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
function dagreLayout(
|
|
82
|
+
nodeIds: string[],
|
|
83
|
+
parentChildEdges: [string, string][],
|
|
84
|
+
opts: { rankdir?: string; nodesep?: number; ranksep?: number } = {},
|
|
85
|
+
): Map<string, { x: number; y: number }> {
|
|
86
|
+
const g = new dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}))
|
|
87
|
+
g.setGraph({
|
|
88
|
+
rankdir: opts.rankdir ?? "TB",
|
|
89
|
+
nodesep: opts.nodesep ?? 60,
|
|
90
|
+
ranksep: opts.ranksep ?? 120,
|
|
91
|
+
marginx: 20,
|
|
92
|
+
marginy: 20,
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
for (const id of nodeIds) {
|
|
96
|
+
g.setNode(id, { width: NODE_W, height: NODE_H })
|
|
97
|
+
}
|
|
98
|
+
for (const [src, tgt] of parentChildEdges) {
|
|
99
|
+
g.setEdge(src, tgt)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
dagre.layout(g)
|
|
103
|
+
|
|
104
|
+
const positions = new Map<string, { x: number; y: number }>()
|
|
105
|
+
for (const id of nodeIds) {
|
|
106
|
+
const n = g.node(id)
|
|
107
|
+
// dagre returns center coords — convert to top-left for React Flow
|
|
108
|
+
positions.set(id, { x: n.x - NODE_W / 2, y: n.y - NODE_H / 2 })
|
|
109
|
+
}
|
|
110
|
+
return positions
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ── Team-column layout (dagre per column) ──────────────────────
|
|
114
|
+
|
|
115
|
+
const COL_GAP = 80
|
|
116
|
+
const GROUP_PAD_X = 30
|
|
117
|
+
const GROUP_PAD_TOP = 36
|
|
118
|
+
const GROUP_PAD_BOTTOM = 24
|
|
119
|
+
|
|
120
|
+
function buildTeamLayout(
|
|
121
|
+
agents: Agent[],
|
|
122
|
+
crons: CronJob[],
|
|
123
|
+
selectedId: string | null,
|
|
124
|
+
): { nodes: Node[]; edges: Edge[] } {
|
|
125
|
+
const agentMapWithCrons = mergeAgentsWithCrons(agents, crons)
|
|
126
|
+
const { root, teams, soloOps } = buildTeams(agents)
|
|
127
|
+
if (!root) return { nodes: [], edges: [] }
|
|
128
|
+
|
|
129
|
+
// Collect all placed IDs
|
|
130
|
+
const placedIds = new Set<string>([root.id])
|
|
131
|
+
for (const t of teams) {
|
|
132
|
+
placedIds.add(t.manager.id)
|
|
133
|
+
t.members.forEach((m) => placedIds.add(m.id))
|
|
134
|
+
}
|
|
135
|
+
soloOps.forEach((a) => placedIds.add(a.id))
|
|
136
|
+
const disconnected = agents.filter((a) => !placedIds.has(a.id))
|
|
137
|
+
|
|
138
|
+
// Build columns
|
|
139
|
+
type Column = { label: string; color?: string; agentIds: string[]; edges: [string, string][] }
|
|
140
|
+
const columns: Column[] = []
|
|
141
|
+
|
|
142
|
+
const agentMap = new Map(agents.map((a) => [a.id, a]))
|
|
143
|
+
for (const t of teams) {
|
|
144
|
+
const ids = [t.manager.id, ...t.members.map((m) => m.id)]
|
|
145
|
+
const colEdges: [string, string][] = []
|
|
146
|
+
for (const id of ids) {
|
|
147
|
+
const a = agentMap.get(id)
|
|
148
|
+
if (!a) continue
|
|
149
|
+
for (const cid of a.directReports) {
|
|
150
|
+
if (ids.includes(cid)) colEdges.push([id, cid])
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
columns.push({ label: `Team ${t.manager.name}`, color: t.manager.color, agentIds: ids, edges: colEdges })
|
|
154
|
+
}
|
|
155
|
+
if (soloOps.length > 0) {
|
|
156
|
+
columns.push({ label: "Solo Ops", agentIds: soloOps.map((a) => a.id), edges: [] })
|
|
157
|
+
}
|
|
158
|
+
if (disconnected.length > 0) {
|
|
159
|
+
columns.push({ label: "Unlinked", agentIds: disconnected.map((a) => a.id), edges: [] })
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Layout each column with dagre independently, then offset horizontally
|
|
163
|
+
const nodes: Node[] = []
|
|
164
|
+
let cursorX = 0
|
|
165
|
+
const ROOT_Y = 0
|
|
166
|
+
const COLUMNS_TOP = 200
|
|
167
|
+
|
|
168
|
+
// Place root centered (we'll adjust x after computing total width)
|
|
169
|
+
const rootAgent = agentMapWithCrons.get(root.id)
|
|
170
|
+
|
|
171
|
+
type ColumnResult = { groupNode: Node; childNodes: Node[]; width: number }
|
|
172
|
+
const columnResults: ColumnResult[] = []
|
|
173
|
+
|
|
174
|
+
for (let ci = 0; ci < columns.length; ci++) {
|
|
175
|
+
const col = columns[ci]
|
|
176
|
+
const positions = dagreLayout(col.agentIds, col.edges, { nodesep: 40, ranksep: 90 })
|
|
177
|
+
|
|
178
|
+
// Compute bounding box of dagre output
|
|
179
|
+
let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity
|
|
180
|
+
for (const pos of positions.values()) {
|
|
181
|
+
minX = Math.min(minX, pos.x)
|
|
182
|
+
maxX = Math.max(maxX, pos.x + NODE_W)
|
|
183
|
+
minY = Math.min(minY, pos.y)
|
|
184
|
+
maxY = Math.max(maxY, pos.y + NODE_H)
|
|
185
|
+
}
|
|
186
|
+
const contentW = maxX - minX
|
|
187
|
+
const contentH = maxY - minY
|
|
188
|
+
const groupW = contentW + GROUP_PAD_X * 2
|
|
189
|
+
const groupH = GROUP_PAD_TOP + contentH + GROUP_PAD_BOTTOM
|
|
190
|
+
|
|
191
|
+
const groupId = `group-${ci}`
|
|
192
|
+
const groupNode: Node = {
|
|
193
|
+
id: groupId,
|
|
194
|
+
type: "teamGroup",
|
|
195
|
+
data: { label: col.label, color: col.color },
|
|
196
|
+
position: { x: cursorX, y: COLUMNS_TOP },
|
|
197
|
+
style: {
|
|
198
|
+
width: groupW,
|
|
199
|
+
height: groupH,
|
|
200
|
+
background: "var(--fill-quaternary)",
|
|
201
|
+
borderRadius: 12,
|
|
202
|
+
border: "1px solid var(--separator)",
|
|
203
|
+
padding: 0,
|
|
204
|
+
},
|
|
205
|
+
selectable: false,
|
|
206
|
+
draggable: false,
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const childNodes: Node[] = []
|
|
210
|
+
for (const id of col.agentIds) {
|
|
211
|
+
const agent = agentMapWithCrons.get(id)
|
|
212
|
+
const pos = positions.get(id)
|
|
213
|
+
if (!agent || !pos) continue
|
|
214
|
+
childNodes.push({
|
|
215
|
+
id,
|
|
216
|
+
type: "agentNode",
|
|
217
|
+
data: agent as unknown as Record<string, unknown>,
|
|
218
|
+
position: { x: pos.x - minX + GROUP_PAD_X, y: pos.y - minY + GROUP_PAD_TOP },
|
|
219
|
+
parentId: groupId,
|
|
220
|
+
extent: "parent" as const,
|
|
221
|
+
selected: id === selectedId,
|
|
222
|
+
})
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
columnResults.push({ groupNode, childNodes, width: groupW })
|
|
226
|
+
cursorX += groupW + COL_GAP
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const totalWidth = cursorX - COL_GAP
|
|
230
|
+
|
|
231
|
+
// Root node
|
|
232
|
+
if (rootAgent) {
|
|
233
|
+
nodes.push({
|
|
234
|
+
id: root.id,
|
|
235
|
+
type: "agentNode",
|
|
236
|
+
data: rootAgent as unknown as Record<string, unknown>,
|
|
237
|
+
position: { x: totalWidth / 2 - NODE_W / 2, y: ROOT_Y },
|
|
238
|
+
selected: root.id === selectedId,
|
|
239
|
+
})
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
for (const cr of columnResults) {
|
|
243
|
+
nodes.push(cr.groupNode)
|
|
244
|
+
nodes.push(...cr.childNodes)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
return { nodes, edges: buildEdges(agents, selectedId) }
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// ── Hierarchy layout (full dagre) ──────────────────────────────
|
|
251
|
+
|
|
252
|
+
function buildHierarchyLayout(
|
|
253
|
+
agents: Agent[],
|
|
254
|
+
crons: CronJob[],
|
|
255
|
+
selectedId: string | null,
|
|
256
|
+
): { nodes: Node[]; edges: Edge[] } {
|
|
257
|
+
const agentMapWithCrons = mergeAgentsWithCrons(agents, crons)
|
|
258
|
+
const agentMap = new Map(agents.map((a) => [a.id, a]))
|
|
259
|
+
|
|
260
|
+
const allIds = agents.map((a) => a.id)
|
|
261
|
+
const allEdges: [string, string][] = []
|
|
262
|
+
for (const a of agents) {
|
|
263
|
+
for (const cid of a.directReports) {
|
|
264
|
+
if (agentMap.has(cid)) allEdges.push([a.id, cid])
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const positions = dagreLayout(allIds, allEdges, { nodesep: 60, ranksep: 140 })
|
|
113
269
|
|
|
114
|
-
|
|
270
|
+
const nodes: Node[] = []
|
|
271
|
+
for (const a of agents) {
|
|
272
|
+
const agent = agentMapWithCrons.get(a.id)
|
|
273
|
+
const pos = positions.get(a.id)
|
|
274
|
+
if (!agent || !pos) continue
|
|
275
|
+
nodes.push({
|
|
276
|
+
id: a.id,
|
|
277
|
+
type: "agentNode",
|
|
278
|
+
data: agent as unknown as Record<string, unknown>,
|
|
279
|
+
position: pos,
|
|
280
|
+
selected: a.id === selectedId,
|
|
281
|
+
})
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return { nodes, edges: buildEdges(agents, selectedId) }
|
|
115
285
|
}
|
|
116
286
|
|
|
287
|
+
// ── Component ──────────────────────────────────────────────────
|
|
288
|
+
|
|
117
289
|
export function OrgMap({ agents, crons, selectedId, onNodeClick }: OrgMapProps) {
|
|
118
|
-
const
|
|
290
|
+
const [layout, setLayout] = useState<MapLayout>("hierarchy")
|
|
291
|
+
|
|
292
|
+
const build = layout === "teams" ? buildTeamLayout : buildHierarchyLayout
|
|
293
|
+
const { nodes: initialNodes, edges: initialEdges } = build(agents, crons, selectedId)
|
|
119
294
|
const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes)
|
|
120
295
|
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges)
|
|
121
296
|
|
|
122
297
|
useEffect(() => {
|
|
123
|
-
const { nodes: n, edges: e } =
|
|
298
|
+
const { nodes: n, edges: e } = build(agents, crons, selectedId)
|
|
124
299
|
setNodes(n)
|
|
125
300
|
setEdges(e)
|
|
126
|
-
}, [agents, crons, selectedId, setNodes, setEdges])
|
|
301
|
+
}, [agents, crons, selectedId, layout, setNodes, setEdges, build])
|
|
127
302
|
|
|
128
303
|
const handleNodeClick = useCallback(
|
|
129
304
|
(_: React.MouseEvent, node: Node) => {
|
|
@@ -152,6 +327,54 @@ export function OrgMap({ agents, crons, selectedId, onNodeClick }: OrgMapProps)
|
|
|
152
327
|
position="bottom-left"
|
|
153
328
|
style={{ left: 16, bottom: 16 }}
|
|
154
329
|
/>
|
|
330
|
+
|
|
331
|
+
{/* Layout toggle */}
|
|
332
|
+
<Panel
|
|
333
|
+
position="bottom-center"
|
|
334
|
+
style={{
|
|
335
|
+
display: "flex",
|
|
336
|
+
alignItems: "center",
|
|
337
|
+
gap: 2,
|
|
338
|
+
padding: 3,
|
|
339
|
+
borderRadius: "var(--radius-sm)",
|
|
340
|
+
background: "var(--material-regular)",
|
|
341
|
+
backdropFilter: "blur(20px)",
|
|
342
|
+
WebkitBackdropFilter: "blur(20px)",
|
|
343
|
+
border: "1px solid var(--separator)",
|
|
344
|
+
}}
|
|
345
|
+
>
|
|
346
|
+
{(["teams", "hierarchy"] as const).map((opt) => {
|
|
347
|
+
const isActive = layout === opt
|
|
348
|
+
return (
|
|
349
|
+
<button
|
|
350
|
+
key={opt}
|
|
351
|
+
onClick={() => setLayout(opt)}
|
|
352
|
+
className="focus-ring"
|
|
353
|
+
style={{
|
|
354
|
+
padding: "4px 12px",
|
|
355
|
+
borderRadius: "var(--radius-sm)",
|
|
356
|
+
fontSize: "var(--text-caption1)",
|
|
357
|
+
fontWeight: "var(--weight-medium)",
|
|
358
|
+
border: "none",
|
|
359
|
+
cursor: "pointer",
|
|
360
|
+
transition: "all 200ms var(--ease-smooth)",
|
|
361
|
+
...(isActive
|
|
362
|
+
? {
|
|
363
|
+
background: "var(--accent-fill)",
|
|
364
|
+
color: "var(--accent)",
|
|
365
|
+
boxShadow: "0 0 0 1px color-mix(in srgb, var(--accent) 40%, transparent)",
|
|
366
|
+
}
|
|
367
|
+
: {
|
|
368
|
+
background: "transparent",
|
|
369
|
+
color: "var(--text-secondary)",
|
|
370
|
+
}),
|
|
371
|
+
}}
|
|
372
|
+
>
|
|
373
|
+
{opt === "teams" ? "Teams" : "Hierarchy"}
|
|
374
|
+
</button>
|
|
375
|
+
)
|
|
376
|
+
})}
|
|
377
|
+
</Panel>
|
|
155
378
|
</ReactFlow>
|
|
156
379
|
)
|
|
157
380
|
}
|
package/lib/agents-registry.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readFileSync, existsSync, readdirSync } from 'fs'
|
|
2
|
+
import { execSync } from 'child_process'
|
|
2
3
|
import { join, basename } from 'path'
|
|
3
4
|
import bundledRegistry from '@/lib/agents.json'
|
|
4
5
|
import type { Agent } from '@/lib/types'
|
|
@@ -295,16 +296,100 @@ function discoverAgents(workspacePath: string): AgentEntry[] | null {
|
|
|
295
296
|
return discovered.length > 0 ? discovered : null
|
|
296
297
|
}
|
|
297
298
|
|
|
299
|
+
// ---------------------------------------------------------------------------
|
|
300
|
+
// CLI-based discovery (openclaw agents list)
|
|
301
|
+
// ---------------------------------------------------------------------------
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Discover agents via `openclaw agents list --json`.
|
|
305
|
+
* Returns AgentEntry[] for any agents found, or null on failure.
|
|
306
|
+
* This catches agents defined in openclaw.json that don't have
|
|
307
|
+
* filesystem directories under agents/.
|
|
308
|
+
*/
|
|
309
|
+
function discoverAgentsViaCli(openclawBin: string): AgentEntry[] | null {
|
|
310
|
+
try {
|
|
311
|
+
const raw = execSync(`${openclawBin} agents list --json`, {
|
|
312
|
+
encoding: 'utf-8',
|
|
313
|
+
timeout: 10000,
|
|
314
|
+
})
|
|
315
|
+
const parsed = JSON.parse(raw)
|
|
316
|
+
const agents: unknown[] = Array.isArray(parsed)
|
|
317
|
+
? parsed
|
|
318
|
+
: parsed.agents ?? parsed.data ?? []
|
|
319
|
+
|
|
320
|
+
if (agents.length === 0) return null
|
|
321
|
+
|
|
322
|
+
let colorIndex = 0
|
|
323
|
+
return agents.map((entry) => {
|
|
324
|
+
const a = entry as Record<string, unknown>
|
|
325
|
+
const id = String(a.id ?? a.name ?? a.slug ?? '')
|
|
326
|
+
const name = String(a.name ?? a.displayName ?? slugToName(id))
|
|
327
|
+
const title = String(a.title ?? a.role ?? 'Agent')
|
|
328
|
+
const reportsTo = a.reportsTo != null ? String(a.reportsTo) : null
|
|
329
|
+
const directReports = Array.isArray(a.directReports)
|
|
330
|
+
? a.directReports.map(String)
|
|
331
|
+
: []
|
|
332
|
+
const tools = Array.isArray(a.tools) ? a.tools.map(String) : ['read', 'write']
|
|
333
|
+
|
|
334
|
+
return {
|
|
335
|
+
id,
|
|
336
|
+
name,
|
|
337
|
+
title,
|
|
338
|
+
reportsTo,
|
|
339
|
+
directReports,
|
|
340
|
+
soulPath: typeof a.soulPath === 'string' ? a.soulPath : null,
|
|
341
|
+
voiceId: typeof a.voiceId === 'string' ? a.voiceId : null,
|
|
342
|
+
color: typeof a.color === 'string' ? a.color : DISCOVER_COLORS[colorIndex++ % DISCOVER_COLORS.length],
|
|
343
|
+
emoji: typeof a.emoji === 'string' ? a.emoji : name.charAt(0).toUpperCase(),
|
|
344
|
+
tools,
|
|
345
|
+
memoryPath: typeof a.memoryPath === 'string' ? a.memoryPath : null,
|
|
346
|
+
description: typeof a.description === 'string' ? a.description : `${name} agent.`,
|
|
347
|
+
}
|
|
348
|
+
})
|
|
349
|
+
} catch {
|
|
350
|
+
return null
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Merge CLI-discovered agents into an existing registry.
|
|
356
|
+
* Only adds agents whose IDs are not already present.
|
|
357
|
+
* Also patches directReports on existing agents to include new CLI agents.
|
|
358
|
+
*/
|
|
359
|
+
function mergeCliAgents(existing: AgentEntry[], cliAgents: AgentEntry[]): AgentEntry[] {
|
|
360
|
+
const existingIds = new Set(existing.map(a => a.id))
|
|
361
|
+
const added: AgentEntry[] = []
|
|
362
|
+
|
|
363
|
+
for (const ca of cliAgents) {
|
|
364
|
+
if (existingIds.has(ca.id)) continue
|
|
365
|
+
added.push(ca)
|
|
366
|
+
existingIds.add(ca.id)
|
|
367
|
+
|
|
368
|
+
// If the CLI agent reports to an existing agent, add it to that agent's directReports
|
|
369
|
+
if (ca.reportsTo) {
|
|
370
|
+
const parent = existing.find(a => a.id === ca.reportsTo)
|
|
371
|
+
if (parent && !parent.directReports.includes(ca.id)) {
|
|
372
|
+
parent.directReports.push(ca.id)
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
return [...existing, ...added]
|
|
378
|
+
}
|
|
379
|
+
|
|
298
380
|
/**
|
|
299
381
|
* Load the agent registry.
|
|
300
382
|
*
|
|
301
383
|
* Resolution order:
|
|
302
384
|
* 1. $WORKSPACE_PATH/clawport/agents.json (user's own config)
|
|
303
385
|
* 2. Auto-discovered from $WORKSPACE_PATH (agents/ directory scan)
|
|
304
|
-
*
|
|
386
|
+
* + merged with `openclaw agents list --json` (catches config-only agents)
|
|
387
|
+
* 3. CLI-only discovery via `openclaw agents list --json`
|
|
388
|
+
* 4. Bundled lib/agents.json (default example registry)
|
|
305
389
|
*/
|
|
306
390
|
export function loadRegistry(): AgentEntry[] {
|
|
307
391
|
const workspacePath = process.env.WORKSPACE_PATH
|
|
392
|
+
const openclawBin = process.env.OPENCLAW_BIN
|
|
308
393
|
|
|
309
394
|
if (workspacePath) {
|
|
310
395
|
// 1. User-provided override
|
|
@@ -318,11 +403,24 @@ export function loadRegistry(): AgentEntry[] {
|
|
|
318
403
|
}
|
|
319
404
|
}
|
|
320
405
|
|
|
321
|
-
// 2. Auto-discover from workspace
|
|
406
|
+
// 2. Auto-discover from workspace filesystem
|
|
322
407
|
const discovered = discoverAgents(workspacePath)
|
|
408
|
+
|
|
409
|
+
// 2b. Merge in any agents known to OpenClaw CLI but missing from filesystem
|
|
410
|
+
if (discovered && openclawBin) {
|
|
411
|
+
const cliAgents = discoverAgentsViaCli(openclawBin)
|
|
412
|
+
if (cliAgents) return mergeCliAgents(discovered, cliAgents)
|
|
413
|
+
return discovered
|
|
414
|
+
}
|
|
323
415
|
if (discovered) return discovered
|
|
416
|
+
|
|
417
|
+
// 3. CLI-only discovery (no workspace agents/ dir, but CLI knows about agents)
|
|
418
|
+
if (openclawBin) {
|
|
419
|
+
const cliAgents = discoverAgentsViaCli(openclawBin)
|
|
420
|
+
if (cliAgents) return cliAgents
|
|
421
|
+
}
|
|
324
422
|
}
|
|
325
423
|
|
|
326
|
-
//
|
|
424
|
+
// 4. Bundled fallback
|
|
327
425
|
return bundledRegistry as AgentEntry[]
|
|
328
426
|
}
|
package/lib/teams.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Agent } from "@/lib/types"
|
|
2
|
+
|
|
3
|
+
export interface Team {
|
|
4
|
+
manager: Agent
|
|
5
|
+
members: Agent[]
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function buildTeams(agents: Agent[]): { root: Agent | null; teams: Team[]; soloOps: Agent[] } {
|
|
9
|
+
const root = agents.find((a) => a.reportsTo === null) ?? null
|
|
10
|
+
if (!root) return { root: null, teams: [], soloOps: [] }
|
|
11
|
+
|
|
12
|
+
const byId = new Map(agents.map((a) => [a.id, a]))
|
|
13
|
+
const teamManagers: Agent[] = []
|
|
14
|
+
const soloOps: Agent[] = []
|
|
15
|
+
|
|
16
|
+
for (const rid of root.directReports) {
|
|
17
|
+
const r = byId.get(rid)
|
|
18
|
+
if (!r) continue
|
|
19
|
+
if (r.directReports.length > 0) {
|
|
20
|
+
teamManagers.push(r)
|
|
21
|
+
} else {
|
|
22
|
+
soloOps.push(r)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const teams: Team[] = teamManagers.map((mgr) => {
|
|
27
|
+
const members: Agent[] = []
|
|
28
|
+
const visited = new Set<string>([mgr.id])
|
|
29
|
+
const queue = [...mgr.directReports]
|
|
30
|
+
while (queue.length > 0) {
|
|
31
|
+
const id = queue.shift()!
|
|
32
|
+
if (visited.has(id)) continue
|
|
33
|
+
visited.add(id)
|
|
34
|
+
const a = byId.get(id)
|
|
35
|
+
if (a) {
|
|
36
|
+
members.push(a)
|
|
37
|
+
queue.push(...a.directReports)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return { manager: mgr, members }
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
return { root, teams, soloOps }
|
|
44
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clawport-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Open-source dashboard for managing, monitoring, and chatting with your OpenClaw AI agents.",
|
|
5
5
|
"homepage": "https://clawport.dev",
|
|
6
6
|
"repository": {
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
"prepublishOnly": "npx tsc --noEmit && vitest run"
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
|
+
"@dagrejs/dagre": "^2.0.4",
|
|
50
51
|
"@tailwindcss/postcss": "^4",
|
|
51
52
|
"@types/node": "^20",
|
|
52
53
|
"@types/react": "^19",
|