@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
|
@@ -4,7 +4,10 @@ import { t } from "../../i18n";
|
|
|
4
4
|
|
|
5
5
|
// These are functions (not module-level consts) so t() runs per-render with the
|
|
6
6
|
// active locale — a frozen const would lock the strings to the locale at import.
|
|
7
|
-
|
|
7
|
+
// Config is organized by concept: Settings (routing / super-agent behaviour) and
|
|
8
|
+
// Engines (provider keys) are separate top-level tabs. Telegram is edited on its
|
|
9
|
+
// own channel page (the canonical mechanism), not here.
|
|
10
|
+
export function projectSettingsSections(): ConfigSection[] {
|
|
8
11
|
return [
|
|
9
12
|
{
|
|
10
13
|
key: "routing",
|
|
@@ -22,16 +25,11 @@ export function projectOverrideSections(): ConfigSection[] {
|
|
|
22
25
|
{ path: "super_agent.system", label: t("settings_ui.cfg_extra_prompt"), kind: "textarea" },
|
|
23
26
|
],
|
|
24
27
|
},
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
{ path: "telegram.chat_id", label: t("settings_ui.cfg_chat_id") },
|
|
31
|
-
{ path: "telegram.bot_token", label: t("settings_ui.cfg_bot_token"), kind: "password" },
|
|
32
|
-
{ path: "telegram.respond_with_engine", label: t("settings_ui.cfg_respond_with_engine"), kind: "boolean" },
|
|
33
|
-
],
|
|
34
|
-
},
|
|
28
|
+
];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function projectEnginesSections(): ConfigSection[] {
|
|
32
|
+
return [
|
|
35
33
|
{
|
|
36
34
|
key: "engines",
|
|
37
35
|
label: t("settings_ui.cfg_engines_label"),
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { useMemo, useState } from "react";
|
|
2
|
+
import useSWR from "swr";
|
|
3
|
+
import { Brain, Bot, Crown, RefreshCw } from "lucide-react";
|
|
4
|
+
import { Agents, Projects } from "../../lib/api";
|
|
5
|
+
import type { AgentEntry, FileContent } from "../../types/daemon";
|
|
6
|
+
import { cn } from "../../lib/cn";
|
|
7
|
+
import { Spinner, Empty } from "../ui";
|
|
8
|
+
import { useToast } from "../Toast";
|
|
9
|
+
import { t } from "../../i18n";
|
|
10
|
+
import { FileViewer } from "../files/FileViewer";
|
|
11
|
+
|
|
12
|
+
// Which memory is open in the right pane.
|
|
13
|
+
type Sel = { kind: "project" } | { kind: "agent"; slug: string };
|
|
14
|
+
|
|
15
|
+
function selId(s: Sel): string {
|
|
16
|
+
return s.kind === "project" ? "project" : `agent:${s.slug}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// On-disk-ish path shown in the viewer header (mirrors the real memory.md
|
|
20
|
+
// locations so it reads familiarly, like the docs surface).
|
|
21
|
+
function selPath(s: Sel): string {
|
|
22
|
+
return s.kind === "project" ? ".apc/memory.md" : `agents/${s.slug}/memory.md`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function loadBody(pid: string, s: Sel): Promise<string> {
|
|
26
|
+
return s.kind === "project"
|
|
27
|
+
? Projects.memory.get(pid).then((r) => r.body)
|
|
28
|
+
: Agents.memory.get(pid, s.slug).then((r) => r.body);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function saveBody(pid: string, s: Sel, body: string): Promise<void> {
|
|
32
|
+
return s.kind === "project"
|
|
33
|
+
? Projects.memory.put(pid, body).then(() => {})
|
|
34
|
+
: Agents.memory.put(pid, s.slug, body).then(() => {});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function SidebarItem({
|
|
38
|
+
active, onClick, icon: Icon, iconClass, label, sub,
|
|
39
|
+
}: {
|
|
40
|
+
active: boolean;
|
|
41
|
+
onClick: () => void;
|
|
42
|
+
icon: typeof Bot;
|
|
43
|
+
iconClass?: string;
|
|
44
|
+
label: string;
|
|
45
|
+
sub?: string;
|
|
46
|
+
}) {
|
|
47
|
+
return (
|
|
48
|
+
<button
|
|
49
|
+
type="button"
|
|
50
|
+
onClick={onClick}
|
|
51
|
+
className={cn(
|
|
52
|
+
"flex w-full items-center gap-2 rounded px-1.5 py-1 text-left text-[13px]",
|
|
53
|
+
active ? "bg-primary/15 text-foreground" : "text-foreground/80 hover:bg-accent/40",
|
|
54
|
+
)}
|
|
55
|
+
>
|
|
56
|
+
<Icon className={cn("size-3.5 shrink-0", iconClass ?? "text-muted-foreground")} />
|
|
57
|
+
<span className="min-w-0 flex-1 truncate">{label}</span>
|
|
58
|
+
{sub && <span className="shrink-0 truncate text-[11px] text-muted-foreground">{sub}</span>}
|
|
59
|
+
</button>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Docs-style two-pane browser for durable memory: a sidebar listing the project
|
|
64
|
+
// ("General") memory plus every agent's memory, and the shared FileViewer on the
|
|
65
|
+
// right for markdown edit / split-preview / save — the same surface as /docs.
|
|
66
|
+
export function MemoryBrowser({ pid }: { pid: string }) {
|
|
67
|
+
const toast = useToast();
|
|
68
|
+
const [sel, setSel] = useState<Sel>({ kind: "project" });
|
|
69
|
+
|
|
70
|
+
const agents = useSWR(`/projects/${pid}/agents`, () => Agents.list(pid));
|
|
71
|
+
|
|
72
|
+
const bodyKey = `/memory/${pid}/${selId(sel)}`;
|
|
73
|
+
const body = useSWR(bodyKey, () => loadBody(pid, sel));
|
|
74
|
+
|
|
75
|
+
// Adapt the raw memory body into the FileContent shape FileViewer expects.
|
|
76
|
+
const file = useMemo<FileContent | null>(() => {
|
|
77
|
+
if (body.data === undefined) return null;
|
|
78
|
+
const content = body.data ?? "";
|
|
79
|
+
return {
|
|
80
|
+
path: selPath(sel),
|
|
81
|
+
name: "memory.md",
|
|
82
|
+
kind: "markdown",
|
|
83
|
+
size: content.length,
|
|
84
|
+
modified: "",
|
|
85
|
+
encoding: "utf8",
|
|
86
|
+
content,
|
|
87
|
+
};
|
|
88
|
+
}, [body.data, sel]);
|
|
89
|
+
|
|
90
|
+
const onSave = async (content: string) => {
|
|
91
|
+
await saveBody(pid, sel, content);
|
|
92
|
+
toast.success(t("project.memories.saved"));
|
|
93
|
+
void body.mutate(content, { revalidate: false });
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const list = (agents.data || []) as AgentEntry[];
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<div className="flex h-full min-h-0 overflow-hidden rounded-xl border border-border bg-card">
|
|
100
|
+
{/* Sidebar */}
|
|
101
|
+
<div className="flex w-64 shrink-0 flex-col border-r border-border">
|
|
102
|
+
<div className="flex shrink-0 items-center gap-2 border-b border-border px-3 py-2">
|
|
103
|
+
<Brain className="size-4 text-muted-foreground" />
|
|
104
|
+
<span className="flex-1 text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
|
105
|
+
{t("project.memories.sidebar_title")}
|
|
106
|
+
</span>
|
|
107
|
+
<button
|
|
108
|
+
type="button"
|
|
109
|
+
onClick={() => { void agents.mutate(); void body.mutate(); }}
|
|
110
|
+
className="text-muted-foreground hover:text-foreground"
|
|
111
|
+
aria-label={t("common.refresh")}
|
|
112
|
+
>
|
|
113
|
+
<RefreshCw className={agents.isValidating ? "size-3.5 animate-spin" : "size-3.5"} />
|
|
114
|
+
</button>
|
|
115
|
+
</div>
|
|
116
|
+
|
|
117
|
+
<div className="min-h-0 flex-1 overflow-y-auto p-1.5">
|
|
118
|
+
{/* General / project memory */}
|
|
119
|
+
<p className="px-1.5 pb-1 pt-1 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground/60">
|
|
120
|
+
{t("project.memories.general_group")}
|
|
121
|
+
</p>
|
|
122
|
+
<SidebarItem
|
|
123
|
+
active={sel.kind === "project"}
|
|
124
|
+
onClick={() => setSel({ kind: "project" })}
|
|
125
|
+
icon={Brain}
|
|
126
|
+
iconClass="text-sky-500"
|
|
127
|
+
label={t("project.memories.general_item")}
|
|
128
|
+
/>
|
|
129
|
+
|
|
130
|
+
{/* Agent memories */}
|
|
131
|
+
<p className="px-1.5 pb-1 pt-3 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground/60">
|
|
132
|
+
{t("project.memories.agents_title")}
|
|
133
|
+
</p>
|
|
134
|
+
{agents.isLoading ? (
|
|
135
|
+
<div className="flex justify-center py-4"><Spinner size={14} /></div>
|
|
136
|
+
) : list.length === 0 ? (
|
|
137
|
+
<div className="px-1.5 py-2">
|
|
138
|
+
<Empty>{t("project.memories.no_agents")}</Empty>
|
|
139
|
+
</div>
|
|
140
|
+
) : (
|
|
141
|
+
list.map((a) => (
|
|
142
|
+
<SidebarItem
|
|
143
|
+
key={a.slug}
|
|
144
|
+
active={sel.kind === "agent" && sel.slug === a.slug}
|
|
145
|
+
onClick={() => setSel({ kind: "agent", slug: a.slug })}
|
|
146
|
+
icon={a.is_master ? Crown : Bot}
|
|
147
|
+
iconClass={a.is_master ? "text-violet-400" : "text-muted-foreground"}
|
|
148
|
+
label={a.slug}
|
|
149
|
+
sub={a.role || undefined}
|
|
150
|
+
/>
|
|
151
|
+
))
|
|
152
|
+
)}
|
|
153
|
+
</div>
|
|
154
|
+
</div>
|
|
155
|
+
|
|
156
|
+
{/* Viewer */}
|
|
157
|
+
<div className="flex min-w-0 flex-1 flex-col">
|
|
158
|
+
<FileViewer file={file} loading={body.isLoading} onSave={onSave} />
|
|
159
|
+
</div>
|
|
160
|
+
</div>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
@@ -324,6 +324,9 @@ export const en = {
|
|
|
324
324
|
specialists: "Specialists",
|
|
325
325
|
recent_tasks: "Recent tasks",
|
|
326
326
|
no_activity: "No open tasks.",
|
|
327
|
+
brain_title: "Team brain",
|
|
328
|
+
brain_desc: "The whole agent map — orchestrators at the core, their specialists clustered around them. Click a node to open it.",
|
|
329
|
+
brain_core: "Team",
|
|
327
330
|
},
|
|
328
331
|
|
|
329
332
|
artifacts: {
|
|
@@ -711,6 +714,8 @@ export const en = {
|
|
|
711
714
|
save_fields_success: "Overrides saved.",
|
|
712
715
|
save_meta_success: "Project metadata saved.",
|
|
713
716
|
no_data: "No data.",
|
|
717
|
+
tab_settings: "Settings",
|
|
718
|
+
tab_project: "Project",
|
|
714
719
|
},
|
|
715
720
|
|
|
716
721
|
telegram: {
|
|
@@ -731,6 +736,9 @@ export const en = {
|
|
|
731
736
|
},
|
|
732
737
|
|
|
733
738
|
memories: {
|
|
739
|
+
sidebar_title: "Memories",
|
|
740
|
+
general_group: "General",
|
|
741
|
+
general_item: "Project memory",
|
|
734
742
|
project_title: "Project memory",
|
|
735
743
|
project_desc: "Durable facts at the project level. .apc/memory.md — read by agents and the super-agent.",
|
|
736
744
|
project_ph: "# Project Memory\n\nStable facts that any agent should know…",
|
|
@@ -756,6 +764,7 @@ export const en = {
|
|
|
756
764
|
workspaces_empty: "No projects. Add one with the button above.",
|
|
757
765
|
sessions_title: "Sessions",
|
|
758
766
|
sessions_desc: "Sessions from all engines (apx · claude · codex), newest first.",
|
|
767
|
+
sessions_desc_scoped: "Sessions in this project's folder ({path}), all engines, newest first.",
|
|
759
768
|
sessions_all: "All engines",
|
|
760
769
|
sessions_empty: "No sessions.",
|
|
761
770
|
sessions_error: "Could not read sessions: {msg}",
|
|
@@ -1477,6 +1486,7 @@ export const en = {
|
|
|
1477
1486
|
stat_records: "Records",
|
|
1478
1487
|
stat_tasks: "Tasks",
|
|
1479
1488
|
stat_heartbeats: "Heartbeats",
|
|
1489
|
+
uncategorized: "Uncategorized",
|
|
1480
1490
|
config_def_desc: "definition (frontmatter + system prompt).",
|
|
1481
1491
|
memory_durable_desc: "durable facts the agent remembers.",
|
|
1482
1492
|
running: "running",
|
|
@@ -325,6 +325,9 @@ export const es = {
|
|
|
325
325
|
specialists: "Especialistas",
|
|
326
326
|
recent_tasks: "Tasks recientes",
|
|
327
327
|
no_activity: "No hay tasks abiertas.",
|
|
328
|
+
brain_title: "Cerebro del equipo",
|
|
329
|
+
brain_desc: "El mapa completo de agentes — los orquestadores en el núcleo y sus especialistas en racimo alrededor. Clic en un nodo para abrirlo.",
|
|
330
|
+
brain_core: "Equipo",
|
|
328
331
|
},
|
|
329
332
|
|
|
330
333
|
artifacts: {
|
|
@@ -709,6 +712,8 @@ export const es = {
|
|
|
709
712
|
save_fields_success: "Overrides guardados.",
|
|
710
713
|
save_meta_success: "Project metadata guardado.",
|
|
711
714
|
no_data: "Sin datos.",
|
|
715
|
+
tab_settings: "Settings",
|
|
716
|
+
tab_project: "Project",
|
|
712
717
|
},
|
|
713
718
|
|
|
714
719
|
telegram: {
|
|
@@ -729,6 +734,9 @@ export const es = {
|
|
|
729
734
|
},
|
|
730
735
|
|
|
731
736
|
memories: {
|
|
737
|
+
sidebar_title: "Memorias",
|
|
738
|
+
general_group: "General",
|
|
739
|
+
general_item: "Memoria del proyecto",
|
|
732
740
|
project_title: "Memoria del proyecto",
|
|
733
741
|
project_desc: "Hechos durables a nivel proyecto. .apc/memory.md — la leen los agentes y el super-agente.",
|
|
734
742
|
project_ph: "# Memoria del proyecto\n\nHechos estables que cualquier agente debería saber…",
|
|
@@ -754,6 +762,7 @@ export const es = {
|
|
|
754
762
|
workspaces_empty: "Sin proyectos. Agregá uno con el botón de arriba.",
|
|
755
763
|
sessions_title: "Sessions",
|
|
756
764
|
sessions_desc: "Sesiones de todos los engines (apx · claude · codex), más nuevas primero.",
|
|
765
|
+
sessions_desc_scoped: "Sesiones en la carpeta de este proyecto ({path}), todos los engines, más nuevas primero.",
|
|
757
766
|
sessions_all: "Todos los engines",
|
|
758
767
|
sessions_empty: "Sin sesiones.",
|
|
759
768
|
sessions_error: "No pude leer las sesiones: {msg}",
|
|
@@ -1475,6 +1484,7 @@ export const es = {
|
|
|
1475
1484
|
stat_records: "Records",
|
|
1476
1485
|
stat_tasks: "Tasks",
|
|
1477
1486
|
stat_heartbeats: "Heartbeats",
|
|
1487
|
+
uncategorized: "Sin categoría",
|
|
1478
1488
|
config_def_desc: "definición (frontmatter + system prompt).",
|
|
1479
1489
|
memory_durable_desc: "hechos durables que el agente recuerda.",
|
|
1480
1490
|
running: "running",
|
|
@@ -2,7 +2,8 @@ import { http } from "../http";
|
|
|
2
2
|
import type { AgentDetail, AgentEntry } from "../../types/daemon";
|
|
3
3
|
|
|
4
4
|
export const Agents = {
|
|
5
|
-
list: (pid: string
|
|
5
|
+
list: (pid: string, opts?: { stats?: boolean }) =>
|
|
6
|
+
http.get<AgentEntry[]>(`/projects/${pid}/agents${opts?.stats ? "?stats=1" : ""}`),
|
|
6
7
|
get: (pid: string, slug: string) => http.get<AgentDetail>(`/projects/${pid}/agents/${slug}`),
|
|
7
8
|
create: (pid: string, body: Partial<AgentEntry> & { slug: string }) =>
|
|
8
9
|
http.post<AgentEntry>(`/projects/${pid}/agents`, body),
|
|
@@ -19,11 +19,12 @@ export const Sessions = {
|
|
|
19
19
|
.then((b) => ({ sessions: unwrapPage<SessionRow>(b).items })),
|
|
20
20
|
// Server-paginated page. Optional `q` runs the same search core as
|
|
21
21
|
// `apx session find` (title; + transcript content when `deep`).
|
|
22
|
-
page: ({ engine, q, deep, limit, offset }: { engine?: string; q?: string; deep?: boolean; limit: number; offset: number }) => {
|
|
22
|
+
page: ({ engine, q, deep, cwd, limit, offset }: { engine?: string; q?: string; deep?: boolean; cwd?: string; limit: number; offset: number }) => {
|
|
23
23
|
const params = new URLSearchParams({ limit: String(limit), offset: String(offset) });
|
|
24
24
|
if (engine) params.set("engine", engine);
|
|
25
25
|
if (q?.trim()) params.set("q", q.trim());
|
|
26
26
|
if (deep) params.set("deep", "1");
|
|
27
|
+
if (cwd?.trim()) params.set("cwd", cwd.trim());
|
|
27
28
|
return http.get<unknown>(`/sessions?${params.toString()}`).then((b) => unwrapPage<SessionRow>(b));
|
|
28
29
|
},
|
|
29
30
|
};
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { useMemo } from "react";
|
|
2
2
|
import { useParams, Routes, Route, Navigate, useLocation, useNavigate } from "react-router-dom";
|
|
3
3
|
import {
|
|
4
|
-
Bot, Heart, Zap, Puzzle,
|
|
5
|
-
MessagesSquare,
|
|
4
|
+
Bot, Heart, Zap, Puzzle, Settings,
|
|
5
|
+
MessagesSquare, KeyRound,
|
|
6
6
|
LayoutDashboard, Boxes, Cpu, ScrollText, History, Brain, FileCode2, Cable,
|
|
7
7
|
Building2, FileText, FolderTree, Sparkles,
|
|
8
8
|
} from "lucide-react";
|
|
@@ -40,7 +40,7 @@ import { SkillsTab } from "./project/SkillsTab";
|
|
|
40
40
|
type NavKey =
|
|
41
41
|
| "" | "chat" | "config" | "telegram"
|
|
42
42
|
| "agents" | "routines" | "tasks" | "mcps" | "integrations" | "vars" | "logs" | "memories" | "artifacts"
|
|
43
|
-
| "structure" | "docs" | "files" | "skills";
|
|
43
|
+
| "structure" | "docs" | "files" | "skills" | "sessions";
|
|
44
44
|
|
|
45
45
|
export function ProjectScreen() {
|
|
46
46
|
const navigate = useNavigate();
|
|
@@ -51,85 +51,75 @@ export function ProjectScreen() {
|
|
|
51
51
|
|
|
52
52
|
const isBase = String(pid) === "0";
|
|
53
53
|
const sections: TabSection[] = useMemo(() => {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
],
|
|
74
|
-
},
|
|
75
|
-
{
|
|
76
|
-
title: t("base.nav_system"),
|
|
77
|
-
items: [
|
|
78
|
-
{ key: "agents", label: t("project.nav.agents"), icon: Bot },
|
|
79
|
-
{ key: "memories", label: t("project.nav.memories"), icon: Brain },
|
|
80
|
-
{ key: "skills", label: t("skills_page.title"), icon: Sparkles },
|
|
81
|
-
{ key: "routines", label: t("project.nav.routines"), icon: Heart },
|
|
82
|
-
{ key: "mcps", label: t("project.nav.mcps"), icon: Puzzle },
|
|
83
|
-
{ key: "integrations", label: "Integrations", icon: Cable },
|
|
84
|
-
{ key: "vars", label: t("project.nav.vars"), icon: KeyRound },
|
|
85
|
-
{ key: "artifacts", label: t("project.nav.artifacts"), icon: FileCode2 },
|
|
86
|
-
{ key: "config", label: t("project.nav.config"), icon: Settings },
|
|
87
|
-
],
|
|
88
|
-
},
|
|
89
|
-
];
|
|
90
|
-
}
|
|
91
|
-
// Structure (org roles/areas) is only meaningful for company/enterprise
|
|
92
|
-
// projects — gate it on the project kind.
|
|
93
|
-
const isCompany = project?.kind === "company";
|
|
94
|
-
return [
|
|
54
|
+
// One shared taxonomy for both Base and projects, in the same order, so the
|
|
55
|
+
// two menus mirror each other. Base additionally gets a "General" admin
|
|
56
|
+
// section (workspaces / engines / agent defaults) and drops "Content"
|
|
57
|
+
// (no docs/files surface). "Workspace" and "Automation" are identical on
|
|
58
|
+
// both. Structure (org roles/areas) only makes sense for company projects.
|
|
59
|
+
const isCompany = !isBase && project?.kind === "company";
|
|
60
|
+
|
|
61
|
+
const out: (TabSection | null)[] = [
|
|
62
|
+
// General — Base-only daemon admin.
|
|
63
|
+
isBase ? {
|
|
64
|
+
title: t("base.nav_general"),
|
|
65
|
+
items: [
|
|
66
|
+
{ key: "workspaces", label: t("base.workspaces_title"), icon: Boxes },
|
|
67
|
+
{ key: "models", label: t("settings.tabs.engines"), icon: Cpu },
|
|
68
|
+
{ key: "agent-defaults", label: t("base.defaults_title"), icon: Bot },
|
|
69
|
+
],
|
|
70
|
+
} : null,
|
|
71
|
+
// Workspace — the overview plus the team's own building blocks
|
|
72
|
+
// (agents / memories / skills / artifacts). "Overview" on both sides.
|
|
95
73
|
{
|
|
96
74
|
title: t("project.sections.workspace"),
|
|
97
75
|
items: [
|
|
98
|
-
{ key: "",
|
|
99
|
-
{ key: "telegram", label: t("project.nav.telegram"), icon: Send },
|
|
100
|
-
{ key: "chat", label: t("project.nav.chat"), icon: MessagesSquare },
|
|
101
|
-
{ key: "agents", label: t("project.nav.agents"), icon: Bot },
|
|
76
|
+
{ key: "", label: t("project.nav.overview"), icon: LayoutDashboard },
|
|
102
77
|
...(isCompany ? [{ key: "structure", label: t("project.nav.structure"), icon: Building2 }] : []),
|
|
103
|
-
{ key: "
|
|
104
|
-
{ key: "
|
|
78
|
+
{ key: "agents", label: t("project.nav.agents"), icon: Bot },
|
|
79
|
+
{ key: "memories", label: t("project.nav.memories"), icon: Brain },
|
|
80
|
+
{ key: "skills", label: t("skills_page.title"), icon: Sparkles },
|
|
81
|
+
{ key: "artifacts", label: t("project.nav.artifacts"), icon: FileCode2 },
|
|
105
82
|
],
|
|
106
83
|
},
|
|
84
|
+
// Activity — chat / sessions / logs.
|
|
107
85
|
{
|
|
86
|
+
title: t("base.nav_activity"),
|
|
87
|
+
items: [
|
|
88
|
+
{ key: "chat", label: t("project.nav.chat"), icon: MessagesSquare },
|
|
89
|
+
{ key: "sessions", label: t("base.sessions_title"), icon: History },
|
|
90
|
+
{ key: "logs", label: t("project.nav.logs"), icon: ScrollText },
|
|
91
|
+
],
|
|
92
|
+
},
|
|
93
|
+
// Content — docs / files. Project-only (Base has no such surface).
|
|
94
|
+
!isBase ? {
|
|
108
95
|
title: t("project.sections.content"),
|
|
109
96
|
items: [
|
|
110
97
|
{ key: "docs", label: t("project.nav.docs"), icon: FileText },
|
|
111
98
|
{ key: "files", label: t("project.nav.files"), icon: FolderTree },
|
|
112
99
|
],
|
|
113
|
-
},
|
|
100
|
+
} : null,
|
|
101
|
+
// Automation — routines / tasks / mcps / integrations / vars.
|
|
102
|
+
// Identical on both sides.
|
|
114
103
|
{
|
|
115
104
|
title: t("project.sections.automation"),
|
|
116
105
|
items: [
|
|
117
|
-
{ key: "routines",
|
|
118
|
-
{ key: "tasks",
|
|
119
|
-
{ key: "mcps",
|
|
120
|
-
{ key: "integrations", label: "Integrations",
|
|
121
|
-
{ key: "vars",
|
|
122
|
-
{ key: "artifacts", label: t("project.nav.artifacts"), icon: FileCode2 },
|
|
123
|
-
{ key: "logs", label: t("project.nav.logs"), icon: ScrollText },
|
|
106
|
+
{ key: "routines", label: t("project.nav.routines"), icon: Heart },
|
|
107
|
+
{ key: "tasks", label: t("project.nav.tasks"), icon: Zap },
|
|
108
|
+
{ key: "mcps", label: t("project.nav.mcps"), icon: Puzzle },
|
|
109
|
+
{ key: "integrations", label: "Integrations", icon: Cable },
|
|
110
|
+
{ key: "vars", label: t("project.nav.vars"), icon: KeyRound },
|
|
124
111
|
],
|
|
125
112
|
},
|
|
113
|
+
// Config — the general project/daemon config.
|
|
126
114
|
{
|
|
127
115
|
title: t("project.sections.config"),
|
|
128
116
|
items: [
|
|
129
|
-
{ key: "config",
|
|
117
|
+
{ key: "config", label: t("project.nav.config"), icon: Settings },
|
|
130
118
|
],
|
|
131
119
|
},
|
|
132
120
|
];
|
|
121
|
+
|
|
122
|
+
return out.filter(Boolean) as TabSection[];
|
|
133
123
|
}, [isBase, project?.kind]);
|
|
134
124
|
|
|
135
125
|
// First path segment after /p/:pid — so deep routes like agents/:slug still
|
|
@@ -171,7 +161,7 @@ export function ProjectScreen() {
|
|
|
171
161
|
<Route path="workspaces" element={<WorkspacesTab />} />
|
|
172
162
|
<Route path="models" element={<ModelsTab />} />
|
|
173
163
|
<Route path="agent-defaults" element={<AgentDefaultsTab />} />
|
|
174
|
-
<Route path="sessions" element={<SessionsTab />} />
|
|
164
|
+
<Route path="sessions" element={<SessionsTab pid={pid} />} />
|
|
175
165
|
<Route path="logs" element={<LogsTab pid={pid} />} />
|
|
176
166
|
<Route path="config" element={<ConfigTab pid={pid} />} />
|
|
177
167
|
<Route path="telegram" element={<TelegramTab pid={pid} />} />
|
|
@@ -7,15 +7,21 @@ import { Badge, Button, Empty, Input, Loading, Tip } from "../../components/ui";
|
|
|
7
7
|
import { UiSelect } from "../../components/UiSelect";
|
|
8
8
|
import { useToast } from "../../components/Toast";
|
|
9
9
|
import { usePersonaName } from "../../hooks/usePersonaName";
|
|
10
|
+
import { useProject } from "../../hooks/useProjects";
|
|
10
11
|
import { t } from "../../i18n";
|
|
11
12
|
|
|
12
13
|
const ENGINE_TONE: Record<string, "success" | "info" | "warning" | "muted"> = {
|
|
13
14
|
apx: "success", claude: "info", codex: "warning",
|
|
14
15
|
};
|
|
15
16
|
|
|
16
|
-
|
|
17
|
+
// `pid` present + not base → scope to that project's local folder. Base (or no
|
|
18
|
+
// pid) shows every session across engines and folders.
|
|
19
|
+
export function SessionsTab({ pid }: { pid?: string } = {}) {
|
|
17
20
|
const toast = useToast();
|
|
18
21
|
const persona = usePersonaName();
|
|
22
|
+
const isBase = !pid || String(pid) === "0";
|
|
23
|
+
const { project } = useProject(isBase ? "" : pid);
|
|
24
|
+
const cwd = isBase ? undefined : project?.path || undefined;
|
|
19
25
|
const [engine, setEngine] = useState("");
|
|
20
26
|
const [input, setInput] = useState("");
|
|
21
27
|
const [query, setQuery] = useState("");
|
|
@@ -28,10 +34,10 @@ export function SessionsTab() {
|
|
|
28
34
|
}, [input]);
|
|
29
35
|
|
|
30
36
|
const paged = usePagedQuery<SessionRow>({
|
|
31
|
-
key: `/sessions?engine=${engine}&q=${query}&deep=${deep ? 1 : 0}`,
|
|
37
|
+
key: `/sessions?engine=${engine}&q=${query}&deep=${deep ? 1 : 0}&cwd=${cwd || ""}`,
|
|
32
38
|
fetchPage: (limit, offset) =>
|
|
33
|
-
Sessions.page({ engine: engine || undefined, q: query || undefined, deep, limit, offset }),
|
|
34
|
-
resetKey: `${engine}|${query}|${deep ? 1 : 0}`,
|
|
39
|
+
Sessions.page({ engine: engine || undefined, q: query || undefined, deep, cwd, limit, offset }),
|
|
40
|
+
resetKey: `${engine}|${query}|${deep ? 1 : 0}|${cwd || ""}`,
|
|
35
41
|
});
|
|
36
42
|
|
|
37
43
|
const clear = () => { setInput(""); setQuery(""); setEngine(""); setDeep(false); };
|
|
@@ -72,7 +78,7 @@ export function SessionsTab() {
|
|
|
72
78
|
<Section
|
|
73
79
|
fullHeight
|
|
74
80
|
title={t("base.sessions_title")}
|
|
75
|
-
description={t("base.sessions_desc")}
|
|
81
|
+
description={isBase ? t("base.sessions_desc") : t("base.sessions_desc_scoped", { path: cwd || "…" })}
|
|
76
82
|
action={
|
|
77
83
|
<Tip content={t("base.sessions_refresh")}>
|
|
78
84
|
<Button size="sm" variant="secondary" onClick={() => paged.mutate()}><RefreshCw size={13} /></Button>
|