@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.
Files changed (29) hide show
  1. package/package.json +1 -1
  2. package/src/core/config/redact.js +44 -0
  3. package/src/host/daemon/api/agents.js +37 -1
  4. package/src/host/daemon/api/config.js +17 -5
  5. package/src/host/daemon/api/sessions.js +9 -0
  6. package/src/interfaces/web/dist/assets/index-_2zKBH4O.js +803 -0
  7. package/src/interfaces/web/dist/assets/index-_2zKBH4O.js.map +1 -0
  8. package/src/interfaces/web/dist/assets/index-xQYf6_ab.css +1 -0
  9. package/src/interfaces/web/dist/index.html +2 -2
  10. package/src/interfaces/web/package-lock.json +3 -3
  11. package/src/interfaces/web/src/components/config/ConfigTabsEditor.tsx +46 -31
  12. package/src/interfaces/web/src/components/config/project-config-sections.ts +9 -11
  13. package/src/interfaces/web/src/components/memory/MemoryBrowser.tsx +162 -0
  14. package/src/interfaces/web/src/i18n/en.ts +10 -0
  15. package/src/interfaces/web/src/i18n/es.ts +10 -0
  16. package/src/interfaces/web/src/lib/api/agents.ts +2 -1
  17. package/src/interfaces/web/src/lib/api/sessions.ts +2 -1
  18. package/src/interfaces/web/src/screens/ProjectScreen.tsx +50 -60
  19. package/src/interfaces/web/src/screens/base/SessionsTab.tsx +11 -5
  20. package/src/interfaces/web/src/screens/project/AgentBrainGraph.tsx +169 -47
  21. package/src/interfaces/web/src/screens/project/AgentDetailScreen.tsx +108 -34
  22. package/src/interfaces/web/src/screens/project/AgentsTab.tsx +72 -16
  23. package/src/interfaces/web/src/screens/project/ConfigTab.tsx +110 -25
  24. package/src/interfaces/web/src/screens/project/MemoriesTab.tsx +7 -128
  25. package/src/interfaces/web/src/screens/project/Overview.tsx +93 -3
  26. package/src/interfaces/web/src/types/daemon.ts +10 -0
  27. package/src/interfaces/web/dist/assets/index-B3pEwe1m.js +0 -803
  28. package/src/interfaces/web/dist/assets/index-B3pEwe1m.js.map +0 -1
  29. 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
- export function projectOverrideSections(): ConfigSection[] {
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
- key: "telegram",
27
- label: t("settings_ui.cfg_telegram_label"),
28
- fields: [
29
- { path: "telegram.route_to_agent", label: t("settings_ui.cfg_route_to_agent") },
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) => http.get<AgentEntry[]>(`/projects/${pid}/agents`),
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, FolderKanban, Settings,
5
- MessagesSquare, Send, KeyRound,
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
- if (isBase) {
55
- // Base = menú global / admin del daemon (distinto al de un proyecto).
56
- return [
57
- {
58
- title: t("base.nav_general"),
59
- items: [
60
- { key: "", label: "Dashboard", icon: LayoutDashboard },
61
- { key: "workspaces", label: t("base.workspaces_title"), icon: Boxes },
62
- { key: "models", label: t("settings.tabs.engines"), icon: Cpu },
63
- { key: "agent-defaults", label: t("base.defaults_title"), icon: Bot },
64
- ],
65
- },
66
- {
67
- title: t("base.nav_activity"),
68
- items: [
69
- { key: "chat", label: t("project.nav.chat"), icon: MessagesSquare },
70
- { key: "sessions", label: t("base.sessions_title"), icon: History },
71
- { key: "tasks", label: t("project.nav.tasks"), icon: Zap },
72
- { key: "logs", label: t("project.nav.logs"), icon: ScrollText },
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: "", label: t("project.nav.overview"), icon: FolderKanban },
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: "memories", label: t("project.nav.memories"), icon: Brain },
104
- { key: "skills", label: t("skills_page.title"), icon: Sparkles },
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", label: t("project.nav.routines"), icon: Heart },
118
- { key: "tasks", label: t("project.nav.tasks"), icon: Zap },
119
- { key: "mcps", label: t("project.nav.mcps"), icon: Puzzle },
120
- { key: "integrations", label: "Integrations", icon: Cable },
121
- { key: "vars", label: t("project.nav.vars"), icon: KeyRound },
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", label: t("project.nav.config"), icon: Settings },
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
- export function SessionsTab() {
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>