@agentprojectcontext/apx 1.67.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/core/config/redact.js +44 -0
- package/src/host/daemon/api/agents.js +37 -1
- package/src/host/daemon/api/config.js +17 -5
- package/src/host/daemon/api/sessions.js +9 -0
- 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/package-lock.json +3 -3
- package/src/interfaces/web/src/components/config/ConfigTabsEditor.tsx +46 -31
- package/src/interfaces/web/src/components/config/project-config-sections.ts +9 -11
- package/src/interfaces/web/src/components/memory/MemoryBrowser.tsx +162 -0
- package/src/interfaces/web/src/i18n/en.ts +10 -0
- package/src/interfaces/web/src/i18n/es.ts +10 -0
- package/src/interfaces/web/src/lib/api/agents.ts +2 -1
- package/src/interfaces/web/src/lib/api/sessions.ts +2 -1
- package/src/interfaces/web/src/screens/ProjectScreen.tsx +50 -60
- package/src/interfaces/web/src/screens/base/SessionsTab.tsx +11 -5
- 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/ConfigTab.tsx +110 -25
- package/src/interfaces/web/src/screens/project/MemoriesTab.tsx +7 -128
- package/src/interfaces/web/src/screens/project/Overview.tsx +93 -3
- package/src/interfaces/web/src/types/daemon.ts +10 -0
- package/src/interfaces/web/dist/assets/index-B3pEwe1m.js +0 -803
- package/src/interfaces/web/dist/assets/index-B3pEwe1m.js.map +0 -1
- package/src/interfaces/web/dist/assets/index-BPGECxzm.css +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,16 +1,17 @@
|
|
|
1
|
-
import { useState } from "react";
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
2
|
import { useNavigate } from "react-router-dom";
|
|
3
3
|
import useSWR from "swr";
|
|
4
4
|
import { RefreshCw, Trash2 } from "lucide-react";
|
|
5
5
|
import { Projects } from "../../lib/api";
|
|
6
6
|
import { Section } from "../../components/Section";
|
|
7
|
-
import { Button, Dialog, Empty, Loading } from "../../components/ui";
|
|
7
|
+
import { Button, Dialog, Empty, Loading, Textarea } from "../../components/ui";
|
|
8
8
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../../components/ui/tabs";
|
|
9
9
|
import { ConfigTabsEditor } from "../../components/config/ConfigTabsEditor";
|
|
10
|
-
import { apcProjectSections,
|
|
10
|
+
import { apcProjectSections, projectSettingsSections, projectEnginesSections } from "../../components/config/project-config-sections";
|
|
11
|
+
import { TelegramTab } from "./TelegramTab";
|
|
11
12
|
import { useToast } from "../../components/Toast";
|
|
12
13
|
import { useProject } from "../../hooks/useProjects";
|
|
13
|
-
import { flattenObject } from "../../lib/config-values";
|
|
14
|
+
import { flattenObject, parseConfigJson } from "../../lib/config-values";
|
|
14
15
|
import { isSecretMarker } from "../../lib/secrets";
|
|
15
16
|
import { t } from "../../i18n";
|
|
16
17
|
|
|
@@ -36,54 +37,91 @@ export function ConfigTab({ pid }: { pid: string }) {
|
|
|
36
37
|
cfg.mutate();
|
|
37
38
|
};
|
|
38
39
|
|
|
40
|
+
const saveOverrideFields = async (set: Record<string, unknown>, unset: string[]) => {
|
|
41
|
+
await Projects.config.set(pid, set);
|
|
42
|
+
if (unset.length) await Projects.config.unset(pid, unset);
|
|
43
|
+
toast.success(t("project.config.save_fields_success"));
|
|
44
|
+
cfg.mutate();
|
|
45
|
+
};
|
|
46
|
+
|
|
39
47
|
return (
|
|
40
48
|
<div className="space-y-6">
|
|
41
49
|
<Section title={t("project.config.section_title")} description={t("project.config.section_desc")}>
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
<TabsTrigger value="
|
|
46
|
-
<TabsTrigger value="
|
|
50
|
+
{/* Organized by concept: Settings · Engines · Telegram · Project · JSON. */}
|
|
51
|
+
<Tabs defaultValue="settings" className="space-y-4">
|
|
52
|
+
<TabsList className="flex flex-wrap">
|
|
53
|
+
<TabsTrigger value="settings">{t("project.config.tab_settings")}</TabsTrigger>
|
|
54
|
+
<TabsTrigger value="engines">{t("settings_ui.cfg_engines_label")}</TabsTrigger>
|
|
55
|
+
{!isBase && <TabsTrigger value="telegram">{t("project.nav.telegram")}</TabsTrigger>}
|
|
56
|
+
<TabsTrigger value="project">{t("project.config.tab_project")}</TabsTrigger>
|
|
57
|
+
<TabsTrigger value="json">JSON</TabsTrigger>
|
|
47
58
|
</TabsList>
|
|
48
59
|
|
|
49
|
-
<TabsContent value="
|
|
60
|
+
<TabsContent value="settings">
|
|
50
61
|
<ConfigTabsEditor
|
|
51
|
-
sections={
|
|
62
|
+
sections={projectSettingsSections()}
|
|
52
63
|
source={cfg.data.project_only}
|
|
53
64
|
placeholderSource={cfg.data.effective}
|
|
54
65
|
jsonTitle={cfg.data.project_config_path}
|
|
55
|
-
|
|
56
|
-
onSaveFields={async (set, unset) => {
|
|
57
|
-
await Projects.config.set(pid, set);
|
|
58
|
-
if (unset.length) await Projects.config.unset(pid, unset);
|
|
59
|
-
toast.success(t("project.config.save_fields_success"));
|
|
60
|
-
cfg.mutate();
|
|
61
|
-
}}
|
|
66
|
+
onSaveFields={saveOverrideFields}
|
|
62
67
|
onSaveJson={saveOverrideJson}
|
|
68
|
+
hideJson
|
|
63
69
|
/>
|
|
64
70
|
</TabsContent>
|
|
65
71
|
|
|
72
|
+
<TabsContent value="engines">
|
|
73
|
+
<ConfigTabsEditor
|
|
74
|
+
sections={projectEnginesSections()}
|
|
75
|
+
source={cfg.data.project_only}
|
|
76
|
+
placeholderSource={cfg.data.effective}
|
|
77
|
+
jsonTitle={cfg.data.project_config_path}
|
|
78
|
+
onSaveFields={saveOverrideFields}
|
|
79
|
+
onSaveJson={saveOverrideJson}
|
|
80
|
+
hideJson
|
|
81
|
+
/>
|
|
82
|
+
</TabsContent>
|
|
83
|
+
|
|
84
|
+
{!isBase && (
|
|
85
|
+
<TabsContent value="telegram">
|
|
86
|
+
<TelegramTab pid={pid} />
|
|
87
|
+
</TabsContent>
|
|
88
|
+
)}
|
|
89
|
+
|
|
66
90
|
<TabsContent value="project">
|
|
67
91
|
<ConfigTabsEditor
|
|
68
92
|
sections={apcProjectSections()}
|
|
69
93
|
source={cfg.data.apc_project || {}}
|
|
70
94
|
jsonTitle={cfg.data.project_json_path}
|
|
71
|
-
jsonDescription=".apc/project.json. Metadata APC portable."
|
|
72
95
|
onSaveFields={async (set, unset) => {
|
|
73
96
|
await Projects.apcProject.set(pid, cleanSet(set), unset);
|
|
74
97
|
toast.success(t("project.config.save_meta_success"));
|
|
75
98
|
cfg.mutate();
|
|
76
99
|
}}
|
|
77
100
|
onSaveJson={saveProjectJson}
|
|
101
|
+
hideJson
|
|
78
102
|
/>
|
|
79
103
|
</TabsContent>
|
|
80
104
|
|
|
81
|
-
<TabsContent value="
|
|
82
|
-
<div className="space-y-
|
|
83
|
-
<
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
105
|
+
<TabsContent value="json">
|
|
106
|
+
<div className="space-y-6">
|
|
107
|
+
<JsonEditor
|
|
108
|
+
title={cfg.data.project_config_path}
|
|
109
|
+
description=".apc/config.json — overrides del proyecto."
|
|
110
|
+
source={cfg.data.project_only}
|
|
111
|
+
onSave={saveOverrideJson}
|
|
112
|
+
/>
|
|
113
|
+
<JsonEditor
|
|
114
|
+
title={cfg.data.project_json_path}
|
|
115
|
+
description=".apc/project.json — metadata APC portable."
|
|
116
|
+
source={cfg.data.apc_project || {}}
|
|
117
|
+
onSave={saveProjectJson}
|
|
118
|
+
/>
|
|
119
|
+
<div className="space-y-2">
|
|
120
|
+
<p className="text-xs text-muted-fg">{t("project.config.effective_read")}</p>
|
|
121
|
+
<pre className="max-h-96 overflow-auto rounded-lg border border-border bg-muted/40 p-3 text-xs">
|
|
122
|
+
{JSON.stringify(cfg.data.effective, null, 2)}
|
|
123
|
+
</pre>
|
|
124
|
+
</div>
|
|
87
125
|
</div>
|
|
88
126
|
</TabsContent>
|
|
89
127
|
</Tabs>
|
|
@@ -216,6 +254,53 @@ function DangerZone({
|
|
|
216
254
|
);
|
|
217
255
|
}
|
|
218
256
|
|
|
257
|
+
// Raw JSON editor for one config file (redacted secrets echo back untouched —
|
|
258
|
+
// the daemon restores real values on save).
|
|
259
|
+
function JsonEditor({
|
|
260
|
+
title,
|
|
261
|
+
description,
|
|
262
|
+
source,
|
|
263
|
+
onSave,
|
|
264
|
+
}: {
|
|
265
|
+
title: string;
|
|
266
|
+
description?: string;
|
|
267
|
+
source: Record<string, unknown>;
|
|
268
|
+
onSave: (next: Record<string, unknown>) => Promise<void>;
|
|
269
|
+
}) {
|
|
270
|
+
const [raw, setRaw] = useState("");
|
|
271
|
+
const [error, setError] = useState("");
|
|
272
|
+
const [busy, setBusy] = useState(false);
|
|
273
|
+
|
|
274
|
+
useEffect(() => {
|
|
275
|
+
setRaw(JSON.stringify(source || {}, null, 2));
|
|
276
|
+
setError("");
|
|
277
|
+
}, [source]);
|
|
278
|
+
|
|
279
|
+
const save = async () => {
|
|
280
|
+
setError("");
|
|
281
|
+
setBusy(true);
|
|
282
|
+
try {
|
|
283
|
+
await onSave(parseConfigJson(raw));
|
|
284
|
+
} catch (e) {
|
|
285
|
+
setError((e as Error).message);
|
|
286
|
+
} finally {
|
|
287
|
+
setBusy(false);
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
return (
|
|
292
|
+
<div className="space-y-2">
|
|
293
|
+
<div>
|
|
294
|
+
<h3 className="text-sm font-medium">{title}</h3>
|
|
295
|
+
{description && <p className="text-xs text-muted-fg">{description}</p>}
|
|
296
|
+
</div>
|
|
297
|
+
<Textarea rows={14} className="font-mono text-xs" value={raw} onChange={(e) => setRaw(e.target.value)} />
|
|
298
|
+
{error && <p className="text-xs text-destructive">{error}</p>}
|
|
299
|
+
<Button variant="primary" loading={busy} onClick={save}>{t("settings_ui.save_json")}</Button>
|
|
300
|
+
</div>
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
|
|
219
304
|
function cleanSet(set: Record<string, unknown>) {
|
|
220
305
|
const out: Record<string, unknown> = {};
|
|
221
306
|
for (const [key, value] of Object.entries(flattenObject(set))) {
|
|
@@ -1,134 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import useSWR from "swr";
|
|
3
|
-
import { Bot, Brain, ChevronDown, ChevronRight, Crown, Save } from "lucide-react";
|
|
4
|
-
import { Agents, Projects } from "../../lib/api";
|
|
5
|
-
import type { AgentEntry } from "../../types/daemon";
|
|
6
|
-
import { Section } from "../../components/Section";
|
|
7
|
-
import { Button, Empty, Loading, Textarea } from "../../components/ui";
|
|
8
|
-
import { useToast } from "../../components/Toast";
|
|
9
|
-
import { t } from "../../i18n";
|
|
10
|
-
|
|
11
|
-
// Editable markdown memory block with dirty-tracking + save.
|
|
12
|
-
function MemoryEditor({
|
|
13
|
-
load,
|
|
14
|
-
save,
|
|
15
|
-
rows = 10,
|
|
16
|
-
placeholder,
|
|
17
|
-
}: {
|
|
18
|
-
load: () => Promise<string>;
|
|
19
|
-
save: (body: string) => Promise<void>;
|
|
20
|
-
rows?: number;
|
|
21
|
-
placeholder?: string;
|
|
22
|
-
}) {
|
|
23
|
-
const toast = useToast();
|
|
24
|
-
const [original, setOriginal] = useState<string | null>(null);
|
|
25
|
-
const [value, setValue] = useState("");
|
|
26
|
-
const [busy, setBusy] = useState(false);
|
|
27
|
-
|
|
28
|
-
useEffect(() => {
|
|
29
|
-
let live = true;
|
|
30
|
-
load().then((b) => { if (live) { setOriginal(b); setValue(b); } }).catch(() => { if (live) { setOriginal(""); setValue(""); } });
|
|
31
|
-
return () => { live = false; };
|
|
32
|
-
}, [load]);
|
|
33
|
-
|
|
34
|
-
if (original === null) return <Loading />;
|
|
35
|
-
const dirty = value !== original;
|
|
36
|
-
|
|
37
|
-
const onSave = async () => {
|
|
38
|
-
setBusy(true);
|
|
39
|
-
try {
|
|
40
|
-
await save(value);
|
|
41
|
-
setOriginal(value);
|
|
42
|
-
toast.success(t("project.memories.saved"));
|
|
43
|
-
} catch (e) { toast.error((e as Error).message); }
|
|
44
|
-
finally { setBusy(false); }
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
return (
|
|
48
|
-
<div className="space-y-2">
|
|
49
|
-
<Textarea
|
|
50
|
-
rows={rows}
|
|
51
|
-
className="font-mono text-xs"
|
|
52
|
-
value={value}
|
|
53
|
-
onChange={(e) => setValue(e.target.value)}
|
|
54
|
-
placeholder={placeholder}
|
|
55
|
-
/>
|
|
56
|
-
<div className="flex items-center justify-between">
|
|
57
|
-
<span className="text-[11px] text-muted-fg">{value.length} {t("project.memories.chars")}</span>
|
|
58
|
-
<Button size="sm" variant="primary" loading={busy} disabled={!dirty} onClick={onSave}>
|
|
59
|
-
<Save size={12} /> {t("project.memories.save_btn")}
|
|
60
|
-
</Button>
|
|
61
|
-
</div>
|
|
62
|
-
</div>
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function AgentMemoryRow({ pid, agent }: { pid: string; agent: AgentEntry }) {
|
|
67
|
-
const [open, setOpen] = useState(false);
|
|
68
|
-
const Icon = agent.is_master ? Crown : Bot;
|
|
69
|
-
return (
|
|
70
|
-
<li className="rounded-xl border border-border bg-muted/30">
|
|
71
|
-
<button
|
|
72
|
-
type="button"
|
|
73
|
-
onClick={() => setOpen((v) => !v)}
|
|
74
|
-
className="flex w-full items-center gap-3 px-3 py-2 text-left"
|
|
75
|
-
>
|
|
76
|
-
{open ? <ChevronDown size={14} className="text-muted-fg" /> : <ChevronRight size={14} className="text-muted-fg" />}
|
|
77
|
-
<Icon size={14} className={agent.is_master ? "text-violet-400" : "text-muted-fg"} />
|
|
78
|
-
<span className="text-sm font-medium">{agent.slug}</span>
|
|
79
|
-
{agent.role && <span className="text-xs text-muted-fg">· {agent.role}</span>}
|
|
80
|
-
</button>
|
|
81
|
-
{open && (
|
|
82
|
-
<div className="border-t border-border p-3">
|
|
83
|
-
<MemoryEditor
|
|
84
|
-
rows={8}
|
|
85
|
-
load={() => Agents.memory.get(pid, agent.slug).then((r) => r.body)}
|
|
86
|
-
save={(body) => Agents.memory.put(pid, agent.slug, body).then(() => {})}
|
|
87
|
-
/>
|
|
88
|
-
</div>
|
|
89
|
-
)}
|
|
90
|
-
</li>
|
|
91
|
-
);
|
|
92
|
-
}
|
|
1
|
+
import { MemoryBrowser } from "../../components/memory/MemoryBrowser";
|
|
93
2
|
|
|
3
|
+
// Durable memory surface. Same docs-style two-pane browser as /docs: a sidebar
|
|
4
|
+
// listing the project ("General") memory plus every agent's memory, and a shared
|
|
5
|
+
// markdown editor (edit / split-preview / save) on the right. Project memory is
|
|
6
|
+
// .apc/memory.md; agent memory is ~/.apx/projects/<id>/agents/<slug>/memory.md.
|
|
94
7
|
export function MemoriesTab({ pid }: { pid: string }) {
|
|
95
|
-
const agents = useSWR(`/projects/${pid}/agents`, () => Agents.list(pid));
|
|
96
|
-
|
|
97
8
|
return (
|
|
98
|
-
<div className="
|
|
99
|
-
<
|
|
100
|
-
title={t("project.memories.project_title")}
|
|
101
|
-
description={t("project.memories.project_desc")}
|
|
102
|
-
>
|
|
103
|
-
<div className="flex items-start gap-3">
|
|
104
|
-
<div className="mt-1 flex size-9 shrink-0 items-center justify-center rounded-xl bg-gradient-to-br from-sky-600 to-indigo-600">
|
|
105
|
-
<Brain className="size-4 text-white" />
|
|
106
|
-
</div>
|
|
107
|
-
<div className="min-w-0 flex-1">
|
|
108
|
-
<MemoryEditor
|
|
109
|
-
rows={12}
|
|
110
|
-
load={() => Projects.memory.get(pid).then((r) => r.body)}
|
|
111
|
-
save={(body) => Projects.memory.put(pid, body).then(() => {})}
|
|
112
|
-
placeholder={t("project.memories.project_ph")}
|
|
113
|
-
/>
|
|
114
|
-
</div>
|
|
115
|
-
</div>
|
|
116
|
-
</Section>
|
|
117
|
-
|
|
118
|
-
<Section
|
|
119
|
-
title={t("project.memories.agents_title")}
|
|
120
|
-
description={t("project.memories.agents_desc")}
|
|
121
|
-
>
|
|
122
|
-
{agents.isLoading && <Loading />}
|
|
123
|
-
{!agents.isLoading && (agents.data?.length ?? 0) === 0 && (
|
|
124
|
-
<Empty>{t("project.memories.no_agents")}</Empty>
|
|
125
|
-
)}
|
|
126
|
-
<ul className="space-y-2">
|
|
127
|
-
{(agents.data || []).map((a) => (
|
|
128
|
-
<AgentMemoryRow key={a.slug} pid={pid} agent={a} />
|
|
129
|
-
))}
|
|
130
|
-
</ul>
|
|
131
|
-
</Section>
|
|
9
|
+
<div className="h-full">
|
|
10
|
+
<MemoryBrowser pid={pid} />
|
|
132
11
|
</div>
|
|
133
12
|
);
|
|
134
13
|
}
|
|
@@ -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
|
|
|
@@ -35,8 +41,9 @@ export function Overview({ pid }: { pid: string }) {
|
|
|
35
41
|
<Card title={t("project.overview.artifacts")} value={artifacts.data?.length ?? "…"} href={`/p/${pid}/artifacts`} icon={FileCode2} />
|
|
36
42
|
</div>
|
|
37
43
|
|
|
38
|
-
{/* Task workflow strip
|
|
39
|
-
|
|
44
|
+
{/* Task workflow strip — always shown once the summary loads, so every
|
|
45
|
+
status reads at least 0 (parity with the base dashboard). */}
|
|
46
|
+
{summary.data && (
|
|
40
47
|
<div className="flex flex-wrap gap-2">
|
|
41
48
|
{TASK_STATUS_ORDER.map((s) => (
|
|
42
49
|
<NavLink
|
|
@@ -52,11 +59,32 @@ export function Overview({ pid }: { pid: string }) {
|
|
|
52
59
|
</div>
|
|
53
60
|
)}
|
|
54
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
|
+
|
|
55
70
|
<div className="grid gap-4 lg:grid-cols-2">
|
|
56
71
|
{/* Agent roster */}
|
|
57
72
|
<Section title={t("project.overview.roster")} className="!p-4">
|
|
58
73
|
{agentList.length === 0 ? (
|
|
59
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>
|
|
60
88
|
) : (
|
|
61
89
|
<div className="space-y-3">
|
|
62
90
|
{orchestrators.length > 0 && (
|
|
@@ -105,6 +133,67 @@ export function Overview({ pid }: { pid: string }) {
|
|
|
105
133
|
);
|
|
106
134
|
}
|
|
107
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
|
+
|
|
108
197
|
function RosterRow({
|
|
109
198
|
label, icon: Icon, agents, pid, navigate,
|
|
110
199
|
}: {
|
|
@@ -127,6 +216,7 @@ function RosterRow({
|
|
|
127
216
|
>
|
|
128
217
|
<span className="text-sm leading-none">{a.emoji || "🤖"}</span>
|
|
129
218
|
<span className="truncate">{a.slug}</span>
|
|
219
|
+
{a.role && <span className="truncate text-[10px] text-muted-fg">· {a.role}</span>}
|
|
130
220
|
</button>
|
|
131
221
|
))}
|
|
132
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 {
|