@agentprojectcontext/apx 1.67.0 → 1.68.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.
@@ -18,8 +18,8 @@
18
18
  <link rel="apple-touch-icon" href="/favicon/dark/apple-touch-icon.png" media="(prefers-color-scheme: dark)" />
19
19
  <link rel="manifest" href="/favicon/white/site.webmanifest" media="(prefers-color-scheme: light)" />
20
20
  <link rel="manifest" href="/favicon/dark/site.webmanifest" media="(prefers-color-scheme: dark)" />
21
- <script type="module" crossorigin src="/assets/index-B3pEwe1m.js"></script>
22
- <link rel="stylesheet" crossorigin href="/assets/index-BPGECxzm.css">
21
+ <script type="module" crossorigin src="/assets/index-vwd6yQVw.js"></script>
22
+ <link rel="stylesheet" crossorigin href="/assets/index-D4BmWoDM.css">
23
23
  </head>
24
24
  <body class="bg-background text-foreground antialiased">
25
25
  <div id="root"></div>
@@ -3195,9 +3195,9 @@
3195
3195
  }
3196
3196
  },
3197
3197
  "node_modules/node-releases": {
3198
- "version": "2.0.50",
3199
- "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.50.tgz",
3200
- "integrity": "sha512-J6l92tKHX6w8Jy5nO1Vuc01NoIiRGi/d6qBKVxh+IQ8Cr3b6HbVNfKiF8ZpFKufTwpwxMmce2W3iQZ861ZRyTg==",
3198
+ "version": "2.0.51",
3199
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz",
3200
+ "integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==",
3201
3201
  "dev": true,
3202
3202
  "license": "MIT",
3203
3203
  "engines": {
@@ -32,6 +32,7 @@ export function ConfigTabsEditor({
32
32
  onSaveFields,
33
33
  onSaveJson,
34
34
  busy,
35
+ hideJson = false,
35
36
  }: {
36
37
  sections: ConfigSection[];
37
38
  source: Record<string, unknown>;
@@ -42,6 +43,8 @@ export function ConfigTabsEditor({
42
43
  onSaveFields: (set: Record<string, unknown>, unset: string[]) => Promise<void>;
43
44
  onSaveJson: (next: Record<string, unknown>) => Promise<void>;
44
45
  busy?: boolean;
46
+ /** Hide the raw-JSON tab (used when JSON editing lives in its own top tab). */
47
+ hideJson?: boolean;
45
48
  }) {
46
49
  const firstKey = sections[0]?.key || "json";
47
50
  const [draft, setDraft] = useState<Record<string, unknown>>({});
@@ -92,51 +95,63 @@ export function ConfigTabsEditor({
92
95
  }
93
96
  };
94
97
 
98
+ const sectionBody = (section: ConfigSection) => (
99
+ <div className="space-y-4">
100
+ {section.description && <p className="text-sm text-muted-fg">{section.description}</p>}
101
+ <div className="grid gap-3 md:grid-cols-2">
102
+ {section.fields.map((field) => (
103
+ <ConfigFieldControl
104
+ key={field.path}
105
+ field={field}
106
+ value={draft[field.path]}
107
+ inherited={getDotted(placeholderSource, field.path)}
108
+ onChange={(value) => setDraft((prev) => ({ ...prev, [field.path]: value }))}
109
+ />
110
+ ))}
111
+ </div>
112
+ <Button variant="primary" loading={busy} onClick={saveFields}>{saveLabel}</Button>
113
+ </div>
114
+ );
115
+
116
+ // Single section with no JSON tab → render the fields flat (no inner tab bar),
117
+ // so the parent's top-level tab is the only tab the user sees.
118
+ if (hideJson && sections.length <= 1) {
119
+ return sections[0] ? sectionBody(sections[0]) : null;
120
+ }
121
+
95
122
  return (
96
123
  <Tabs defaultValue={firstKey} className="space-y-4">
97
124
  <TabsList className="flex flex-wrap">
98
125
  {sections.map((section) => (
99
126
  <TabsTrigger key={section.key} value={section.key}>{section.label}</TabsTrigger>
100
127
  ))}
101
- <TabsTrigger value="json">JSON</TabsTrigger>
128
+ {!hideJson && <TabsTrigger value="json">JSON</TabsTrigger>}
102
129
  </TabsList>
103
130
 
104
131
  {sections.map((section) => (
105
132
  <TabsContent key={section.key} value={section.key}>
106
- <div className="space-y-4">
107
- {section.description && <p className="text-sm text-muted-fg">{section.description}</p>}
108
- <div className="grid gap-3 md:grid-cols-2">
109
- {section.fields.map((field) => (
110
- <ConfigFieldControl
111
- key={field.path}
112
- field={field}
113
- value={draft[field.path]}
114
- inherited={getDotted(placeholderSource, field.path)}
115
- onChange={(value) => setDraft((prev) => ({ ...prev, [field.path]: value }))}
116
- />
117
- ))}
118
- </div>
119
- <Button variant="primary" loading={busy} onClick={saveFields}>{saveLabel}</Button>
120
- </div>
133
+ {sectionBody(section)}
121
134
  </TabsContent>
122
135
  ))}
123
136
 
124
- <TabsContent value="json">
125
- <div className="space-y-3">
126
- <div>
127
- <h3 className="text-sm font-medium">{jsonTitle}</h3>
128
- {jsonDescription && <p className="text-xs text-muted-fg">{jsonDescription}</p>}
137
+ {!hideJson && (
138
+ <TabsContent value="json">
139
+ <div className="space-y-3">
140
+ <div>
141
+ <h3 className="text-sm font-medium">{jsonTitle}</h3>
142
+ {jsonDescription && <p className="text-xs text-muted-fg">{jsonDescription}</p>}
143
+ </div>
144
+ <Textarea
145
+ rows={18}
146
+ className="font-mono text-xs"
147
+ value={raw}
148
+ onChange={(event) => setRaw(event.target.value)}
149
+ />
150
+ {jsonError && <p className="text-xs text-destructive">{jsonError}</p>}
151
+ <Button variant="primary" loading={busy} onClick={saveJson}>{t("settings_ui.save_json")}</Button>
129
152
  </div>
130
- <Textarea
131
- rows={18}
132
- className="font-mono text-xs"
133
- value={raw}
134
- onChange={(event) => setRaw(event.target.value)}
135
- />
136
- {jsonError && <p className="text-xs text-destructive">{jsonError}</p>}
137
- <Button variant="primary" loading={busy} onClick={saveJson}>{t("settings_ui.save_json")}</Button>
138
- </div>
139
- </TabsContent>
153
+ </TabsContent>
154
+ )}
140
155
  </Tabs>
141
156
  );
142
157
  }
@@ -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
+ }
@@ -711,6 +711,8 @@ export const en = {
711
711
  save_fields_success: "Overrides saved.",
712
712
  save_meta_success: "Project metadata saved.",
713
713
  no_data: "No data.",
714
+ tab_settings: "Settings",
715
+ tab_project: "Project",
714
716
  },
715
717
 
716
718
  telegram: {
@@ -731,6 +733,9 @@ export const en = {
731
733
  },
732
734
 
733
735
  memories: {
736
+ sidebar_title: "Memories",
737
+ general_group: "General",
738
+ general_item: "Project memory",
734
739
  project_title: "Project memory",
735
740
  project_desc: "Durable facts at the project level. .apc/memory.md — read by agents and the super-agent.",
736
741
  project_ph: "# Project Memory\n\nStable facts that any agent should know…",
@@ -756,6 +761,7 @@ export const en = {
756
761
  workspaces_empty: "No projects. Add one with the button above.",
757
762
  sessions_title: "Sessions",
758
763
  sessions_desc: "Sessions from all engines (apx · claude · codex), newest first.",
764
+ sessions_desc_scoped: "Sessions in this project's folder ({path}), all engines, newest first.",
759
765
  sessions_all: "All engines",
760
766
  sessions_empty: "No sessions.",
761
767
  sessions_error: "Could not read sessions: {msg}",
@@ -709,6 +709,8 @@ export const es = {
709
709
  save_fields_success: "Overrides guardados.",
710
710
  save_meta_success: "Project metadata guardado.",
711
711
  no_data: "Sin datos.",
712
+ tab_settings: "Settings",
713
+ tab_project: "Project",
712
714
  },
713
715
 
714
716
  telegram: {
@@ -729,6 +731,9 @@ export const es = {
729
731
  },
730
732
 
731
733
  memories: {
734
+ sidebar_title: "Memorias",
735
+ general_group: "General",
736
+ general_item: "Memoria del proyecto",
732
737
  project_title: "Memoria del proyecto",
733
738
  project_desc: "Hechos durables a nivel proyecto. .apc/memory.md — la leen los agentes y el super-agente.",
734
739
  project_ph: "# Memoria del proyecto\n\nHechos estables que cualquier agente debería saber…",
@@ -754,6 +759,7 @@ export const es = {
754
759
  workspaces_empty: "Sin proyectos. Agregá uno con el botón de arriba.",
755
760
  sessions_title: "Sessions",
756
761
  sessions_desc: "Sesiones de todos los engines (apx · claude · codex), más nuevas primero.",
762
+ sessions_desc_scoped: "Sesiones en la carpeta de este proyecto ({path}), todos los engines, más nuevas primero.",
757
763
  sessions_all: "Todos los engines",
758
764
  sessions_empty: "Sin sesiones.",
759
765
  sessions_error: "No pude leer las sesiones: {msg}",
@@ -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>