@agentprojectcontext/apx 1.68.0 → 1.70.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/core/agent/tools/handlers/_obsidian.js +28 -0
- package/src/core/agent/tools/handlers/obsidian-list-notes.js +28 -0
- package/src/core/agent/tools/handlers/obsidian-read-note.js +31 -0
- package/src/core/agent/tools/handlers/obsidian-search-notes.js +30 -0
- package/src/core/agent/tools/handlers/obsidian-write-note.js +38 -0
- package/src/core/agent/tools/names.js +10 -0
- package/src/core/agent/tools/registry.js +8 -0
- package/src/core/integrations/catalog.js +6 -3
- package/src/core/integrations/index.js +2 -0
- package/src/core/integrations/mcp-sync.js +71 -0
- package/src/core/integrations/obsidian-memory.js +108 -0
- package/src/core/integrations/plugins/obsidian.js +299 -0
- package/src/core/mcp/runner.js +54 -20
- package/src/core/memory/broker.js +4 -1
- package/src/core/memory/index.js +29 -2
- package/src/core/memory/indexer.js +88 -1
- package/src/core/memory/store.js +23 -3
- package/src/host/daemon/api/agents.js +37 -1
- package/src/host/daemon/api/integrations.js +27 -2
- package/src/host/daemon/api/mcps.js +2 -2
- package/src/host/daemon/index.js +7 -1
- package/src/interfaces/cli/commands/obsidian.js +79 -0
- package/src/interfaces/cli/index.js +48 -1
- package/src/interfaces/web/dist/assets/index-CDz9OwCP.css +1 -0
- package/src/interfaces/web/dist/assets/index-YFsZFhM6.js +798 -0
- package/src/interfaces/web/dist/assets/index-YFsZFhM6.js.map +1 -0
- package/src/interfaces/web/dist/index.html +2 -2
- package/src/interfaces/web/src/components/integrations/BrandLogos.tsx +29 -0
- package/src/interfaces/web/src/components/integrations/ComingSoonPlugin.tsx +5 -4
- package/src/interfaces/web/src/components/integrations/FolderInput.tsx +137 -0
- package/src/interfaces/web/src/components/integrations/PluginConnect.tsx +129 -10
- package/src/interfaces/web/src/i18n/en.ts +25 -0
- package/src/interfaces/web/src/i18n/es.ts +25 -0
- package/src/interfaces/web/src/lib/api/agents.ts +2 -1
- package/src/interfaces/web/src/lib/api/integrations.ts +10 -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,21 +1,34 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from "react";
|
|
2
2
|
import {
|
|
3
|
-
forceSimulation, forceLink, forceManyBody, forceCenter, forceCollide,
|
|
3
|
+
forceSimulation, forceLink, forceManyBody, forceCenter, forceCollide, forceX, forceY,
|
|
4
4
|
type Simulation,
|
|
5
5
|
} from "d3-force";
|
|
6
6
|
import { t } from "../../i18n";
|
|
7
7
|
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// (
|
|
8
|
+
// Generic animated "brain" graph (d3-force + SVG). It takes an explicit node +
|
|
9
|
+
// edge set so callers can model *any* topology — a single agent's hubbed brain
|
|
10
|
+
// (Memory / Threads / Tasks / Routines / Team + cross-links) or the whole
|
|
11
|
+
// project's agent map (orchestrators with their specialists as satellites).
|
|
12
|
+
//
|
|
13
|
+
// Motion is SMIL/CSS driven (breathing halos, a pulsing core, energy beads
|
|
14
|
+
// flowing down the edges) so the graph stays alive without React re-renders;
|
|
15
|
+
// d3 only drives layout.
|
|
16
|
+
|
|
17
|
+
export type BrainKind =
|
|
18
|
+
| "agent" | "memory" | "thread" | "task" | "routine" | "agentlink" | "hub";
|
|
19
|
+
export type BrainRole = "core" | "hub" | "leaf";
|
|
11
20
|
|
|
12
21
|
export interface BrainNode {
|
|
13
22
|
id: string;
|
|
14
23
|
label: string;
|
|
15
|
-
kind:
|
|
16
|
-
|
|
24
|
+
kind: BrainKind;
|
|
25
|
+
role?: BrainRole; // visual weight; defaults to "leaf"
|
|
26
|
+
relation?: string;
|
|
17
27
|
detail?: string;
|
|
28
|
+
emoji?: string;
|
|
29
|
+
slug?: string; // for navigation (project map)
|
|
18
30
|
}
|
|
31
|
+
export interface BrainEdge { source: string; target: string; }
|
|
19
32
|
|
|
20
33
|
interface SimNode extends BrainNode {
|
|
21
34
|
x?: number; y?: number; vx?: number; vy?: number;
|
|
@@ -23,20 +36,32 @@ interface SimNode extends BrainNode {
|
|
|
23
36
|
}
|
|
24
37
|
interface SimLink { source: SimNode; target: SimNode; }
|
|
25
38
|
|
|
26
|
-
const KIND_COLOR: Record<
|
|
39
|
+
const KIND_COLOR: Record<BrainKind, string> = {
|
|
27
40
|
agent: "#a78bfa", memory: "#38bdf8", thread: "#34d399",
|
|
28
|
-
task: "#fbbf24", routine: "#f472b6", agentlink: "#c084fc",
|
|
41
|
+
task: "#fbbf24", routine: "#f472b6", agentlink: "#c084fc", hub: "#94a3b8",
|
|
29
42
|
};
|
|
30
|
-
function
|
|
31
|
-
|
|
43
|
+
function kindLabel(k: BrainKind): string {
|
|
44
|
+
const m: Record<string, string> = {
|
|
32
45
|
agent: t("agents_ui.kind_agent"), memory: t("agents_ui.kind_memory"), thread: t("agents_ui.kind_thread"),
|
|
33
46
|
task: t("agents_ui.kind_task"), routine: t("agents_ui.kind_routine"), agentlink: t("agents_ui.kind_hierarchy"),
|
|
34
47
|
};
|
|
48
|
+
return m[k] ?? k;
|
|
35
49
|
}
|
|
36
50
|
|
|
37
|
-
const
|
|
51
|
+
const RADIUS: Record<BrainRole, number> = { core: 24, hub: 12, leaf: 6 };
|
|
52
|
+
const roleOf = (n: BrainNode): BrainRole => n.role ?? "leaf";
|
|
53
|
+
|
|
54
|
+
export function BrainGraph({
|
|
55
|
+
nodes, edges, height = 460, onNodeClick,
|
|
56
|
+
}: {
|
|
57
|
+
nodes: BrainNode[];
|
|
58
|
+
edges: BrainEdge[];
|
|
59
|
+
height?: number;
|
|
60
|
+
onNodeClick?: (n: BrainNode) => void;
|
|
61
|
+
}) {
|
|
62
|
+
const W = 760, H = height;
|
|
63
|
+
const CX = W / 2, CY = H / 2;
|
|
38
64
|
|
|
39
|
-
export function AgentBrainGraph({ center, nodes }: { center: string; nodes: BrainNode[] }) {
|
|
40
65
|
const svgRef = useRef<SVGSVGElement | null>(null);
|
|
41
66
|
const simRef = useRef<Simulation<SimNode, SimLink> | null>(null);
|
|
42
67
|
const nodesRef = useRef<SimNode[]>([]);
|
|
@@ -45,22 +70,56 @@ export function AgentBrainGraph({ center, nodes }: { center: string; nodes: Brai
|
|
|
45
70
|
const [, setVersion] = useState(0);
|
|
46
71
|
const [selected, setSelected] = useState<BrainNode | null>(null);
|
|
47
72
|
|
|
73
|
+
const hideLeafLabels = nodes.length > 44;
|
|
74
|
+
|
|
48
75
|
useEffect(() => {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const
|
|
76
|
+
// Seed positions so the first paint already looks like a graph (no blank
|
|
77
|
+
// flash, and a sane starting layout for d3).
|
|
78
|
+
const coreR = Math.min(W, H) * 0.30;
|
|
79
|
+
const hubs = nodes.filter((n) => roleOf(n) === "hub");
|
|
80
|
+
const hubAngle = new Map<string, number>();
|
|
81
|
+
hubs.forEach((h, i) => hubAngle.set(h.id, (i / Math.max(1, hubs.length)) * Math.PI * 2 - Math.PI / 2));
|
|
82
|
+
|
|
83
|
+
const simNodes: SimNode[] = nodes.map((n, i) => {
|
|
84
|
+
const role = roleOf(n);
|
|
85
|
+
if (role === "core") return { ...n, x: CX, y: CY, fx: CX, fy: CY };
|
|
86
|
+
const a = hubAngle.get(n.id) ?? (i / Math.max(1, nodes.length)) * Math.PI * 2;
|
|
87
|
+
const r = role === "hub" ? coreR : coreR * 1.7;
|
|
88
|
+
return { ...n, x: CX + Math.cos(a) * r, y: CY + Math.sin(a) * r };
|
|
89
|
+
});
|
|
90
|
+
const byId = new Map(simNodes.map((n) => [n.id, n]));
|
|
91
|
+
const links: SimLink[] = edges
|
|
92
|
+
.map((e) => ({ source: byId.get(e.source), target: byId.get(e.target) }))
|
|
93
|
+
.filter((l): l is SimLink => !!l.source && !!l.target);
|
|
94
|
+
|
|
52
95
|
nodesRef.current = simNodes;
|
|
53
96
|
linksRef.current = links;
|
|
97
|
+
setVersion((v) => v + 1); // commit the seeded layout even if the tick timer is throttled
|
|
98
|
+
|
|
99
|
+
const linkDist = (l: SimLink) => {
|
|
100
|
+
const ra = roleOf(l.source), rb = roleOf(l.target);
|
|
101
|
+
if (ra === "core" || rb === "core") return 150;
|
|
102
|
+
if (ra === "hub" && rb === "hub") return 120;
|
|
103
|
+
return 62;
|
|
104
|
+
};
|
|
105
|
+
const charge = (n: SimNode) => {
|
|
106
|
+
const r = roleOf(n);
|
|
107
|
+
return r === "core" ? -620 : r === "hub" ? -320 : -110;
|
|
108
|
+
};
|
|
54
109
|
|
|
55
110
|
const sim = forceSimulation<SimNode>(simNodes)
|
|
56
|
-
.force("link", forceLink<SimNode, SimLink>(links).distance(
|
|
57
|
-
.force("charge", forceManyBody().strength(
|
|
58
|
-
.force("center", forceCenter(
|
|
59
|
-
.force("
|
|
111
|
+
.force("link", forceLink<SimNode, SimLink>(links).distance(linkDist).strength(0.55))
|
|
112
|
+
.force("charge", forceManyBody<SimNode>().strength(charge))
|
|
113
|
+
.force("center", forceCenter(CX, CY).strength(0.04))
|
|
114
|
+
.force("x", forceX(CX).strength(0.03))
|
|
115
|
+
.force("y", forceY(CY).strength(0.03))
|
|
116
|
+
.force("collide", forceCollide<SimNode>((n) => RADIUS[roleOf(n)] + 8))
|
|
117
|
+
.alphaDecay(0.028)
|
|
60
118
|
.on("tick", () => setVersion((v) => v + 1));
|
|
61
119
|
simRef.current = sim;
|
|
62
120
|
return () => { sim.stop(); };
|
|
63
|
-
|
|
121
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
122
|
+
}, [nodes, edges, height]);
|
|
64
123
|
|
|
65
124
|
const toSvg = (e: React.PointerEvent) => {
|
|
66
125
|
const rect = svgRef.current!.getBoundingClientRect();
|
|
@@ -70,7 +129,7 @@ export function AgentBrainGraph({ center, nodes }: { center: string; nodes: Brai
|
|
|
70
129
|
};
|
|
71
130
|
};
|
|
72
131
|
const onDown = (n: SimNode) => (e: React.PointerEvent) => {
|
|
73
|
-
if (n
|
|
132
|
+
if (roleOf(n) === "core") return;
|
|
74
133
|
dragRef.current = n;
|
|
75
134
|
(e.target as Element).setPointerCapture?.(e.pointerId);
|
|
76
135
|
simRef.current?.alphaTarget(0.3).restart();
|
|
@@ -91,41 +150,106 @@ export function AgentBrainGraph({ center, nodes }: { center: string; nodes: Brai
|
|
|
91
150
|
const simNodes = nodesRef.current;
|
|
92
151
|
const links = linksRef.current;
|
|
93
152
|
|
|
153
|
+
// Legend: the item kinds actually present (exclude the core agent + structural hubs).
|
|
154
|
+
const legendKinds = [...new Set(nodes.map((n) => n.kind))].filter((k) => k !== "agent" && k !== "hub");
|
|
155
|
+
|
|
156
|
+
const pick = (n: SimNode) => { setSelected(n); onNodeClick?.(n); };
|
|
157
|
+
|
|
94
158
|
return (
|
|
95
159
|
<div className="space-y-3">
|
|
96
|
-
<div className="overflow-hidden rounded-xl border border-border bg-muted/
|
|
160
|
+
<div className="overflow-hidden rounded-xl border border-border bg-gradient-to-b from-background to-muted/20">
|
|
97
161
|
<svg
|
|
98
162
|
ref={svgRef}
|
|
99
163
|
viewBox={`0 0 ${W} ${H}`}
|
|
100
|
-
|
|
164
|
+
style={{ height }}
|
|
165
|
+
className="w-full touch-none select-none"
|
|
101
166
|
onPointerMove={onMove}
|
|
102
167
|
onPointerUp={onUp}
|
|
103
168
|
onPointerLeave={onUp}
|
|
104
169
|
>
|
|
105
|
-
|
|
106
|
-
<
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
170
|
+
<defs>
|
|
171
|
+
<filter id="brain-glow" x="-80%" y="-80%" width="260%" height="260%">
|
|
172
|
+
<feGaussianBlur stdDeviation="3" result="blur" />
|
|
173
|
+
<feMerge><feMergeNode in="blur" /><feMergeNode in="SourceGraphic" /></feMerge>
|
|
174
|
+
</filter>
|
|
175
|
+
<radialGradient id="brain-core" cx="50%" cy="50%" r="50%">
|
|
176
|
+
<stop offset="0%" stopColor={KIND_COLOR.agent} stopOpacity="0.9" />
|
|
177
|
+
<stop offset="55%" stopColor={KIND_COLOR.agent} stopOpacity="0.35" />
|
|
178
|
+
<stop offset="100%" stopColor={KIND_COLOR.agent} stopOpacity="0" />
|
|
179
|
+
</radialGradient>
|
|
180
|
+
<radialGradient id="brain-bg" cx="50%" cy="50%" r="60%">
|
|
181
|
+
<stop offset="0%" stopColor={KIND_COLOR.agent} stopOpacity="0.10" />
|
|
182
|
+
<stop offset="100%" stopColor={KIND_COLOR.agent} stopOpacity="0" />
|
|
183
|
+
</radialGradient>
|
|
184
|
+
</defs>
|
|
185
|
+
|
|
186
|
+
<rect x={0} y={0} width={W} height={H} fill="url(#brain-bg)" />
|
|
187
|
+
|
|
188
|
+
{/* Edges: faint spine + a bead of energy flowing along it */}
|
|
189
|
+
{links.map((l, i) => {
|
|
190
|
+
const color = KIND_COLOR[l.target.kind === "hub" ? l.source.kind : l.target.kind];
|
|
191
|
+
const dur = 1.4 + (i % 5) * 0.35;
|
|
192
|
+
return (
|
|
193
|
+
<g key={i}>
|
|
194
|
+
<line x1={l.source.x} y1={l.source.y} x2={l.target.x} y2={l.target.y}
|
|
195
|
+
stroke={color} strokeOpacity={0.16} strokeWidth={1.4} />
|
|
196
|
+
<line x1={l.target.x} y1={l.target.y} x2={l.source.x} y2={l.source.y}
|
|
197
|
+
stroke={color} strokeOpacity={0.5} strokeWidth={2}
|
|
198
|
+
strokeLinecap="round" strokeDasharray="1 14">
|
|
199
|
+
<animate attributeName="stroke-dashoffset" values="15;0" dur={`${dur}s`} repeatCount="indefinite" />
|
|
200
|
+
</line>
|
|
201
|
+
</g>
|
|
202
|
+
);
|
|
203
|
+
})}
|
|
204
|
+
|
|
205
|
+
{/* Nodes */}
|
|
206
|
+
{simNodes.map((n, idx) => {
|
|
207
|
+
const role = roleOf(n);
|
|
208
|
+
const color = KIND_COLOR[n.kind];
|
|
209
|
+
if (role === "core") {
|
|
210
|
+
const display = (n.emoji && n.emoji.trim()) || n.label.slice(0, 2).toUpperCase();
|
|
111
211
|
return (
|
|
112
212
|
<g key={n.id} transform={`translate(${n.x},${n.y})`}>
|
|
113
|
-
<circle r={
|
|
114
|
-
|
|
115
|
-
|
|
213
|
+
<circle r={54} fill="url(#brain-core)">
|
|
214
|
+
<animate attributeName="r" values="50;58;50" dur="4s" repeatCount="indefinite" />
|
|
215
|
+
<animate attributeName="opacity" values="0.85;1;0.85" dur="4s" repeatCount="indefinite" />
|
|
216
|
+
</circle>
|
|
217
|
+
<circle r={26} fill="none" stroke={KIND_COLOR.agent} strokeWidth={1.5} opacity={0.5}>
|
|
218
|
+
<animate attributeName="r" values="26;48" dur="3.2s" repeatCount="indefinite" />
|
|
219
|
+
<animate attributeName="opacity" values="0.5;0" dur="3.2s" repeatCount="indefinite" />
|
|
220
|
+
</circle>
|
|
221
|
+
<circle r={24} fill={KIND_COLOR.agent} filter="url(#brain-glow)" />
|
|
222
|
+
<circle r={24} fill="none" stroke="#ffffff" strokeOpacity={0.35} strokeWidth={1} />
|
|
223
|
+
<text textAnchor="middle" dominantBaseline="central" fontSize={display.length <= 2 ? 20 : 11} fontWeight={700} fill="#1a1030">
|
|
224
|
+
{display.length > 8 ? display.slice(0, 8) : display}
|
|
116
225
|
</text>
|
|
117
226
|
</g>
|
|
118
227
|
);
|
|
119
228
|
}
|
|
120
229
|
const isSel = selected?.id === n.id;
|
|
230
|
+
const r = RADIUS[role] + (isSel ? 3 : 0);
|
|
231
|
+
const beat = 2.4 + (idx % 6) * 0.4;
|
|
232
|
+
const begin = `${(idx % 6) * 0.3}s`;
|
|
233
|
+
const isHub = role === "hub";
|
|
234
|
+
const showLabel = isHub || isSel || !hideLeafLabels;
|
|
121
235
|
return (
|
|
122
236
|
<g key={n.id} transform={`translate(${n.x},${n.y})`} className="cursor-grab active:cursor-grabbing"
|
|
123
|
-
onPointerDown={onDown(n)} onClick={() =>
|
|
124
|
-
<circle r={
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
237
|
+
onPointerDown={onDown(n)} onClick={() => pick(n)}>
|
|
238
|
+
<circle r={r} fill={color} filter="url(#brain-glow)" opacity={0.3}>
|
|
239
|
+
<animate attributeName="r" values={`${r};${r + 6};${r}`} dur={`${beat}s`} begin={begin} repeatCount="indefinite" />
|
|
240
|
+
<animate attributeName="opacity" values="0.32;0.08;0.32" dur={`${beat}s`} begin={begin} repeatCount="indefinite" />
|
|
241
|
+
</circle>
|
|
242
|
+
<circle r={r} fill={color} fillOpacity={isSel ? 1 : 0.95}
|
|
243
|
+
stroke={isSel ? "#fff" : "#ffffff"} strokeOpacity={isSel ? 1 : 0.25} strokeWidth={isSel ? 2 : 1} />
|
|
244
|
+
{n.emoji && isHub && (
|
|
245
|
+
<text textAnchor="middle" dominantBaseline="central" fontSize={11} style={{ pointerEvents: "none" }}>{n.emoji}</text>
|
|
246
|
+
)}
|
|
247
|
+
{showLabel && (
|
|
248
|
+
<text x={r + 4} y={4} fontSize={isHub ? 11 : 10}
|
|
249
|
+
className={isHub ? "fill-foreground font-medium" : "fill-foreground/80"} style={{ pointerEvents: "none" }}>
|
|
250
|
+
{n.label.length > 22 ? `${n.label.slice(0, 22)}…` : n.label}
|
|
251
|
+
</text>
|
|
252
|
+
)}
|
|
129
253
|
</g>
|
|
130
254
|
);
|
|
131
255
|
})}
|
|
@@ -133,22 +257,20 @@ export function AgentBrainGraph({ center, nodes }: { center: string; nodes: Brai
|
|
|
133
257
|
</div>
|
|
134
258
|
|
|
135
259
|
<div className="flex flex-wrap items-center gap-3 text-[11px] text-muted-fg">
|
|
136
|
-
{(() =>
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
</span>
|
|
142
|
-
));
|
|
143
|
-
})()}
|
|
260
|
+
{legendKinds.map((k) => (
|
|
261
|
+
<span key={k} className="inline-flex items-center gap-1">
|
|
262
|
+
<span className="size-2 rounded-full" style={{ background: KIND_COLOR[k] }} /> {kindLabel(k)}
|
|
263
|
+
</span>
|
|
264
|
+
))}
|
|
144
265
|
<span className="ml-auto">{t("agents_ui.nodes_drag_hint", { n: String(nodes.length) })}</span>
|
|
145
266
|
</div>
|
|
146
267
|
{selected && (
|
|
147
268
|
<div className="rounded-lg border border-border bg-card p-3 text-xs">
|
|
148
269
|
<div className="flex items-center gap-2">
|
|
149
270
|
<span className="size-2 rounded-full" style={{ background: KIND_COLOR[selected.kind] }} />
|
|
271
|
+
{selected.emoji && <span>{selected.emoji}</span>}
|
|
150
272
|
<span className="font-medium">{selected.label}</span>
|
|
151
|
-
<span className="text-muted-fg">· {selected.relation}</span>
|
|
273
|
+
{selected.relation && <span className="text-muted-fg">· {selected.relation}</span>}
|
|
152
274
|
</div>
|
|
153
275
|
{selected.detail && <p className="mt-1 whitespace-pre-wrap text-muted-fg">{selected.detail}</p>}
|
|
154
276
|
</div>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useMemo, useState } from "react";
|
|
2
2
|
import { useNavigate, useParams } from "react-router-dom";
|
|
3
3
|
import useSWR from "swr";
|
|
4
4
|
import {
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
Heart, MessagesSquare, Save, Send, Settings, Sparkles, Trash2, Wrench, Activity,
|
|
7
7
|
} from "lucide-react";
|
|
8
8
|
import { Agents, Conversations, Messages, Routines, Tasks, Tools } from "../../lib/api";
|
|
9
|
-
import type { AgentDetail, AgentEntry, MessageEntry, RoutineEntry } from "../../types/daemon";
|
|
9
|
+
import type { AgentDetail, AgentEntry, FileContent, MessageEntry, RoutineEntry } from "../../types/daemon";
|
|
10
10
|
import { Section } from "../../components/Section";
|
|
11
11
|
import { Badge, Button, Field, Input, Loading, Switch, Textarea } from "../../components/ui";
|
|
12
12
|
import { Tip } from "../../components/ui/tip";
|
|
@@ -14,10 +14,11 @@ import { UiSelect } from "../../components/UiSelect";
|
|
|
14
14
|
import { useToast } from "../../components/Toast";
|
|
15
15
|
import { ConfirmDialog } from "../../components/common/ConfirmDialog";
|
|
16
16
|
import { EmojiInput, AutonomyPicker, AreaRoleFields } from "../../components/agents/AgentFormFields";
|
|
17
|
+
import { FileViewer } from "../../components/files/FileViewer";
|
|
17
18
|
import { cn } from "../../lib/cn";
|
|
18
19
|
import { t } from "../../i18n";
|
|
19
20
|
import type { AgentAutonomy } from "../../types/daemon";
|
|
20
|
-
import {
|
|
21
|
+
import { BrainGraph, type BrainNode, type BrainEdge } from "./AgentBrainGraph";
|
|
21
22
|
|
|
22
23
|
type TabKey = "overview" | "memories" | "records" | "sleep" | "brain" | "config";
|
|
23
24
|
function buildTabs(): { key: TabKey; label: string; icon: typeof Bot }[] {
|
|
@@ -167,7 +168,7 @@ export function AgentDetailScreen({ pid }: { pid: string }) {
|
|
|
167
168
|
</div>
|
|
168
169
|
)}
|
|
169
170
|
|
|
170
|
-
{tab === "memories" && <MemoryEditor pid={pid} slug={slug}
|
|
171
|
+
{tab === "memories" && <MemoryEditor pid={pid} slug={slug} onSaved={() => detail.mutate()} />}
|
|
171
172
|
|
|
172
173
|
{tab === "records" && <RecordsList records={records.data || []} loading={records.isLoading} />}
|
|
173
174
|
|
|
@@ -176,6 +177,7 @@ export function AgentDetailScreen({ pid }: { pid: string }) {
|
|
|
176
177
|
{tab === "brain" && (
|
|
177
178
|
<BrainTab
|
|
178
179
|
slug={slug}
|
|
180
|
+
emoji={a.emoji || undefined}
|
|
179
181
|
memory={a.memory || ""}
|
|
180
182
|
threads={(threads.data || []).map((t) => ({ id: t.id, label: t.title || t.filename }))}
|
|
181
183
|
tasks={myTasks.map((t) => ({ id: t.id, label: t.title, detail: t.body || undefined }))}
|
|
@@ -311,26 +313,38 @@ function Stat({ label, value, icon: I }: { label: string; value: number; icon: t
|
|
|
311
313
|
);
|
|
312
314
|
}
|
|
313
315
|
|
|
314
|
-
|
|
316
|
+
// Durable memory for a single agent, using the same docs-style editor as the
|
|
317
|
+
// project /memories surface (markdown edit / split-preview / save) instead of a
|
|
318
|
+
// bare textarea.
|
|
319
|
+
function MemoryEditor({ pid, slug, onSaved }: { pid: string; slug: string; onSaved: () => void }) {
|
|
315
320
|
const toast = useToast();
|
|
316
|
-
const
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
321
|
+
const body = useSWR(`/memory/${pid}/agent:${slug}`, () => Agents.memory.get(pid, slug).then((r) => r.body));
|
|
322
|
+
|
|
323
|
+
const file = useMemo<FileContent | null>(() => {
|
|
324
|
+
if (body.data === undefined) return null;
|
|
325
|
+
const content = body.data ?? "";
|
|
326
|
+
return {
|
|
327
|
+
path: `agents/${slug}/memory.md`,
|
|
328
|
+
name: "memory.md",
|
|
329
|
+
kind: "markdown",
|
|
330
|
+
size: content.length,
|
|
331
|
+
modified: "",
|
|
332
|
+
encoding: "utf8",
|
|
333
|
+
content,
|
|
334
|
+
};
|
|
335
|
+
}, [body.data, slug]);
|
|
336
|
+
|
|
337
|
+
const onSave = async (content: string) => {
|
|
338
|
+
await Agents.memory.put(pid, slug, content);
|
|
339
|
+
toast.success(t("project.agent_detail.memory_saved"));
|
|
340
|
+
void body.mutate(content, { revalidate: false });
|
|
341
|
+
onSaved();
|
|
325
342
|
};
|
|
343
|
+
|
|
326
344
|
return (
|
|
327
|
-
<
|
|
328
|
-
<
|
|
329
|
-
|
|
330
|
-
<span className="text-[11px] text-muted-fg">{value.length} {t("project.memories.chars")}</span>
|
|
331
|
-
<Button size="sm" variant="primary" loading={busy} disabled={!dirty} onClick={save}><Save size={12} /> {t("project.memories.save_btn")}</Button>
|
|
332
|
-
</div>
|
|
333
|
-
</Section>
|
|
345
|
+
<div className="flex h-[65vh] min-h-[420px] flex-col overflow-hidden rounded-xl border border-border bg-card">
|
|
346
|
+
<FileViewer file={file} loading={body.isLoading} onSave={onSave} />
|
|
347
|
+
</div>
|
|
334
348
|
);
|
|
335
349
|
}
|
|
336
350
|
|
|
@@ -447,10 +461,28 @@ function ToolsPicker({ value, onChange }: { value: string; onChange: (v: string)
|
|
|
447
461
|
);
|
|
448
462
|
}
|
|
449
463
|
|
|
464
|
+
// Cross-link heuristic: two items are "related" when their titles share a
|
|
465
|
+
// meaningful word (ignoring short/stop words). Used to wire tasks↔threads so
|
|
466
|
+
// the brain reads as a web, not a wheel.
|
|
467
|
+
const STOP = new Set([
|
|
468
|
+
"the", "and", "for", "with", "from", "into", "your", "that", "this", "una", "las", "los",
|
|
469
|
+
"del", "por", "con", "para", "post", "posts", "demo", "week", "weekly",
|
|
470
|
+
]);
|
|
471
|
+
function keywords(s: string): Set<string> {
|
|
472
|
+
return new Set(
|
|
473
|
+
s.toLowerCase().split(/[^a-záéíóúñ0-9]+/).filter((w) => w.length > 3 && !STOP.has(w)),
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
function shareKeyword(a: Set<string>, b: Set<string>): boolean {
|
|
477
|
+
for (const w of a) if (b.has(w)) return true;
|
|
478
|
+
return false;
|
|
479
|
+
}
|
|
480
|
+
|
|
450
481
|
function BrainTab({
|
|
451
|
-
slug, memory, threads, tasks, routines, parent, children,
|
|
482
|
+
slug, emoji, memory, threads, tasks, routines, parent, children,
|
|
452
483
|
}: {
|
|
453
484
|
slug: string;
|
|
485
|
+
emoji?: string;
|
|
454
486
|
memory: string;
|
|
455
487
|
threads: { id: string; label: string }[];
|
|
456
488
|
tasks: { id: string; label: string; detail?: string }[];
|
|
@@ -458,22 +490,64 @@ function BrainTab({
|
|
|
458
490
|
parent: string | null;
|
|
459
491
|
children: string[];
|
|
460
492
|
}) {
|
|
461
|
-
const nodes
|
|
462
|
-
const
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
493
|
+
const { nodes, edges } = useMemo(() => {
|
|
494
|
+
const nodes: BrainNode[] = [];
|
|
495
|
+
const edges: BrainEdge[] = [];
|
|
496
|
+
const CORE = "__core";
|
|
497
|
+
nodes.push({ id: CORE, label: slug, kind: "agent", role: "core", emoji, relation: "self" });
|
|
498
|
+
|
|
499
|
+
// A category hub groups its items so items hang off the hub (a two-level
|
|
500
|
+
// tree) instead of all wiring straight to the core.
|
|
501
|
+
const hub = (id: string, label: string, kind: BrainNode["kind"]) => {
|
|
502
|
+
nodes.push({ id, label, kind, role: "hub", relation: "cluster" });
|
|
503
|
+
edges.push({ source: CORE, target: id });
|
|
504
|
+
};
|
|
505
|
+
|
|
506
|
+
const mem = memoryFacts(memory);
|
|
507
|
+
const th = threads.slice(0, 8);
|
|
508
|
+
const ts = tasks.slice(0, 8);
|
|
509
|
+
|
|
510
|
+
if (mem.length) {
|
|
511
|
+
hub("hub-mem", t("agents_ui.kind_memory"), "memory");
|
|
512
|
+
mem.forEach((f, i) => { nodes.push({ id: `m${i}`, label: f, kind: "memory", relation: "knows", detail: f }); edges.push({ source: "hub-mem", target: `m${i}` }); });
|
|
513
|
+
}
|
|
514
|
+
if (th.length) {
|
|
515
|
+
hub("hub-thread", t("agents_ui.kind_thread"), "thread");
|
|
516
|
+
th.forEach((x) => { nodes.push({ id: `th-${x.id}`, label: x.label, kind: "thread", relation: "in_thread" }); edges.push({ source: "hub-thread", target: `th-${x.id}` }); });
|
|
517
|
+
}
|
|
518
|
+
if (ts.length) {
|
|
519
|
+
hub("hub-task", t("agents_ui.kind_task"), "task");
|
|
520
|
+
ts.forEach((x) => { nodes.push({ id: `ts-${x.id}`, label: x.label, kind: "task", relation: "handles_task", detail: x.detail }); edges.push({ source: "hub-task", target: `ts-${x.id}` }); });
|
|
521
|
+
}
|
|
522
|
+
if (routines.length) {
|
|
523
|
+
hub("hub-routine", t("agents_ui.kind_routine"), "routine");
|
|
524
|
+
routines.forEach((r) => { nodes.push({ id: `rt-${r.name}`, label: r.name, kind: "routine", relation: "ticks", detail: `schedule: ${r.schedule}` }); edges.push({ source: "hub-routine", target: `rt-${r.name}` }); });
|
|
525
|
+
}
|
|
526
|
+
if (children.length) {
|
|
527
|
+
hub("hub-team", t("agents_ui.kind_hierarchy"), "agentlink");
|
|
528
|
+
children.forEach((c) => { nodes.push({ id: `c-${c}`, label: c, kind: "agentlink", role: "hub", relation: "orchestrates", slug: c }); edges.push({ source: "hub-team", target: `c-${c}` }); });
|
|
529
|
+
}
|
|
530
|
+
if (parent) {
|
|
531
|
+
nodes.push({ id: `p-${parent}`, label: parent, kind: "agentlink", role: "hub", relation: "reports_to", slug: parent });
|
|
532
|
+
edges.push({ source: `p-${parent}`, target: CORE });
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Cross-links: wire a task to a thread that shares a keyword (first match).
|
|
536
|
+
const thKw = th.map((x) => ({ id: `th-${x.id}`, kw: keywords(x.label) }));
|
|
537
|
+
ts.forEach((x) => {
|
|
538
|
+
const kw = keywords(x.label);
|
|
539
|
+
const hit = thKw.find((tk) => shareKeyword(kw, tk.kw));
|
|
540
|
+
if (hit) edges.push({ source: `ts-${x.id}`, target: hit.id });
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
return { nodes, edges };
|
|
544
|
+
}, [slug, emoji, memory, threads, tasks, routines, parent, children]);
|
|
471
545
|
|
|
472
546
|
return (
|
|
473
547
|
<Section title={t("project.agent_detail.brain_title")} description={t("project.agent_detail.brain_desc")}>
|
|
474
|
-
{nodes.length
|
|
548
|
+
{nodes.length <= 1
|
|
475
549
|
? <p className="text-xs text-muted-fg">{t("project.agent_detail.brain_empty")}</p>
|
|
476
|
-
: <
|
|
550
|
+
: <BrainGraph nodes={nodes} edges={edges} />}
|
|
477
551
|
</Section>
|
|
478
552
|
);
|
|
479
553
|
}
|